ENGINEER BLOG ENGINEER BLOG
  • 公開日
  • 最終更新日

【Lambda Powertools】S3 イベント通知の二重処理を防いでみた

この記事を共有する

目次

パーソル&サーバーワークスの印鑰です。

AWSを中心としたクラウドインフラの設計・構築・運用を担当しています。 エージェント開発にも注力しているエンジニアです。

はじめに

Amazon S3 イベント通知は「少なくとも1回配信(at-least-once delivery)」の設計です。通常は問題になりませんが、まれに同一イベントが重複して配信されることがあります。

  • 「同じファイルの処理が2回走ってしまった」
  • 「課金処理が二重に実行された」

こうした事故を防ぐため、Powertools for AWS Lambda(Python)の Idempotency ユーティリティ と Amazon DynamoDB を組み合わせた冪等性(idempotency)制御パターンを紹介します。Amazon S3 イベントに含まれる sequencerETagversionId を組み合わせてユニーク判定し、重複イベントを安全にスキップするアーキテクチャです。

Amazon S3 イベント通知の重複が発生する理由

Amazon S3 のイベント通知は、信頼性を優先して「少なくとも1回」配信されます。AWS 公式ドキュメントにも以下の記載があります。

Amazon S3 event notifications are designed to be delivered at least once.

まれなケースですが、Amazon S3 内部のリトライ機構により同一オブジェクト操作に対して複数回の通知が発生し得ます。

システムアーキテクチャ

本ブログでは以下の構成を採用します。

構成図

001.png

コンポーネント 役割
Amazon S3 オブジェクトのアップロード先。Amazon EventBridge への通知を有効化
Amazon EventBridge Amazon S3 イベントをルールでキャッチし、AWS Lambda を呼び出す
AWS Lambda(Powertools Idempotency) Powertools のデコレータで重複チェック。ユニークなら本処理を実行、重複ならスキップ
Amazon DynamoDB Idempotency テーブル。処理状態とユニークキーを一時保存。TTL で自動削除

前提条件

ハンズオンをはじめる前に、以下をお読みください。

所要時間

  • 約 30 分
    • 構築作業:約 10 分(AWS CloudFormation のデプロイ + Lambda コード設定)
    • 動作確認:約 15 分(テストイベント実行 + Amazon CloudWatch Logs 確認)
    • 環境削除:約 5 分

必要な権限

以下の権限が必要です。

  • AWS CloudFormation スタックをデプロイできること
  • Amazon S3 / AWS Lambda / Amazon DynamoDB / Amazon EventBridge を操作できること

リージョン

東京リージョン(ap-northeast-1) にリソースを作成してください。

費用について

このハンズオンの費用の目安は、東京リージョンで 月間10万イベントの場合、追加コストは 数円〜数十円 程度です。

サービス 料金体系 備考
Amazon DynamoDB(オンデマンド) 書き込み: $0.715 / 100万 WRU
読み取り: $0.1425 / 100万 RRU
TTL 削除は無料。Idempotency チェックのみの利用のため非常に低コスト
AWS Lambda $0.20 / 100万リクエスト + 実行時間 重複時は Amazon DynamoDB チェックのみで即座に完了するため実行時間は極小
Amazon EventBridge $1.00 / 100万イベント Amazon S3 からのイベントマッチング

※2026年6月時点の料金です。

設定手順

ステップ一覧

  1. AWS CloudFormation でベース環境を構築(Amazon S3 バケット、Amazon DynamoDB テーブル、IAM ロール、AWS Lambda 関数、Amazon EventBridge ルール)
  2. AWS Lambda 関数のコードを設定(Powertools Idempotency を使った重複チェック + 本処理)
  3. テストイベントで動作確認

ステップ1: AWS CloudFormation でベース環境を構築

以下の AWS CloudFormation テンプレートをデプロイし、ベース環境を構築します。

1-1. AWS CloudFormation テンプレートファイルの作成

ローカル PC で s3-event-dedup-stack.yaml という名前のファイルを作って、以下の内容をコピー&ペーストしてください。

ここを押すと展開します

