AWS 고급 #3 Lambda 기초

9 분 소요

#1 ECS / Fargate, #2 ECR컨테이너가 항상 떠 있는 모델이었습니다. 트래픽이 0일 때도 컨테이너 1개는 살아 있습니다. 트래픽 변동이 크거나, 짧은 처리만 필요하거나, 운영 부담을 더 줄이고 싶을 때는 다른 선택지가 어울립니다. 바로 Lambda입니다.

이번 글은 Lambda의 동작 방식, 모델 (runtime / handler / event), 호출 방식, 콜드 스타트, 동시성과 한도, 로깅까지 한 번에 정리하겠습니다.

Lambda가 하는 일 #

AWS Lambda는 서버리스 함수 실행 플랫폼입니다. 이벤트가 들어오면 그제서야 함수가 깨어나고, 끝나면 다시 사라집니다. 트래픽이 0이면 비용도 0입니다.

Lambda의 그림
이벤트 (HTTP / S3 업로드 / SQS 메시지 / Cron)
Lambda가 컨테이너 hot 또는 cold로 띄움
내 핸들러 함수 실행 (수 ms ~ 15분)
응답 / 결과 → 호출자
일정 시간 idle 후 컨테이너 종료

언제 Lambda가 어울리는가 #

Lambda가 어울리는 경우 #

  • 이벤트 주도. S3 업로드 → 썸네일 생성, SQS 메시지 → 처리, EventBridge 스케줄 → 배치
  • 변동 큰 트래픽. 평소 0, 가끔 폭증. 0일 때 비용 안 듦
  • 짧은 처리. 보통 수 초 ~ 수 분
  • 사이드 / 보조 워크로드. 메인 시스템 옆의 보조 함수
  • API의 일부만. 모든 API가 Lambda 일 필요 없음

Lambda가 맞지 않는 경우 #

경우이유
항상 트래픽이 도는 큰 API동시성 / 콜드 스타트 / 비용에서 ECS가 유리
15분 넘는 처리Lambda 한 호출은 최대 15분
매우 큰 메모리 / GPULambda는 최대 10GB 메모리, GPU 없음
Stateful 연결 (WebSocket의 백엔드 등)가능하지만 설계 복잡
항상 켜져있는 DB connection pool호출마다 새로 연결되는 방식

비교 #

EC2ECS / FargateLambda
켜져 있는 시간항상항상 (Service)호출마다
운영 부담중간 (Fargate 작음)작음
콜드 스타트없음작음있음 (수십 ms ~ 수 초)
시간 한도무한무한15분
트래픽 0 비용중간0
동시 처리OS 레벨컨테이너 1개에 다중함수 인스턴스 1개당 동시 1개

마지막 줄이 중요: Lambda는 함수 인스턴스 1개가 동시에 1개의 호출만 처리. 동시 호출이 N 개면 인스턴스도 N 개로 자동 확장.

첫 Lambda: Hello, World #

함수 만들기 (콘솔) #

콘솔 → Lambda → “함수 생성” → 직접 작성 → Python 3.13.

lambda_function.py (콘솔의 기본)
def lambda_handler(event, context):
    return {
        "statusCode": 200,
        "body": "Hello, Lambda"
    }

저장하고 “테스트”, 빈 이벤트로 호출. CloudWatch Logs에 자동으로 로그 그룹 (/aws/lambda/<함수이름>) 생성.

CLI로 만들기 #

코드를 zip으로 묶어:

zip + 생성
zip function.zip lambda_function.py

aws lambda create-function \
  --function-name hello \
  --runtime python3.13 \
  --role arn:aws:iam::123456789012:role/lambda-basic-role \
  --handler lambda_function.lambda_handler \
  --zip-file fileb://function.zip

IaC (Terraform 살짝) #

terraform
resource "aws_lambda_function" "hello" {
  function_name = "hello"
  runtime       = "python3.13"
  handler       = "lambda_function.lambda_handler"
  role          = aws_iam_role.lambda.arn
  filename      = "function.zip"
  source_code_hash = filebase64sha256("function.zip")

  memory_size = 256
  timeout     = 10
}

모델: Runtime / Handler / Event / Context #

Lambda의 모델을 네 키워드로 정리.

Runtime #

언어 + 버전. 매니지드 runtime:

  • Python (3.10 ~ 3.13)
  • Node.js (18, 20, 22)
  • Java (8, 11, 17, 21)
  • .NET, Ruby, Go (provided.al2023 위)
  • Custom Runtime. Rust / Zig / Swift 등 직접 지원

