【ITニュース解説】Fix Lambda S3 Trigger Error Handling Before It Loses Events
2026年08月22日に「Dev.to」が公開したITニュース「Fix Lambda S3 Trigger Error Handling Before It Loses Events」について初心者にもわかりやすく解説しています。
ITニュース概要
AWS LambdaのS3トリガーは、適切なエラー処理設計をしないとデータ消失のリスクがある。特に、再試行の仕組み、イベントの複数回配信、バッチ処理でのエラー分離に注意が必要だ。冪等性の確保やDLQ(デッドレターキュー)の設定は必須で、堅牢なシステム構築にはこれらの考慮が重要となる。
ITニュース解説
アマゾンウェブサービス(AWS)のS3(Simple Storage Service)にファイルがアップロードされたことをきっかけに、Lambda関数を起動して何らかの処理を行うシステムは非常に一般的で便利だ。しかし、この S3-Lambda トリガーの仕組みを深く理解せずに設計すると、予期せぬデータの消失やシステム障害につながる可能性がある。多くの人は、このトリガー部分を「ただ動く配管」のように考えてしまいがちだが、その背後には重要な設計上の考慮点がある。
あるシステムでは、8ヶ月間問題なく動作していたデータ取り込みパイプラインが、パートナーからの大量ファイルアップロードによって突然、約3%のデータを失う事態に見舞われた。ログにもエラーはなく、アラートも発生しなかった。原因はアプリケーションコードのバグではなく、S3-Lambdaトリガーのイベント処理とエラーハンドリングが適切に設計されていなかったことだった。それまではたまたまリトライが成功していたため、問題が表面化しなかったのだ。
S3がオブジェクトを検知すると、イベント通知が発行され、Lambdaを呼び出す。このイベント配信は非同期で行われ、「少なくとも一回」という特性を持つ。つまり、S3はLambdaが処理を終えるのを待たず、同じイベントが複数回Lambdaに送られる可能性がある。これは異常なケースではなく、仕様として認識しておく必要がある。また、Lambdaが受け取るのはS3に保存されたファイル本体ではなく、バケット名やオブジェクトキー、サイズ、eTagといったメタデータだけだ。実際のファイル内容を処理するには、Lambda関数内で改めてS3からファイルを読み込む必要がある。
Lambdaの呼び出しは非同期であるため、リトライの仕組みも重要となる。S3自体はイベントのリトライを行わないが、Lambdaの非同期呼び出しレイヤーがデフォルトで2回まで自動リトライする。この2回のリトライ後も処理が失敗した場合、デッドレターキュー(DLQ)や失敗先が設定されていなければ、イベントは「サイレントに破棄」されてしまう。つまり、何の痕跡も残さずにデータが消えてしまうのだ。さらに、S3は負荷が高い状況などで複数のイベントをまとめて一つのLambda呼び出しとして送ることがある。そのため、Lambdaハンドラーが event['Records'] に常に1つのレコードしかないと仮定してコードを書くと、データを取りこぼす原因となる。
このようなS3-Lambdaトリガーの特性を理解せずにいると、いくつかの一般的な間違いを犯しがちだ。一つ目は、冪等性(べきとうせい)のチェックを怠ることである。S3はイベントを「少なくとも一回」配信するため、同じファイルに対してLambdaが複数回呼び出される可能性がある。もし冪等性のチェックがなければ、データベースに重複したデータが登録されたり、請求処理が二重に行われたりする問題が発生する。
二つ目は、DLQを設定しないことだ。DLQはオプションのように見えるが、スキーマ変更などによってLambdaの処理が常に失敗するようになると、2回のリトライ後にイベントが完全に消失する。CloudWatchのログには2回の失敗記録が残るものの、その後イベントがどうなったかは追跡できないため、問題が自然に解決したと誤解してしまうこともある。
三つ目は、再帰的呼び出しの罠である。LambdaがS3のオブジェクトを処理した後、その処理結果をトリガー元と同じS3のプレフィックスに書き戻してしまうと、その書き込み自体が再びLambdaをトリガーする。これが無限ループとなり、アカウントの同時実行数制限をあっという間に使い果たし、予想外の高額なAWS請求が発生することがある。
四つ目は、バッチ処理全体を一つのエラーハンドリングで囲むことだ。もしS3から送られてきたバッチに5つのレコードが含まれていて、そのうち1つが不正なデータだった場合、最初の不正なレコードで例外を発生させてしまうと、残りの4つの正常なレコードまで処理されずに失敗してしまう。これにより、正常なデータまで再処理されるか、最悪の場合消失してしまう。
五つ目は、予約済み同時実行数を設定しないことである。数千ものファイルが一気にS3にアップロードされると、Lambdaの同時実行数が急増する。このとき、同時実行数の制限がなければ、アカウント全体のスロットリングが発生したり、Lambdaが処理しきれないイベントがサイレントに破棄されたりする。
これらの問題を解決するためには、いくつかの「正しいアプローチ」がある。最も重要なのは、Lambdaハンドラーを各レコードを独立して処理するように構造化し、個別の失敗を収集することだ。最初のレコードでエラーが発生しても、他のレコードの処理を停止させないようにする。
冪等性を確保するためには、DynamoDBの条件付き書き込みなどを使って、同じオブジェクトバージョンがすでに処理されていないかをチェックする方法が有効だ。これにより、安価かつアトミックに重複処理を防ぐことができる。
DLQは必ず設定し、そのキューの深度を監視して、異常が発生したらすぐにアラートが上がるようにすべきだ。誰も見ていないDLQは、イベントがゆっくりと消失するだけの場所になってしまう。さらに良い方法は、S3イベントを直接Lambdaに送るのではなく、S3→SQS→Lambdaという経路で処理することである。これにより、S3の固定的な2回のリトライポリシーから解放され、SQSが持つ優れたリトライ機能、可視性、バックオフ制御、イベントを再処理する能力などを利用できるようになる。
ログは、バケット名、オブジェクトキー、リクエストIDなどを含む構造化されたJSON形式で出力すべきだ。これにより、トラブル発生時にCloudWatch Logs Insightsを使って、特定のオブジェクトがどのように処理され、リトライされたかを素早く追跡できるようになる。
さらに高度なパターンとして、複数のチームが同じS3イベントを必要とする場合、S3の直接通知ではなくEventBridgeを経由させると、複数のターゲットにイベントをファンアウトさせることができ、設定の競合を防げる。LambdaがSQSからメッセージを消費する場合、ReportBatchItemFailuresを有効にすることで、バッチ内の失敗したレコードだけを再送させ、成功したレコードの不必要な再処理を防ぐことができる。
メモリやタイムアウトの予算を超えるような大規模なファイルを処理する場合は、一つのLambda関数で無理に処理しようとせず、Step Functionsのワークフローをトリガーするのが賢明だ。これにより、ファイルの分割処理、リトライ、長時間実行されるステップのオーケストレーションが可能になる。また、S3バケットとLambdaが異なるAWSアカウントに存在する場合、直接トリガーするよりもSNSやEventBridgeを経由させることで、権限管理がより予測可能で安定したものになる。
そして何よりも、本番環境で問題が起きる前に、意図的にエラーパスをテストすることが重要だ。不正なJSONファイルやゼロバイトのファイルをアップロードしたり、重複したイベントを発生させたりして、DLQや冪等性ロジックが正しく機能するかを確認すべきだ。
パフォーマンスについても考慮すべき点がある。Lambdaのメモリ割り当てを増やすとCPUも比例して増加するため、圧縮解除やパース処理など、CPUを多く使うワークロードでは、メモリを増やすことで処理時間が短縮され、結果的に総コストが下がることがある。S3イベントは突発的なトラフィックが発生しやすいため、コールドスタートの影響が大きい。レイテンシーが重要なパイプラインでは、プロビジョンドコンカレンシーを利用することでコールドスタートを抑制できるが、アイドル時にもコストがかかるため、予想されるバーストに合わせて適切にサイズを設定することが重要だ。また、数ギガバイトにもなる巨大なファイルを一度にメモリに読み込むのは避けるべきである。レンジリクエストやストリーム処理を利用して、必要な部分だけを順次処理することで、メモリの無駄遣いやメモリ不足による強制終了を防ぐことができる。S3→SQS→Lambdaのパターンは、バースト的な呼び出しスパイクを平滑化し、Lambdaの同時実行数とS3へのアップロードレートを切り離す効果もあるため、高頻度で小ファイルがトリガーされるようなケースではコスト削減にもつながる。
これらの知識は特別なものではないが、これらを実践するかどうかが、「たいてい動く」システムと、リトライ、バースト、そして避けられない不正なファイルに対しても「確実に動く」システムとの違いを生み出す。これらの原則を最初から設計に組み込むことで、より堅牢で信頼性の高いシステムを構築できるだろう。