AWSTemplateFormatVersion: '2010-09-09'
Description: S3 Event Deduplication with Powertools Idempotency
Parameters:
  ProjectName:
    Type: String
    Default: s3-event-dedup
    Description: Project name used as prefix for resource names
  TtlSeconds:
    Type: Number
    Default: 7200
    Description: TTL for idempotency records (seconds). Default 2 hours.
Resources:
  # ============================================================
  # S3 Bucket
  # ============================================================
  SourceBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub '${ProjectName}-source-${AWS::AccountId}'
      NotificationConfiguration:
        EventBridgeConfiguration:
          EventBridgeEnabled: true
      VersioningConfiguration:
        Status: Enabled
  # ============================================================
  # DynamoDB Table (Idempotency)
  # ============================================================
  IdempotencyTable:
    Type: AWS::DynamoDB::Table
    DeletionPolicy: Delete
    Properties:
      TableName: !Sub '${ProjectName}-idempotency'
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: id
          AttributeType: S
      KeySchema:
        - AttributeName: id
          KeyType: HASH
      TimeToLiveSpecification:
        AttributeName: expiration
        Enabled: true
  # ============================================================
  # IAM Role - Lambda
  # ============================================================
  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${ProjectName}-lambda-role'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: IdempotencyPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - dynamodb:PutItem
                  - dynamodb:GetItem
                  - dynamodb:UpdateItem
                  - dynamodb:DeleteItem
                Resource: !GetAtt IdempotencyTable.Arn
              - Effect: Allow
                Action:
                  - s3:GetObject
                Resource: !Sub '${SourceBucket.Arn}/*'
  # ============================================================
  # CloudWatch Logs - Lambda Log Group
  # ============================================================
  DedupFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    DeletionPolicy: Delete
    Properties:
      LogGroupName: !Sub '/aws/lambda/${ProjectName}-handler'
      RetentionInDays: 14
  # ============================================================
  # Lambda Function
  # ============================================================
  DedupFunction:
    Type: AWS::Lambda::Function
    DependsOn: DedupFunctionLogGroup
    Properties:
      FunctionName: !Sub '${ProjectName}-handler'
      Runtime: python3.13
      Handler: index.lambda_handler
      Role: !GetAtt LambdaRole.Arn
      Timeout: 60
      MemorySize: 256
      LoggingConfig:
        LogGroup: !Ref DedupFunctionLogGroup
      Layers:
        - !Sub 'arn:aws:lambda:${AWS::Region}:017000801446:layer:AWSLambdaPowertoolsPythonV3-python313-x86_64:7'
      Environment:
        Variables:
          IDEMPOTENCY_TABLE_NAME: !Ref IdempotencyTable
          TTL_SECONDS: !Ref TtlSeconds
          POWERTOOLS_SERVICE_NAME: !Sub '${ProjectName}'
      Code:
        ZipFile: |
          # プレースホルダー: ステップ2でコードを設定します
          def lambda_handler(event, context):
              return {'statusCode': 200}
  # ============================================================
  # EventBridge Rule
  # ============================================================
  S3EventRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub '${ProjectName}-s3-object-created'
      Description: Catch S3 ObjectCreated events and invoke Lambda
      State: ENABLED
      EventPattern:
        source:
          - aws.s3
        detail-type:
          - Object Created
        detail:
          bucket:
            name:
              - !Ref SourceBucket
      Targets:
        - Id: DedupFunctionTarget
          Arn: !GetAtt DedupFunction.Arn
  # ============================================================
  # Lambda Permission for EventBridge
  # ============================================================
  LambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref DedupFunction
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt S3EventRule.Arn
Outputs:
  SourceBucketName:
    Value: !Ref SourceBucket
    Description: Source S3 bucket name
    Export:
      Name: !Sub '${AWS::StackName}-SourceBucketName'
  IdempotencyTableName:
    Value: !Ref IdempotencyTable
    Description: DynamoDB idempotency table name
    Export:
      Name: !Sub '${AWS::StackName}-IdempotencyTableName'
  DedupFunctionName:
    Value: !Ref DedupFunction
    Description: Lambda function name
    Export:
      Name: !Sub '${AWS::StackName}-DedupFunctionName'

ファイルを保存してください。

テンプレートのポイント

