LambdaとS3の無限ループを止める:再帰トリガーの原因と3つの対策

S3バケットへのアップロードをトリガーにLambdaが起動し、処理結果を同じバケットに書き戻す——この構成は一見シンプルだが、Lambda無限ループという本番障害の定番パターンだ。気づいたときにはLambdaの同時実行数が上限に張り付き、コストが爆発している。

TL;DR:Lambda S3再帰トリガー対策まとめ

対策難易度確実性適用場面
プレフィックス/サフィックスフィルタ入出力パスが明確に分離できる場合
出力バケットを完全分離新規設計・構成変更が可能な場合
オブジェクトメタデータで処理済みフラグ同一バケット・同一パスが必須の場合

なぜLambda S3再帰トリガーが発生するのか

S3のイベント通知は、バケット上で発生したオブジェクト操作イベント(s3:ObjectCreated:*など)をLambdaに配信する仕組みだ。Lambdaが処理結果を同じバケットにPutObjectすると、それ自体が新たなObjectCreatedイベントを発生させる。S3はそのイベントを再びLambdaに送り、Lambdaはまた書き込み、以降これが繰り返される。

graph LR User["ユーザー"] -->|"PutObject"| S3["S3バケット"] S3 -->|"ObjectCreated イベント"| Lambda["Lambda関数"] Lambda -->|"処理結果をPutObject"| S3 S3 -->|"再びObjectCreated"| Lambda Lambda -->|"また書き込み..."| S3 style S3 fill:#FF9900,color:#fff style Lambda fill:#FF6B6B,color:#fff
  1. Upload:ユーザーがオブジェクトをS3にアップロード
  2. Trigger:S3がObjectCreatedイベントをLambdaに送信
  3. Process & Write:LambdaがS3に処理済みオブジェクトをPutObject
  4. Re-trigger:PutObjectが新たなObjectCreatedイベントを発生させ、Lambdaが再起動
  5. Loop:以降、同じサイクルが無限に継続

AWSはこのパターンを認識しており、再帰ループ検出機能(Lambda Recursive Loop Detection)を提供しているが、これはあくまでセーフティネットであり、設計レベルで解決するのが正しいアプローチだ。

設定確認:現在のトリガー構成を把握する

対策を打つ前に、現在どのイベント通知が設定されているかを確認する。S3トリガーはS3バケット側で管理されるリソースベースの設定であり、aws lambda list-event-source-mappingsにはリストされない点に注意が必要だ。S3トリガーの確認は必ず以下のコマンドで行う。

# S3バケットのイベント通知設定を確認
aws s3api get-bucket-notification-configuration \
  --bucket your-bucket-name

出力例として、LambdaFunctionConfigurationss3:ObjectCreated:*がプレフィックスフィルタなしで設定されていれば、それが無限ループの根本原因だ。

# LambdaのリソースベースポリシーでS3からの呼び出し許可を確認
aws lambda get-policy \
  --function-name your-function-name \
  --query 'Policy' \
  --output text

対策1:プレフィックス/サフィックスフィルタで入出力を分離する

最もすぐに適用できる対策は、S3イベント通知にプレフィックスまたはサフィックスフィルタを設定し、Lambdaが書き込む出力オブジェクトがトリガー条件に一致しないようにすることだ。入力ファイルをinput/プレフィックス配下に置き、出力をoutput/配下に書き込む設計であれば、トリガーをinput/プレフィックスのみに限定できる。

🔽 通知設定JSONを展開する
{
  "LambdaFunctionConfigurations": [
    {
      "LambdaFunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:your-function-name",
      "Events": ["s3:ObjectCreated:*"],
      "Filter": {
        "Key": {
          "FilterRules": [
            {
              "Name": "prefix",
              "Value": "input/"
            }
          ]
        }
      }
    }
  ]
}
# 上記JSONをnotification.jsonとして保存後、適用する
aws s3api put-bucket-notification-configuration \
  --bucket your-bucket-name \
  --notification-configuration file://notification.json

この設定後、Lambdaの出力先を必ずoutput/プレフィックス配下に変更すること。出力先のプレフィックスがinput/と重複していれば、フィルタの意味がなくなる。

プレフィックスフィルタは『どのオブジェクトがトリガーになるか』を制御するが、『Lambdaが何を書き込むか』は制御しない。フィルタと出力先の両方を変更しないと対策は完結しない。

対策2:出力バケットを完全に分離する

設計変更が可能であれば、これが最も確実な対策だ。入力バケットと出力バケットを別々に用意し、Lambdaは入力バケットのイベントのみをトリガーとして受け取り、処理結果は出力バケットに書き込む。物理的にバケットが異なるため、出力の書き込みが入力トリガーを発火させる経路が存在しない。

# 出力用バケットをap-northeast-1に作成する例
aws s3api create-bucket \
  --bucket your-output-bucket-name \
  --region ap-northeast-1 \
  --create-bucket-configuration LocationConstraint=ap-northeast-1
# Lambda実行ロールに出力バケットへのPutObject権限を付与するポリシー例
# (aws iam put-role-policy で適用)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::your-output-bucket-name/*"
    },
    {
      "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::your-input-bucket-name/*"
    }
  ]
}
graph LR User["ユーザー"] -->|"PutObject"| InputBucket["input-bucket"] InputBucket -->|"ObjectCreated イベント"| Lambda["Lambda関数"] Lambda -->|"処理結果をPutObject"| OutputBucket["output-bucket"] OutputBucket -->|"トリガーなし"| NoTrigger["Lambdaは起動しない"] style InputBucket fill:#FF9900,color:#fff style OutputBucket fill:#3F8624,color:#fff style Lambda fill:#232F3E,color:#fff style NoTrigger fill:#cccccc,color:#333
  1. input-bucket:ユーザーのアップロード先。このバケットのみにS3トリガーを設定する
  2. Lambda:input-bucketのObjectCreatedイベントで起動し、処理を実行
  3. output-bucket:処理結果の書き込み先。このバケットにはS3トリガーを設定しない