또는 **컨테이너 이미지 (ECR)**로 배포하는 방법도 있습니다. 위의 #2 ECR과 자연스럽게 연결되며, 큰 의존성이 있을 때 유리합니다 (zip 한도 250MB, 컨테이너는 10GB).

Handler #

Lambda가 호출할 함수의 이름으로, <파일이름>.<함수이름> 형식으로 지정합니다.

myapp/handler.py
def main(event, context):
    ...

→ Handler 설정: myapp.handler.main

Event #

호출자가 보낸 데이터. JSON 객체. 호출 소스마다 모양이 다름.

API Gateway의 event
{
  "version": "2.0",
  "routeKey": "GET /hello",
  "headers": {...},
  "queryStringParameters": {...},
  "body": "..."
}
S3 ObjectCreated의 event
{
  "Records": [
    {
      "eventSource": "aws:s3",
      "s3": {
        "bucket": {"name": "my-bucket"},
        "object": {"key": "uploads/photo.jpg"}
      }
    }
  ]
}

Lambda Powertools (Python / TypeScript / Java) 같은 라이브러리가 이 event 들을 타입 안전하게 다루는 데 도움이 됩니다. 실전에서는 적극 활용하세요.

Context #

런타임 정보. 함수 실행 시간 한도 (get_remaining_time_in_millis()), request id, 함수 이름 등.

context가 주는 정보
def handler(event, context):
    print(context.aws_request_id)
    print(context.function_name)
    print(context.get_remaining_time_in_millis())  # 남은 시간 ms

호출 방식: 동기 vs 비동기 vs 스트림 #

호출 소스에 따라 Lambda의 동작이 다릅니다.

1) 동기 (Synchronous) #

호출자가 결과를 기다림. 응답을 받을 때까지 블록.

호출 소스
API Gateway
ALB
Cognito
직접 Invoke API
aws lambda invoke \
  --function-name hello \
  --payload '{"name": "world"}' \
  --cli-binary-format raw-in-base64-out \
  out.json

2) 비동기 (Asynchronous) #

호출자가 “큐에 넣고” 끝. Lambda가 백그라운드로 처리. 실패 시 자동 재시도 (기본 2회) + DLQ (Dead Letter Queue)로 이송.

호출 소스
S3 ObjectCreated
SNS
EventBridge
InvocationType=Event의 Invoke
aws lambda invoke \
  --function-name hello \
  --invocation-type Event \
  --payload '{"key": "value"}' \
  --cli-binary-format raw-in-base64-out \
  out.json

3) 스트림 / 폴링 #

Lambda가 큐 / 스트림을 자동 폴링.

호출 소스
SQS
DynamoDB Streams
Kinesis
MSK (Managed Kafka)

설정만 해두면 새 메시지가 오는 대로 Lambda가 batch로 받아 처리. 실패하면 retry + DLQ.

동시성 (Concurrency)의 의미 #

Lambda의 가장 중요한 한 가지입니다. 함수 인스턴스 1개가 동시 1 호출만 처리하니, 동시에 들어온 호출 수 = 띄워진 인스턴스 수.

동시 호출의 흐름
초당 10개 호출, 각 호출 1초 → 동시 ~10개 인스턴스
초당 100개 호출, 각 호출 100ms → 동시 ~10개 인스턴스

계정 한도 (Account Concurrency) #

리전별 기본값은 1,000입니다. 운영 워크로드에서는 종종 부족하므로, Service Quotas 콘솔에서 증액을 요청하세요.

Reserved Concurrency #

특정 함수에 “최대 N 개” 를 보장 + 제한.

이 함수는 최대 100개
aws lambda put-function-concurrency \
  --function-name hello \
  --reserved-concurrent-executions 100

용도:

  • 위험한 함수의 폭주 차단 (예: 유료 외부 API 호출하는 함수)
  • 다른 중요 함수에 동시성 여유 남기기 (이 함수가 1,000 다 차지 못 하게)
  • DB connection 폭주 방지 (RDS connection pool 보호)

Provisioned Concurrency: 콜드 스타트 회피 #

미리 N 개 인스턴스를 데워둠. 동시 호출이 N이하면 콜드 스타트 0.

aws lambda put-provisioned-concurrency-config \
  --function-name hello \
  --qualifier prod \
  --provisioned-concurrent-executions 10

비용: 데워둔 인스턴스 시간만큼 과금 (약간 저렴한 단가). API의 입구라 콜드 스타트가 사용자 경험에 직접 영향이라면 검토.

콜드 스타트: 가장 자주 만나는 함정 #