リソース ポイント
Amazon S3 バケット Amazon EventBridge 通知を有効化 + バージョニング有効
Amazon DynamoDB テーブル パーティションキー id(String)+ TTL 属性 expiration。Powertools のデフォルト仕様に合わせた設計
AWS Lambda Layer AWS 公式の Powertools Layer ARN を指定。pip install 不要
IAM ロール Amazon DynamoDB への PutItem / GetItem / UpdateItem / DeleteItem + Amazon S3 の GetObject
Amazon EventBridge ルール Object Created イベントを対象バケットに絞ってキャッチ

1-2. AWS CloudFormation コンソールを開く

  1. マネジメントコンソールで「CloudFormation」を開く
  2. 左ペインから「スタック」にて「スタックの作成」→「新しいリソースを使用(標準)」を選択

1-3. テンプレートのアップロード

  1. 「既存のテンプレートを選択」を選択
  2. 「テンプレートファイルのアップロード」を選択
  3. 作成したAWS CloudFormationテンプレートファイルを選んでアップロード
  4. 「次へ」を選択

1-4. スタック詳細の指定

  1. スタック名に s3-event-dedup を入力し「次へ」を選択

パラメーターはデフォルトのままで OK です。変更する場合は以下を参考にしてください。

パラメーター デフォルト値 説明
ProjectName s3-event-dedup リソース名のプレフィックス
TtlSeconds 7200 Idempotency レコードの TTL(秒)。デフォルト2時間

1-5. スタックオプションの設定

  1. 「スタックオプションの設定」にて、「AWS CloudFormation によって IAM リソースがカスタム名で作成される場合があることを承認します。」にチェックを入れ、「次へ」を選択

1-6. 確認して作成

  1. 「確認して作成」にて設定内容をざっと確認し、「送信」を選択

大事なポイント: テンプレート内の Amazon S3 バケット名には ${AWS::AccountId} を使用しています。AWS CloudFormation が自動的にご自身の AWS アカウント ID(12桁の数字)を埋め込むため、手動での置き換えは不要です。

1-7. デプロイ完了の確認

スタックのステータスが「CREATE_COMPLETE」になるまで、待ちましょう。(約3~5分かかります)


ステップ2: AWS Lambda 関数のコードを設定

AWS CloudFormation でデプロイしたプレースホルダーコードを、Powertools Idempotency を使った実装に置き換えます。

2-1. AWS Lambda コンソールを開く

  1. マネジメントコンソールで「Lambda」を開く
  2. 左ペインから「関数」を選択
  3. 関数一覧から s3-event-dedup-handler を選択
  4. 「コード」タブを選択

2-2. コードを貼り付け

以下のコードを index.py に貼り付け、「Deploy」を押下します。

ここを押すと展開します