対策3:オブジェクトメタデータで処理済みフラグを管理する

同一バケット・同一プレフィックスが業務要件として外せない場合、Lambdaの冒頭でオブジェクトのメタデータを確認し、処理済みであれば即座に終了する方法がある。ただし、この方法はLambdaが一度は起動するため、コスト削減効果はなく、あくまで無限ループを止めるための最終手段として位置づけること。

🔽 Pythonコード例を展開する
import boto3

s3 = boto3.client('s3')

def lambda_handler(event, context):
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = event['Records'][0]['s3']['object']['key']

    # オブジェクトのメタデータを取得
    response = s3.head_object(Bucket=bucket, Key=key)
    metadata = response.get('Metadata', {})

    # 処理済みフラグが立っていれば即終了
    if metadata.get('processed') == 'true':
        print(f'Skipping already-processed object: {key}')
        return

    # --- ここに実際の処理を記述 ---
    # processed_content = do_something(bucket, key)

    # 処理済みフラグをメタデータに付与して上書き保存
    # ※ S3はオブジェクトのメタデータ単独更新不可。copy_objectで自己コピーする
    s3.copy_object(
        Bucket=bucket,
        CopySource={'Bucket': bucket, 'Key': key},
        Key=key,
        Metadata={'processed': 'true'},
        MetadataDirective='REPLACE'
    )

S3はオブジェクトのメタデータを単独で更新するAPIを持たない。メタデータを変更するにはcopy_objectで同一キーに自己コピーする必要があり、このcopy_object自体がObjectCreatedイベントを発生させる。つまり、このコードだけでは2回目のLambda起動が発生する——2回目はprocessed: trueフラグで即終了するため無限ループにはならないが、余分な起動コストは残る。

実際の障害パターン:誤診から正しい診断へ

よくある誤診はこうだ。CloudWatchのLambda同時実行数メトリクスが急上昇しているのを見て、『コードのバグでスレッドが詰まっている』と判断し、タイムアウト値を短くする対処を行う。タイムアウトを短くしても同時実行数は下がらない——むしろLambdaが短時間で終了するぶん、次のトリガーが早く来て状況が悪化することもある。

正しい診断の手順はシンプルだ。CloudWatch LogsでLambdaのログを確認し、同じオブジェクトキーに対して繰り返しINVOCATIONが記録されていれば再帰トリガーが確定する。

# 直近1時間のLambdaログをフィルタして確認
aws logs filter-log-events \
  --log-group-name /aws/lambda/your-function-name \
  --start-time $(date -d '1 hour ago' +%s000) \
  --filter-pattern '"ObjectCreated"' \
  --query 'events[*].message' \
  --output text

同じS3キーが繰り返しログに現れていれば、それが証拠だ。タイムアウトではなくトリガー設定の問題として対処する。

緊急停止:ループが今まさに走っている場合

無限ループが進行中でコストが積み上がっているなら、まずLambdaの同時実行数を0に設定してトリガーへの応答を止める。これはコードの変更なしに即座に適用できる。

# Lambda同時実行数を0に設定して即座に停止
aws lambda put-function-concurrency \
  --function-name your-function-name \
  --reserved-concurrent-executions 0
# 対策適用後、同時実行数制限を解除する
aws lambda delete-function-concurrency \
  --function-name your-function-name

同時実行数を0にするとLambdaはスロットリングされ、S3からのイベントはLambdaに配信されなくなる。S3はイベント配信失敗時に再試行するため、イベント自体はキューに残る可能性がある点は把握しておくこと。対策を適用してから同時実行数制限を解除する順序を守ること。

Lambda S3再帰トリガー対策のまとめと次のステップ

Lambda S3再帰トリガーの根本原因は、トリガー条件と出力先が重複していることだ。新規設計であれば出力バケットの分離が最もシンプルで確実。既存構成への対処ならプレフィックスフィルタが即効性がある。どちらの対策も、設定変更後にaws s3api get-bucket-notification-configurationでフィルタが正しく反映されているかを必ず確認すること。

用語集

用語説明
S3イベント通知S3バケット上のオブジェクト操作(作成・削除など)を検知して、Lambda・SQS・SNSなどに通知する仕組み
プレフィックスフィルタS3イベント通知の送信対象をオブジェクトキーの前方一致文字列で絞り込む設定
リソースベーストリガーLambda側ではなくイベントソース側(S3など)で設定・管理されるトリガー。イベントソースマッピングとは異なる
同時実行数(Reserved Concurrency)特定のLambda関数が同時に実行できるインスタンス数の上限。0に設定するとスロットリングで実行を止められる
再帰ループ検出AWSがLambdaの再帰的な自己呼び出しを検出して停止するセーフティ機能。設計レベルの対策の代替にはならない

コメント

このブログの人気の投稿

カスタムVPCのEC2インターネット接続不可を解決する — Internet GatewayとRoute Tableの設定手順

EC2 SSH接続タイムアウトの原因と修正方法 — セキュリティグループのインバウンドルール完全ガイド

S3パブリックアクセス拒否の原因と解決策:バケットレベルの「Block Public Access」が優先される仕組み