Lambda S3 무한 루프 완전 차단 가이드 — 재귀 트리거 방지 실전
S3 버킷에 파일을 업로드하면 Lambda가 실행되고, Lambda가 처리 결과를 같은 버킷에 저장하면 또 Lambda가 실행된다. 이 Lambda S3 무한 루프는 처음 설계할 때는 눈에 띄지 않다가, 프로덕션에서 갑자기 Lambda 동시 실행 한도를 소진하거나 S3 요청 비용이 폭발적으로 증가하면서 발견된다.
TL;DR — Lambda S3 재귀 트리거 차단 방법 요약
| 방법 | 핵심 원리 | 적용 난이도 | 권장 여부 |
|---|---|---|---|
| 출력 접두사(Prefix) 분리 | 입력/출력 경로를 다르게 설정해 트리거 조건 자체를 제거 | 낮음 | ✅ 1순위 |
| 출력 버킷 분리 | 처리 결과를 별도 버킷에 저장 | 낮음 | ✅ 1순위 |
| 객체 메타데이터 확인 | Lambda가 직접 처리 여부를 메타데이터로 판별 후 조기 종료 | 중간 | ⚠️ 보조 수단 |
| 객체 태그 확인 | 처리 완료 태그가 있으면 즉시 반환 | 중간 | ⚠️ 보조 수단 |
Lambda S3 재귀 트리거가 발생하는 구조
S3 이벤트 알림은 버킷 단위로 설정된다. 특정 접두사나 접미사 필터를 걸지 않으면, 버킷 내 모든 ObjectCreated 이벤트가 Lambda를 호출한다. Lambda가 처리 결과를 같은 버킷에 쓰는 순간 새로운 ObjectCreated 이벤트가 발생하고, 이 이벤트가 다시 Lambda를 호출한다.
- 사용자 업로드:
raw/input.csv가 S3에 생성된다. - S3 이벤트 알림:
ObjectCreated이벤트가 Lambda를 호출한다. - Lambda 처리: 파일을 변환하고
processed/output.csv를 같은 버킷에 저장한다. - 재귀 트리거:
processed/output.csv생성이 또 다른ObjectCreated이벤트를 발생시킨다. - 무한 반복: 3→4→3→4가 Lambda 동시 실행 한도에 도달할 때까지 반복된다.
AWS는 이 문제를 인지하고 있으며, 공식 문서에서도 같은 버킷을 입출력으로 사용할 때 접두사 필터를 반드시 적용하도록 명시하고 있다.
마치 거울 두 개를 마주 보게 세워두는 것과 같다. 반사가 무한히 이어지는 건 물리 법칙이지, 버그가 아니다. 설계 자체를 바꿔야 한다.
해결 방법 1: 접두사(Prefix) 분리 — 가장 확실한 차단
S3 이벤트 알림의 필터 조건을 수정해서, Lambda가 입력 경로의 이벤트에만 반응하도록 제한한다. Lambda의 출력 경로가 트리거 조건에 포함되지 않으면 재귀 자체가 성립하지 않는다. 코드 변경 없이 인프라 설정만으로 해결되므로 가장 먼저 적용해야 한다.
현재 S3 이벤트 알림 설정 확인
aws s3api get-bucket-notification-configuration \
--bucket my-processing-bucket
출력에서 Filter 항목이 없거나 접두사 조건이 없으면 무한 루프 구조가 확정된다.
접두사 필터 적용
아래 notification.json을 작성한다. Lambda는 raw/ 접두사 객체 생성 이벤트에만 반응하고, Lambda가 결과를 저장하는 processed/ 경로는 트리거 조건에서 제외된다.
🔽 notification.json 전체 보기
{
"LambdaFunctionConfigurations": [
{
"LambdaFunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:MyProcessingFunction",
"Events": ["s3:ObjectCreated:*"],
"Filter": {
"Key": {
"FilterRules": [
{
"Name": "prefix",
"Value": "raw/"
}
]
}
}
}
]
}
aws s3api put-bucket-notification-configuration \
--bucket my-processing-bucket \
--notification-configuration file://notification.json
적용 후 Lambda가 결과를 저장할 경로를 코드에서도 processed/로 고정해야 한다.
import boto3
s3 = boto3.client('s3')
def lambda_handler(event, context):
record = event['Records'][0]
source_bucket = record['s3']['bucket']['name']
source_key = record['s3']['object']['key']
# raw/ 접두사 외의 이벤트가 혹시 들어오면 즉시 반환 (방어 코드)
if not source_key.startswith('raw/'):
print(f'Skipping non-raw key: {source_key}')
return
# 파일 처리 로직
response = s3.get_object(Bucket=source_bucket, Key=source_key)
content = response['Body'].read()
processed_content = process(content) # 실제 처리 함수
# 출력은 반드시 processed/ 경로에 저장
output_key = source_key.replace('raw/', 'processed/', 1)
s3.put_object(
Bucket=source_bucket,
Key=output_key,
Body=processed_content
)
def process(content):
# 실제 변환 로직 구현
return content
해결 방법 2: 출력 버킷 분리 — 구조적으로 가장 깔끔한 설계
입력 버킷과 출력 버킷을 완전히 분리하면 이벤트 알림 필터 설정 실수 자체가 불가능해진다. 팀 규모가 크거나 여러 Lambda가 같은 버킷을 공유하는 환경이라면 이 방법이 장기적으로 더 안전하다.
(트리거 설정됨)"] InputBucket -->|"ObjectCreated 이벤트"| Lambda["Lambda 함수"] Lambda -->|"처리 결과 저장"| OutputBucket["my-output-bucket
(트리거 없음)"] style OutputBucket fill:#2ecc71,color:#fff style InputBucket fill:#3498db,color:#fff
- 입력 버킷:
my-input-bucket에만 S3 이벤트 알림을 설정한다. - Lambda: 입력 버킷에서 파일을 읽어 처리한다.
- 출력 버킷: 처리 결과를
my-output-bucket에 저장한다. 이 버킷에는 Lambda 트리거가 없다.
aws s3api put-bucket-notification-configuration \
--bucket my-input-bucket \
--notification-configuration file://input-notification.json
Lambda IAM 실행 역할에 출력 버킷 쓰기 권한을 추가해야 한다.
🔽 Lambda 실행 역할 IAM 정책 예시
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::my-input-bucket/*"
},
{
"Effect": "Allow",
"Action": ["s3:PutObject"],
"Resource": "arn:aws:s3:::my-output-bucket/*"
}
]
}
해결 방법 3: 객체 메타데이터 확인 — 보조 방어 레이어
접두사 분리가 불가능한 레거시 구조에서 사용하는 방법이다. Lambda가 저장하는 객체에 커스텀 메타데이터를 추가하고, 트리거될 때 해당 메타데이터가 있으면 즉시 반환한다. 단독으로 사용하기보다 방법 1이나 2와 함께 적용하는 보조 수단으로 사용해야 한다 — S3 HeadObject 호출 비용이 추가되고, 메타데이터 확인 전에 이미 Lambda 호출 비용이 발생하기 때문이다.
import boto3
s3 = boto3.client('s3')
PROCESSED_MARKER = 'x-amz-meta-processed-by-lambda'
def lambda_handler(event, context):
record = event['Records'][0]
source_bucket = record['s3']['bucket']['name']
source_key = record['s3']['object']['key']
# 객체 메타데이터 확인
head = s3.head_object(Bucket=source_bucket, Key=source_key)
if head.get('Metadata', {}).get('processed-by-lambda') == 'true':
print(f'Already processed: {source_key}. Skipping.')
return
# 파일 처리 로직
response = s3.get_object(Bucket=source_bucket, Key=source_key)
content = response['Body'].read()
processed_content = process(content)
# 처리 완료 마커 메타데이터와 함께 저장
s3.put_object(
Bucket=source_bucket,
Key=source_key,
Body=processed_content,
Metadata={'processed-by-lambda': 'true'}
)
def process(content):
return content
메타데이터 키는 S3가 소문자로 정규화한다. head_object 응답의 Metadata 딕셔너리에서 키를 참조할 때 소문자로 확인해야 한다.
실제 무한 루프 발생 시 긴급 차단 절차
이미 루프가 돌고 있다면 Lambda 동시 실행 수를 즉시 0으로 제한해서 추가 호출을 막는 것이 첫 번째 조치다. S3 이벤트 알림을 수정하는 것보다 빠르다.
# 1단계: Lambda 동시 실행 수를 0으로 제한 (즉시 차단)
aws lambda put-function-concurrency \
--function-name MyProcessingFunction \
--reserved-concurrent-executions 0
# 2단계: S3 이벤트 알림 수정 (접두사 필터 적용)
aws s3api put-bucket-notification-configuration \
--bucket my-processing-bucket \
--notification-configuration file://notification.json
# 3단계: Lambda 동시 실행 제한 해제
aws lambda delete-function-concurrency \
--function-name MyProcessingFunction
1단계에서 reserved-concurrent-executions를 0으로 설정하면 Lambda가 완전히 스로틀링되어 새 호출이 거부된다. 이미 실행 중인 인스턴스는 완료될 때까지 계속 실행되므로, 실행 중인 함수가 추가 객체를 생성하지 않는지 CloudWatch Logs에서 확인한다.
경험에서 나온 진단 — 증상, 오진, 실제 원인
무한 루프가 처음 발생했을 때 가장 흔한 오진은 'Lambda 타임아웃 설정이 너무 짧아서 재시도가 발생한다'는 판단이다. CloudWatch Metrics에서 Invocations가 급격히 증가하고 Duration은 정상 범위인 것을 보면 재시도가 아니라 새로운 호출이 계속 발생하고 있다는 것을 알 수 있다.
# Lambda 호출 수 급증 확인
aws cloudwatch get-metric-statistics \
--namespace AWS/Lambda \
--metric-name Invocations \
--dimensions Name=FunctionName,Value=MyProcessingFunction \
--start-time 2024-01-15T10:00:00Z \
--end-time 2024-01-15T10:30:00Z \
--period 60 \
--statistics Sum
호출 수가 기하급수적으로 증가하는 패턴이 보이면, S3 이벤트 알림 설정을 즉시 확인한다. 재시도는 일정한 간격으로 제한된 횟수만 발생하지만, 재귀 트리거는 Lambda 동시 실행 한도에 도달할 때까지 가속된다.
실제 원인 확인은 S3 서버 액세스 로그나 CloudTrail에서 PutObject 이벤트의 userAgent를 보면 된다. Lambda 실행 역할의 ARN이 userAgent나 요청자 정보에 나타나면 Lambda가 직접 객체를 생성하고 있는 것이 확정된다.
# CloudTrail에서 S3 PutObject 이벤트 확인
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=PutObject \
--start-time 2024-01-15T10:00:00Z \
--end-time 2024-01-15T10:15:00Z \
--max-results 10
Lambda 실행 역할 ARN이 요청자로 반복 등장하면 재귀 트리거가 확정이다.
Lambda S3 재귀 트리거 방지 — 마무리 및 다음 단계
Lambda S3 무한 루프를 방지하는 가장 확실한 방법은 접두사 분리 또는 버킷 분리로 트리거 조건 자체를 제거하는 것이다. 메타데이터 확인은 보조 방어 레이어로만 사용한다. 이미 루프가 발생했다면 Lambda 동시 실행 수를 0으로 제한해 즉시 차단한 뒤 이벤트 알림을 수정한다.
핵심 용어 정리
| 용어 | 설명 |
|---|---|
| S3 이벤트 알림 (Event Notification) | S3 버킷에서 특정 이벤트 발생 시 Lambda, SQS, SNS 등을 호출하는 기능 |
| ObjectCreated 이벤트 | PUT, POST, COPY, CompleteMultipartUpload 등으로 객체가 생성될 때 발생하는 S3 이벤트 |
| 접두사 필터 (Prefix Filter) | S3 이벤트 알림이 반응할 객체 키의 시작 경로를 제한하는 필터 조건 |
| 예약 동시 실행 수 (Reserved Concurrency) | 특정 Lambda 함수에 할당된 최대 동시 실행 인스턴스 수. 0으로 설정하면 함수가 완전히 스로틀링됨 |
| 재귀 트리거 (Recursive Trigger) | Lambda의 출력이 자기 자신을 다시 호출하는 이벤트를 생성하는 구조적 순환 패턴 |
댓글
댓글 쓰기