DBなしでEventBridgeによるStep Functionsの重複実行を排除する方法

2023.06.20
DBなしでEventBridgeによるStep Functionsの重複実行を排除する方法
この記事をシェアする

DynamoDB等を使わなくてもEventBridgeによるStep Functionsの重複実行を防ぎ、exactly-once(必ず1回だけの実行)を保証する方法を紹介します。

はじめに:EventBridgeがStep Functionsを重複して呼び出す問題

こんにちは、酒井です!

EventBridgeからStep Functionsを起動するアーキテクチャはよくありますよね。読者の皆さんはこのアーキテクチャを採用する際に、EventBridgeの重複実行にちゃんと気をつけていますか??

EventBridgeの実行保証は「少なくとも1回(at-least-once)」であり、稀にターゲットを重複して呼び出してしまうことがあります。

EventBridgeがStep Functionsを重複して呼び出す様子を表すアーキテクチャ図

ターゲットは、重複して呼び出されても、システムの正常性を保つ必要があります。ちなみに、複数回実行しても結果を同じ状態に保つ性質を「冪等性」と言います。開発者はターゲットの冪等性を担保することが重要になります。

この記事では、EventBridgeの重複実行時にも、必ず1回だけStep Functionsを実行する方法を紹介します。

結論:Step FunctionsのNameパラメータを使えば重複排除できる

本記事で紹介する解決法は、「イベントのキー値とStep FunctionsのNameパラメータを1対1で対応させる」方法です。