"""
Amazon S3 イベント重複制御 AWS Lambda
- Powertools for AWS Lambda の Idempotency ユーティリティを使用
- Amazon EventBridge 経由で Amazon S3 ObjectCreated イベントを受信
- sequencer + ETag + versionId の組み合わせで重複判定
- ユニークイベントのみ本処理を実行
"""
import json
import os
import boto3
from aws_lambda_powertools import Logger
from aws_lambda_powertools.utilities.idempotency import (
    DynamoDBPersistenceLayer,
    IdempotencyConfig,
    idempotent
)
# ロガー
logger = Logger()
# 環境変数
IDEMPOTENCY_TABLE_NAME = os.environ['IDEMPOTENCY_TABLE_NAME']
TTL_SECONDS = int(os.environ['TTL_SECONDS'])
# Powertools Idempotency の設定
# DynamoDB を永続化レイヤーとして使用
persistence_layer = DynamoDBPersistenceLayer(
    table_name=IDEMPOTENCY_TABLE_NAME
)
# Idempotency 設定
# event_key_jmespath: イベントからユニークキーを抽出する JMESPath 式
# → detail.object 内の key, etag, version-id, sequencer を "#" で結合
idempotency_config = IdempotencyConfig(
    expires_after_seconds=TTL_SECONDS,
    event_key_jmespath='detail.object.[key, etag, "version-id", sequencer] | join(`#`, @)'
)
# S3 クライアント
s3_client = boto3.client('s3')
@idempotent(config=idempotency_config, persistence_store=persistence_layer)
def lambda_handler(event, context):
    """
    Powertools Idempotency により、同一イベントに対して1回だけ実行される。
    動作:
    - 初回(ユニーク): 本処理を実行し、結果を DynamoDB に保存
    - 2回目以降(重複): 処理をスキップし、前回の結果を返却
    """
    # EventBridge 経由の S3 イベントから情報を抽出
    detail = event.get('detail', {})
    bucket_name = detail.get('bucket', {}).get('name', '')
    obj = detail.get('object', {})
    object_key = obj.get('key', '')
    etag = obj.get('etag', '')
    version_id = obj.get('version-id', 'null')
    sequencer = obj.get('sequencer', '')
    # 受信イベント全体をログ出力(生のイベント JSON を確認するため)
    logger.info("Received event", extra={"event": event})
    logger.info("Processing unique event", extra={
        "bucket": bucket_name,
        "key": object_key,
        "etag": etag,
        "version_id": version_id,
        "sequencer": sequencer
    })
    # ================================================================
    # ここから本処理(ビジネスロジック)
    # ================================================================
    # サンプル: オブジェクトのメタデータを取得
    try:
        response = s3_client.head_object(
            Bucket=bucket_name,
            Key=object_key
        )
        content_length = response.get('ContentLength', 0)
        content_type = response.get('ContentType', 'unknown')
        logger.info("Object metadata retrieved", extra={
            "size_bytes": content_length,
            "content_type": content_type
        })
    except Exception as e:
        logger.error("Failed to get object metadata", extra={"error": str(e)})
        raise
    # ================================================================
    # 本処理ここまで
    # 実際のビジネスロジックに置き換えてください
    # 例: オブジェクトの加工、別バケットへのコピー、DB 書き込みなど
    # ================================================================
    result = {
        'statusCode': 200,
        'body': {
            'result': 'success',
            'bucket': bucket_name,
            'key': object_key,
            'size': content_length,
            'contentType': content_type
        }
    }
    logger.info("Processing completed", extra={"result": result})
    return result

コードのポイント

処理 説明
@idempotent デコレータ 関数全体を冪等性制御で包む。Amazon DynamoDB への書き込み・チェック・結果キャッシュを自動で実行
event_key_jmespath detail.object.[key, etag, "version-id", sequencer] | join(`#`, @) で4つのフィールドを # 区切りで結合。これがユニークキーになる
expires_after_seconds TTL を2時間(7200秒)に設定。この時間が経過すると Amazon DynamoDB のレコードが自動削除され、同じイベントを再処理可能になる
DynamoDBPersistenceLayer Powertools が Amazon DynamoDB の条件付き書き込み(attribute_not_exists)を内部で使用。ミリ秒レベルの同時実行でも排他制御が保証される
INPROGRESS ステータス 処理開始時に INPROGRESS を書き込み、完了時に COMPLETED に更新。処理中に重複イベントが来ると IdempotencyAlreadyInProgressError が発生し排他制御される

JMESPath 式の解説

detail.object.[key, etag, "version-id", sequencer] | join(`#`, @)

この式は以下のように動作します:

  1. detail.object からイベントの object 部分を取得
  2. [key, etag, "version-id", sequencer] で4つのフィールドを配列として抽出
  3. join('#', @)# 区切りの文字列に結合

例: uploads/report.pdf#d41d8cd98f00b204e9800998ecf8427e#096fKKXTRTtl3on89fVO#0055AED6DCD90281E5

この文字列がハッシュ化され、Amazon DynamoDB の id 属性に保存されます。

補足: バージョニング無効のバケットで使う場合 Amazon S3 の versionId(Amazon EventBridge では version-id)は、バージョニングが有効なバケットでのみイベントに含まれます。バージョニングが無効の場合、このフィールドは null またはイベントに存在しません。 本ブログでは AWS CloudFormation テンプレートで VersioningConfiguration: Status: Enabled を設定しているため、version-id は常にイベントに含まれます。 バージョニング無効のバケットで使用する場合は、JMESPath 式から version-id を除外してください。

  • バージョニング無効の場合
event_key_jmespath='detail.object.[key, etag, sequencer] | join(`#`, @)'