함수 인스턴스가 새로 만들어질 때 초기 비용이 발생합니다. 두 단계로 구분됩니다.

콜드 스타트의 분해
[INIT 단계] : 한 번만
  ├─ 컨테이너 환경 준비
  ├─ runtime 시작
  ├─ 핸들러 모듈 import
  └─ 핸들러 함수 외부의 코드 실행 (전역)
[INVOKE 단계] : 매 호출마다
  └─ 핸들러 함수 실행

INIT은 인스턴스의 첫 호출에만 비용. 같은 인스턴스가 다음 호출을 받으면 INIT 건너뜀 (warm).

콜드 스타트 시간 #

대략적인 구간:

언어 / 형태INIT 시간
Python (작은 의존성)~150 ms
Python (큰 의존성, e.g. boto3 + pandas)~1 ~ 2초
Node.js (작은)~100 ms
Java~500 ms ~ 수 초
컨테이너 이미지 (큰)~수 초

콜드 스타트 줄이기 #

  1. 메모리 늘리기. Lambda는 메모리에 비례해 vCPU도 늘어남. 256MB → 1024MB만 해도 INIT이 빨라짐
  2. 의존성 슬리밍. 안 쓰는 패키지 빼기, tree-shaking
  3. 전역 변수 활용. 핸들러 외부에서 한 번 만든 객체 (boto3 client, DB connection 등)를 warm 한 인스턴스가 재사용
  4. Provisioned Concurrency. 위에서 본 대로
  5. Lambda SnapStart (Java). INIT 결과를 스냅샷 떠서 빠른 복구. Java 한정

전역 변수 패턴 #

좋은 패턴 : 전역에서 client 만들기
import boto3

# Lambda 인스턴스 한 번만 : INIT 단계
s3 = boto3.client("s3")

def handler(event, context):
    # 핸들러는 깨끗하게 : 매 호출마다 client 안 만듦
    return s3.list_buckets()
나쁜 패턴
def handler(event, context):
    # 매 호출마다 boto3 client : 느림
    s3 = boto3.client("s3")
    return s3.list_buckets()

메모리와 시간 한도 #

항목한도
메모리128MB ~ 10,240MB (1MB 단위)
시간1초 ~ 900초 (15분)
임시 디스크 (/tmp)512MB ~ 10GB
환경 변수4KB
Payload (동기)6MB
Payload (비동기)256KB
zip (소스)50MB (압축)
zip (압축 해제)250MB
컨테이너 이미지10GB

메모리는 vCPU와 묶여 있습니다. 1,769MB에서 vCPU 1개 분량입니다. 메모리를 늘리면 CPU도 같이 늘어 함수가 빨라지는 경우가 흔합니다. 메모리만 늘려도 비용이 줄 수 있습니다 (Lambda Power Tuning으로 최적값 찾기).

로깅: CloudWatch Logs #

Lambda의 모든 stdout / stderr는 자동으로 CloudWatch Logs/aws/lambda/<함수이름> 로그 그룹에 들어갑니다.

로깅
import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)

def handler(event, context):
    logger.info("Hello %s", event.get("name"))
    return "ok"

운영에선 구조화 로그 권장. JSON으로 출력하면 CloudWatch Logs Insights에서 필드별 쿼리 가능.

JSON 로그
import json, logging

logger = logging.getLogger()

def handler(event, context):
    logger.info(json.dumps({
        "request_id": context.aws_request_id,
        "user_id": event.get("user_id"),
        "action": "process",
        "duration_ms": 42
    }))

PowertoolsLogger가 이걸 한 줄로 깔끔하게 해줍니다.

Layers: 코드 재사용 #

여러 함수가 같은 의존성 / 유틸리티를 쓸 때 Lambda Layer로 분리해 공유합니다.

Layer 만들기
# python의존성
mkdir -p python
pip install requests -t python/
zip -r layer.zip python

aws lambda publish-layer-version \
  --layer-name my-utils \
  --zip-file fileb://layer.zip \
  --compatible-runtimes python3.13
함수에 붙이기
aws lambda update-function-configuration \
  --function-name hello \
  --layers arn:aws:lambda:ap-northeast-2:123456789012:layer:my-utils:1

장점: 함수 zip 작아짐, 의존성 한 번만 업데이트. 단점: 너무 많아지면 추적 어려움. 5개 한도.

Lambda 비용 #

Lambda 비용
호출당 비용 = (호출 수 × $0.0000002)
            + (실행시간 GB-초 × $0.0000166667)