イベントのキー値とは、決まったものを指しているわけではなく、要件によって異なります。具体的には、異なるイベント間でユニークであり、重複したイベント間で共通の値であれば何でもOKです。例えば、S3イベント通知の場合、S3オブジェクトのキー(s3://バケット名/[オブジェクトのキー])をイベントのキー値とみなすことができます。

Nameパラメータとは、Step Functionsの APIのパラメータです。Nameパラメータはユニークでなければならず、使用済みのNameパラメータを再度指定してしまうと、実行に失敗するという性質を持っています。

今回はこの方法に着目して、イベントのキー値とNameパラメータを1対1に対応させることで、重複したイベントが発生した際に、Step Functionsの実行を意図的に失敗させる仕組みを作ります。

大まかな方法

要件に合わせてイベントからキー値を抽出して、NameパラメータにマッピングさせてあげればOK!ただし、Nameパラメータを独自に設定するには、EventBridgeとStep Functionsの単純な統合では無理で、間にLambdaなどを挟む必要があります。(API Gatewayでも可能 ※後述)

重複排除の大まかな仕組みを表すアーキテクチャ図

詳しい方法

次の3つのAWSリソースを設定していきましょう。

  • Step Functions
  • Lambda
  • EventBridge

なお、EventBridgeのイベントソースはなんでもよいですが、この記事の例では「S3オブジェクトの作成」とします。別のイベントソースの場合、Lambdaの実装(Nameパラメータに指定するキーの値)が変わってきますので注意してください。

1. Step Functionsの作成

Step Functionsに条件はなく、中身はなんでもOKです。Lambdaから呼び出すので、Step FunctionsのArnを控えておきましょう。

2. Lambdaの実装

下記がLambdaのコードで、ポイントはStep Functionsの呼び出し時に指定するNameパラメータに、イベントのキーをそのまま設定することです。

import boto3
import json

stepfunctions = boto3.client('stepfunctions')

def lambda_handler(event, context):
    # 要件に合わせてイベントのキー値を抽出
    key = event["detail"]["object"]["key"]
    
    try:
        response = stepfunctions.start_execution(
            stateMachineArn = 'arn:aws:states:<Region>:<AccountId>:stateMachine:<StatemachineName>',
            name = key,  # Nameパラメータにキーを設定
            input = json.dumps(event)
        )

    # StartExecution APIは、Nameパラメータが重複した場合、ExecutionAlreadyExists例外を投げる
    except stepfunctions.exceptions.ExecutionAlreadyExists as e:
        print(f'キー{key}は既に処理しています')

なお、キー値とNameパラメータが1対1ならばよいので、キー値をハッシュ化した値をNameパラメータに設定してもよいです。この方が、キー値に人名などが入っている場合には好ましいかもしれませんね。

EventBridgeとStep Functionsの単純な統合ではなぜダメ?

EventBridgeとStep Functionsの単純な統合では、Nameパラメータは常にランダム値です。なので、上のLambdaのように、キー値とNameパラメータを1:1に対応させられませんので、イベントの重複発生時には、Step Functionsも重複して走ってしまいます。

3. EventBridgeの設定

Event Bridgeを設定します。記事の本質とずれるので詳しくは説明しませんが、Event Bridgeルールを作成し、イベントパターンにベントソースに合わせたトリガの条件を設定します。

{
  "detail": {
    "bucket": {
      "name": ["deduplicate-s3-01"]
    }
  },
  "detail-type": ["Object Created"],
  "source": ["aws.s3"]
}

※記事の話題とズレますが、EventBridgeでS3イベントを検知するにはさらに別の手順も必要です。実際に手元で試したい方は下記リンクの手順も実施してください。

ルールのターゲットには、上記のLambdaを設定してください。

EventBridgeルールのターゲット設定。Lambdaを設定している。

検証と結果

EventBridgeを意図的に重複実行するのは無理なので、疑似的な方法として、同じキーのS3オブジェクトを2回PutObjectし、Step Functionsの重複起動を止められるか確認します。もし、1回だけ起動していれば、検証成功です。

下記はLambdaのログです。1回目の実行は特に何のメッセージもありませんが、2回目の実行では重複を検知していることがわかります。

1回目のLambdaのログ。特にメッセージは無い。
Lambdaのログ(1回目の実行)
2回目のLambdaのログ。重複を検知しているメッセージを出力している。
Lambdaのログ(2回目の実行)

下記はStep Functionsの実行履歴で、1回目の呼び出しに対してのみStep Functionsを実行しており、2回目の実行はありませんね。

Step Functionsの実行ログ。計2回実行したが、1回目のみ呼び出され、2回目は呼び出されていない。
Step Functionsの実行ログ

上記の方法で、重複したイベントに対して、Step Functionsを1回だけ起動することができました。以上で検証は成功です。

この方法の利点

この方法の良い点は、AWSに起動履歴の記録を任せられ、ユーザは気にしなくてよい点です。具体的には、AWSがどのNameに対してStep Functionsを起動済みかを覚えていてくれるので、ユーザがDBで起動履歴を管理するようなことをしなくてよくなります。

別の方法(API Gatewayを使用)

Lambdaの代わりに、API Gatewayでも同じことができます。具体的には、LambdaをAPI Gatewayに置き換え、API Gatewayのマッピングテンプレートを使う方法です。

マッピングテンプレートを使う方法では、Lambdaと同じように、Nameパラメータにキー値をマッピングすればOKです。(動作確認済みです。詳しい手順は省略します)

注意:意図的にリトライするにはキーを変える必要がある

上記の方法は、「NameパラメータはAWSアカウント、リージョン、ステートマシンの単位で、90日間は必ず固有であること」という制約を利用しています。

ですから、意図的にリトライするには、Nameパラメータに前の試行とは別の値を設定する必要があります。そうしないと、重複排除が働いてしまい、失敗しますのでご注意ください。

さいごに

以上が、EventBridgeの重複実行時にも、正確に一度だけStep Functionsを起動する方法となります。

残念な点は、Lambda関数を独自に実装しなくてはならない点ですね。EventBridgeでNameパラメータを好きに設定できればいいんですが、現在のAWSではそうはなっていません。今後のAWSのアップデートに期待しましょう。

また、NameパラメータでStep Functionsの重複起動を防ぐ方法は、上流がEventBridgeの場合以外にも応用できる方法です。上流に合わせて、Lambda関数を変更すれば対応が可能ですので、ぜひ試してみてください。

この記事をシェアする
著者:酒井亮太郎
シナモロール