ステップ3: 動作確認

実際に S3 にファイルをアップロードし、生のイベントを確認した上で重複チェックの動作を検証します。

3-1. S3 にテストファイルをアップロード

まず S3 にファイルをアップロードして、実際のイベントを発生させます。

テストファイルの作成

ローカル PC でメモ帳などのテキストエディタを開き、以下の内容で test-file.txt を作成してください。

hello world
マネジメントコンソールからアップロード
  1. マネジメントコンソールで「S3」を開く
  2. 左ペインから「バケット > 汎用バケット」を選択
  3. バケット一覧から s3-event-dedup-source-{AWSアカウントID} を選択
  4. 「フォルダの作成」を押下し、フォルダ名に uploads と入力して「フォルダの作成」を押下
  5. 作成した uploads/ フォルダを選択して開く
  6. 「アップロード」を押下
  7. 「ファイルを追加」を押下し、先ほど作成した test-file.txt を選択
  8. 「アップロード」を押下

ポイント: アップロードが完了すると、S3 → EventBridge → Lambda の一連のフローが自動的に発火します。

3-2. CloudWatch Logs で生のイベント JSON を確認

アップロードにより S3 → EventBridge → Lambda が発火します。Lambda コード内の logger.info("Received event", extra={"event": event}) により、受信した生のイベント JSON がログに出力されます。

  1. マネジメントコンソールで「CloudWatch」を開く
  2. 左ペインから「ログ > ログ管理」を選択
  3. ロググループ一覧から/aws/lambda/s3-event-dedup-handler を選択
  4. 最新のログストリームを開く

以下のような JSON ログが確認できます(実際の値は環境により異なります)。

{
  "level": "INFO",
  "location": "lambda_handler:53",
  "message": "Received event",
  "timestamp": "2026-06-18 11:57:11,170+0000",
  "service": "s3-event-dedup",
  "event": {
    "version": "0",
    "id": "2ddb9c43-95b4-0222-8ad0-06bf8b78c7c9",
    "detail-type": "Object Created",
    "source": "aws.s3",
    "account": "123456789012",
    "time": "2026-06-18T11:57:08Z",
    "region": "ap-northeast-1",
    "resources": [
      "arn:aws:s3:::s3-event-dedup-source-123456789012"
    ],
    "detail": {
      "version": "0",
      "event-version": "1.0",
      "bucket": {
        "name": "s3-event-dedup-source-123456789012"
      },
      "object": {
        "key": "uploads/test-file.txt",
        "size": 11,
        "etag": "5eb63bbbe01eeed093cb22bb8f5acdc3",
        "version-id": "xC2MDsJit.Tq1btOcARHs2aUvyaZHSve",
        "sequencer": "006A33DD1493FD0501"
      },
      "request-id": "7VYPGZ3FA9CGFN9C",
      "requester": "123456789012",
      "source-ip-address": "xxx.xxx.xxx.xxx",
      "reason": "PutObject"
    }
  },
  "xray_trace_id": "1-6a33dd15-4a42cd5e4242b20749932dfb"
}

ポイント: ログ全体の中で、テストイベントとして使うのは "event" キーの中身"version": "0"** から始まるオブジェクト)です。"level""message""timestamp" などの外側のフィールドは Powertools Logger が付与したメタ情報のため、テストイベントには含めません。

3-3. 重複チェックの確認(テストイベント実行)

手順 3-2 で確認したログの "event" の中身 をコピーし、テストイベントとして Lambda を再実行します。

  1. マネジメントコンソールで「Lambda」を開く
  2. 左ペインから「関数」を選択
  3. 関数一覧から s3-event-dedup-handler を選択
  4. 「テスト」タブを選択
  5. ステップ3-2 のログから "event" の中身(以下の JSON)を イベント JSON に貼り付ける
  6. JSON 形式 ボタンを押下
  7. 「テスト」を押下

補足: 「イベントの JSON 構文が無効です。JSON 構文を修正して、もう一度保存を試みてください。」と表示された場合は、コピー範囲を確認し JSON 構文を修正してください。