월 100만 호출 + 호출당 100ms + 256MB:

  • 호출 비용: 100만 × $0.0000002 = $0.20
  • 시간 비용: 100만 × 0.1초 × 0.25GB × $0.0000166667 = $0.42
  • 총: ~$0.62 / 월

매우 저렴합니다. 무료 티어로 월 100만 호출 + 400,000GB-초 무료. 작은 워크로드는 사실상 0.

비용이 커지는 경우:

  • 호출당 시간이 길고, 메모리가 큰 함수 (e.g. 5분 × 3GB)
  • 동시성이 항상 높은 함수. ECS / Fargate가 더 쌀 수 있음

자주 만나는 함정 #

1) 콜드 스타트로 첫 호출 느림 #

API 입구의 Lambda가 0.5 ~ 2초씩 걸리면 사용자가 버티기 어렵습니다. 위의 “콜드 스타트 줄이기” 5가지와 Provisioned Concurrency를 검토하세요.

2) 핸들러 안에서 매번 객체 생성 #

def handler(event, context):
    db = create_db_connection()
    boto = boto3.client("s3")
    ...

매 호출마다 100ms의 낭비. 전역으로 끌어내기.

3) RDS connection pool 폭주 #

Lambda 동시 100개 → DB 연결 100개 → DB가 connection 한도 초과. 대안:

  • RDS Proxy. Lambda 들이 공유하는 connection pool
  • Reserved Concurrency로 함수 동시성 제한
  • DynamoDB 같은 서버리스 DB 사용

4) 비동기 호출의 실수가 조용히 사라짐 #

S3 → Lambda가 실패해도 호출자는 모름. DLQ (SQS) 또는 Lambda Destination으로 실패 캡처.

비동기 실패를 SQS DLQ로
aws lambda put-function-event-invoke-config \
  --function-name hello \
  --maximum-retry-attempts 2 \
  --destination-config '{"OnFailure":{"Destination":"arn:aws:sqs:...:dlq"}}'

5) 시간 초과로 잘려 나감 #

15분 한도 모르고 큰 처리를 한 함수에. 14분 59초에 강제 종료. 긴 처리는 #7 Step Functions로 분할 또는 ECS / Fargate.

6) Payload가 6MB 넘음 #

API Gateway → Lambda 동기 호출의 한도. 큰 파일은 S3 presigned URL 패턴으로 우회. Lambda가 presigned URL만 발급, 클라이언트가 직접 S3에 업로드.

7) 환경 변수에 비밀 #

평문 환경변수에 DB 비밀번호 → 로그 / 콘솔에서 노출. **#6 Secrets Manager**로 옮기기.

정리 #

이번 글에서 잡은 것:

  • Lambda의 쓰임새. 이벤트 주도, 변동 큰 트래픽, 짧은 처리, 사이드 워크로드. 트래픽 0이면 비용 0
  • Lambda가 안 맞는 경우. 항상 큰 트래픽, 15분 초과, 큰 메모리 / GPU, stateful
  • 모델. Runtime / Handler / Event / Context. Event 모양은 호출 소스마다
  • 호출 방식. 동기 / 비동기 / 스트림. 비동기는 자동 retry + DLQ
  • 동시성. 함수 인스턴스 1개 = 동시 1 호출. 호출이 늘면 인스턴스 자동 확장
  • Reserved Concurrency. 폭주 차단 + 다른 함수 보호
  • Provisioned Concurrency. 콜드 스타트 회피
  • 콜드 스타트. INIT의 비용. 메모리 늘리기 / 의존성 슬리밍 / 전역 변수 / SnapStart / Provisioned으로 완화
  • 한도. 메모리 10GB, 시간 15분, payload 6MB, 컨테이너 10GB
  • 로깅. CloudWatch Logs 자동. JSON 구조화 로그 권장
  • Layers. 의존성 공유 (5개 한도)
  • 비용. 호출 + 실행 시간. 작은 워크로드는 거의 0
  • 함정. 콜드 스타트, 핸들러 안 객체 생성, RDS 폭주 (RDS Proxy), 비동기 실패 누락 (DLQ), 15분 한도, Payload 6MB, 환경변수 비밀

다음: API Gateway + Lambda #

함수만 있으면 호출할 경로가 부족합니다. HTTP 요청으로 Lambda를 부르는 가장 흔한 패턴, API Gateway + Lambda를 다음 글에서.

#4 API Gateway + Lambda에서는 REST API와 HTTP API의 차이, Lambda 통합, 라우트 / 메소드, 권한 (IAM / Cognito / Lambda authorizer), 스테이지 / 배포까지, 서버리스 API의 쓰임새를 한 번에 정리하겠습니다.

X