{
  "version": "0",
  "id": "2ddb9c43-95b4-0222-8ad0-06bf8b78c7c9",
  "detail-type": "Object Created",
  "source": "aws.s3",
  "account": "123456789012",
  "time": "2026-06-18T11:57:08Z",
  "region": "ap-northeast-1",
  "resources": [
    "arn:aws:s3:::s3-event-dedup-source-123456789012"
  ],
  "detail": {
    "version": "0",
    "event-version": "1.0",
    "bucket": {
      "name": "s3-event-dedup-source-123456789012"
    },
    "object": {
      "key": "uploads/test-file.txt",
      "size": 11,
      "etag": "5eb63bbbe01eeed093cb22bb8f5acdc3",
      "version-id": "xC2MDsJit.Tq1btOcARHs2aUvyaZHSve",
      "sequencer": "006A33DD1493FD0501"
    },
    "request-id": "7VYPGZ3FA9CGFN9C",
    "requester": "123456789012",
    "source-ip-address": "xxx.xxx.xxx.xxx",
    "reason": "PutObject"
  }
}

想定される実行結果:

{
  "body": {
    "bucket": "s3-event-dedup-source-123456789012",
    "contentType": "text/plain",
    "key": "uploads/test-file.txt",
    "result": "success",
    "size": 11
  },
  "statusCode": 200
}

ポイント: ステップ3-1 のアップロード時に既に処理が完了しているため、同じイベントを再実行すると 本処理は実行されず、前回の結果がそのまま返却されます。これが Powertools Idempotency による重複制御です。CloudWatch Logs を確認すると、今回は Processing unique event のログが出力されていないことがわかります。 なお、この確認は TTL(2時間)が切れる前 に行ってください。TTL 経過後は Amazon DynamoDB のレコードが削除され、再びユニークイベントとして本処理が実行されます。

3-4. DynamoDB テーブルの確認

  1. マネジメントコンソールで「DynamoDB」を開く
  2. 左ペインから「項目を探索」を選択
  3. テーブル s3-event-dedup-idempotency を選択

以下のようなレコードが確認できます。

属性
id s3-event-dedup-handler.index.lambda_handler#ハッシュ値
status COMPLETED
data 本処理の結果 JSON
expiration (現在時刻 + 7200 の Unix エポック秒)

補足: id の値は「Lambda 関数名 + モジュール名.関数名 + イベントキーのハッシュ」で構成されます。今回の場合、ファイル名 index.pylambda_handler 関数に @idempotent を付与しているため s3-event-dedup-handler.index.lambda_handler#... という形式になります。Powertools が自動生成するため、手動で確認する際はハッシュ部分を気にする必要はありません。


処理フロー図

002.png

「前回の結果を返却」とは: Powertools は初回(ユニーク)実行時に lambda_handler の戻り値を Amazon DynamoDB の data 属性に保存します。2回目(重複)では関数本体を実行せず、保存済みの戻り値をそのまま呼び出し元に返します。この仕組みを「レスポンスキャッシュ」と呼びます。

ミリ秒レベルの同時実行への耐性

本アーキテクチャがミリ秒レベルの重複イベントに耐えられる理由:

観点 説明
Amazon DynamoDB の整合性 Powertools 内部で PutItem + attribute_not_exists(id) を使用。アトミックな操作であり、同時に2つのリクエストが到着しても1つだけが成功する
INPROGRESS ステータス 1つ目の AWS Lambda が処理中の状態で2つ目の同一イベントが来ると、IdempotencyAlreadyInProgressError が発生し処理をブロック。二重実行を完全に防止
AWS Lambda 同時実行 AWS Lambda は同時に複数のイベントを処理可能だが、Amazon DynamoDB への条件付き書き込みが排他ロックとして機能
リトライ時 万が一 Amazon DynamoDB への書き込みがスロットリングで失敗した場合でも、Powertools が内部でリトライし冪等性を保証

重要: Amazon DynamoDB の条件付き書き込みは、複数の AWS Lambda が同時に同じキーで書き込みを試みても、必ず1つだけが成功します。Powertools はこの仕組みを活用し、さらに INPROGRESS / COMPLETED のステータス管理を加えることで、手動実装よりも堅牢な排他制御を提供しています。

環境削除手順

検証が完了したら、不要な課金を避けるためリソースを削除します。

1. S3 バケットを空にする

  1. マネジメントコンソールで「S3」を開く
  2. 左ペインから「バケット > 汎用バケット」を選択
  3. バケット一覧から s3-event-dedup-source-{AWSアカウントID} を選択
  4. 「空にする」を押下
  5. 確認のため 完全に削除 と入力し、「空にする」を押下

補足: バージョニングが有効なため、オブジェクトの個別削除ではバージョンや削除マーカーが残ります。「空にする」操作を使うことで、すべてのバージョンを含めて完全に削除されます。

2. CloudFormation スタックを削除

  1. マネジメントコンソールで「CloudFormation」を開く
  2. 左ペインから「スタック」を選択
  3. スタック一覧から s3-event-dedup を選択
  4. 「スタックを削除」を押下
  5. 確認ダイアログで s3-event-dedup を入力し「スタックを削除」を押下

スタックのステータスが DELETE_COMPLETE になれば削除完了です。S3 バケット、DynamoDB テーブル、Lambda 関数、EventBridge ルール、IAM ロール、ロググループがすべて削除されます。

まとめ

Amazon S3 イベント通知の重複配信に対して、Powertools for AWS Lambda の Idempotency ユーティリティを使った冪等性制御パターンを紹介しました。

ポイント 内容
ユニークキー objectKey + ETag + versionId + sequencer を JMESPath で結合
重複判定 Powertools が Amazon DynamoDB の条件付き書き込み(attribute_not_exists)を内部で実行。デコレータ1つで完結
排他制御 INPROGRESS ステータスにより、処理中のイベントに対する二重実行も防止
TTL 2時間で自動削除。重複はまれな事象のため短期間で十分
同時実行耐性 Amazon DynamoDB の強い整合性により、ミリ秒レベルの同時アクセスでも1回だけ処理を保証
テスト容易性 S3 にアップロードして生のイベントを確認し、同じイベントを再実行するだけで重複制御の動作を確認可能

「Amazon S3 イベントの重複処理を防ぎたいけど、どこから手をつけていいかわからない」という方の参考になれば幸いです。

参考


付録

付録A: Powertools for AWS Lambda の Idempotency とは

Powertools for AWS Lambda(Python) は AWS が提供する AWS Lambda 開発用ユーティリティライブラリです。その中の Idempotency ユーティリティ は、Amazon DynamoDB を使った冪等性制御を「デコレータ1つ」で実現します。

付録B: Amazon DynamoDB テーブル設計(Idempotency テーブル)

Powertools Idempotency が使用する Amazon DynamoDB テーブルの構造です。

テーブル構造

属性名 説明
id(パーティションキー) String Powertools が自動生成するユニークキー(関数名 + コードファイル名(拡張子除く) + ハンドラ名 + イベントキーのハッシュ)
status String 処理状態(INPROGRESS / COMPLETED / EXPIRED
data String 処理結果の JSON(COMPLETED 時に保存)
expiration Number TTL(Unix エポック秒)。設定した時間後に自動削除

ポイント

  • テーブルの作成時に指定するのは パーティションキー id(String) のみ
  • statusdataexpiration は Powertools が自動的に書き込む
  • TTL 属性名は expiration を指定(Powertools のデフォルト)
  • TTL を2時間に設定し、不要なレコードを自動削除(コスト最小化)
  • 重複はまれな事象のため、長期保存は不要

なぜ TTL は2時間で十分か

Amazon S3 イベント通知のドキュメントでは「通常は数秒以内、まれに1分以上かかる」と記載されています。重複イベントが数時間遅れで到着するケースは実質的にないため、2時間の TTL で十分にカバーできます。

付録C: 応用パターン

Amazon SQS を挟む構成(高スループット向け)

大量のイベントをバッファリングしつつ重複排除したい場合は、以下の構成も有効です。

003.png

Amazon SQS を挟むメリット:

  • AWS Lambda の同時実行数を制御可能(ReservedConcurrentExecutions
  • スパイク時のバッファリング
  • DLQ(デッドレターキュー)によるエラーハンドリング

Powertools Idempotency のロジックはまったく同じです。Amazon SQS 経由の場合は event_key_jmespath のパスを Amazon SQS メッセージボディに合わせて調整するだけです。

Powertools を使わない手動実装

Powertools に依存したくない場合は、Amazon DynamoDB の PutItem + ConditionExpression='attribute_not_exists(id)' を直接使って同等の重複チェックを実装できます。

ステップ2 の Lambda コードを、Powertools を使わずに書き換えた完全な例が以下です。EventBridge イベントから keyetagversion-idsequencer を取り出して # 区切りで結合し、それを dedup_key(重複判定用のユニークキー)として DynamoDB に条件付き書き込みします。

ここを押すと展開します

"""
Amazon S3 イベント重複制御 AWS Lambda(Powertools を使わない手動実装版)
- Amazon EventBridge 経由で Amazon S3 ObjectCreated イベントを受信
- key + etag + version-id + sequencer の組み合わせで重複判定
- ユニークイベントのみ本処理を実行
"""
import os
import time
import json
import boto3
from botocore.exceptions import ClientError
# 環境変数
DEDUP_TABLE_NAME = os.environ['IDEMPOTENCY_TABLE_NAME']
TTL_SECONDS = int(os.environ['TTL_SECONDS'])
# AWS クライアント
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(DEDUP_TABLE_NAME)
s3_client = boto3.client('s3')
def lambda_handler(event, context):
    # 受信イベント全体をログ出力(生のイベント JSON を確認するため)
    print(json.dumps(event, ensure_ascii=False, default=str))
    # EventBridge 経由の S3 イベントから情報を抽出
    detail = event.get('detail', {})
    bucket_name = detail.get('bucket', {}).get('name', '')
    obj = detail.get('object', {})
    object_key = obj.get('key', '')
    etag = obj.get('etag', '')
    version_id = obj.get('version-id', 'null')
    sequencer = obj.get('sequencer', '')
    # ユニークキーを組み立てる
    # key + etag + version-id + sequencer を "#" 区切りで結合
    dedup_key = '#'.join([object_key, etag, version_id, sequencer])
    print(f"dedup_key: {dedup_key}")
    # TTL(現在時刻 + TTL_SECONDS 秒後の Unix エポック秒)
    ttl_epoch = int(time.time()) + TTL_SECONDS
    # 重複チェック(条件付き書き込み)
    try:
        table.put_item(
            Item={'id': dedup_key, 'expiration': ttl_epoch},
            ConditionExpression='attribute_not_exists(id)'
        )
    except ClientError as e:
        if e.response['Error']['Code'] == 'ConditionalCheckFailedException':
            # 既にレコードが存在する = 重複イベント → 処理をスキップ
            print(f"Duplicate event. Skipped. dedup_key: {dedup_key}")
            return {'statusCode': 200, 'body': {'result': 'duplicate'}}
        raise
    # ================================================================
    # ここから本処理(ユニークイベントのみ実行される)
    # 実際のビジネスロジックに置き換えてください
    # ================================================================
    response = s3_client.head_object(Bucket=bucket_name, Key=object_key)
    print(f"Processed: s3://{bucket_name}/{object_key}, "
          f"size={response.get('ContentLength', 0)}")
    return {'statusCode': 200, 'body': {'result': 'success'}}

このコードを使う場合、環境変数 IDEMPOTENCY_TABLE_NAME にはブログで作成した Idempotency テーブル名(s3-event-dedup-idempotency)を、TTL_SECONDS には 7200 を設定してください。テーブルのパーティションキー id(String)と TTL 属性 expiration をそのまま使えます。

補足: 手動実装では INPROGRESS / COMPLETED のステータス管理やレスポンスキャッシュは含まれません。仕組みの学習には有用ですが、本番運用では Powertools の利用をお勧めします。

この記事は私が書きました

印鑰 幸太

記事一覧

全ての AWS 認定を取得。AWSサービスでは、AWS CloudFormationが好きです。ジム通いが趣味です。

印鑰 幸太

この記事を共有する

クラウドのご相談

CONTACT

クラウド導入や運用でお悩みの方は、お気軽にご相談ください。
専門家がサポートします。

サービス資料ダウンロード

DOWNLOAD

ビジネスをクラウドで加速させる準備はできていますか?
今すぐサービス資料をダウンロードして、詳細をご確認ください。