DBなしでEventBridgeによるStep Functionsの重複実行を排除する方法
目次
DynamoDB等を使わなくてもEventBridgeによるStep Functionsの重複実行を防ぎ、exactly-once(必ず1回だけの実行)を保証する方法を紹介します。
はじめに:EventBridgeがStep Functionsを重複して呼び出す問題
こんにちは、酒井です!
EventBridgeからStep Functionsを起動するアーキテクチャはよくありますよね。読者の皆さんはこのアーキテクチャを採用する際に、EventBridgeの重複実行にちゃんと気をつけていますか??
EventBridgeの実行保証は「少なくとも1回(at-least-once)」であり、稀にターゲットを重複して呼び出してしまうことがあります。
ターゲットは、重複して呼び出されても、システムの正常性を保つ必要があります。ちなみに、複数回実行しても結果を同じ状態に保つ性質を「冪等性」と言います。開発者はターゲットの冪等性を担保することが重要になります。
この記事では、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を意図的に重複実行するのは無理なので、疑似的な方法として、同じキーのS3オブジェクトを2回PutObjectし、Step Functionsの重複起動を止められるか確認します。もし、1回だけ起動していれば、検証成功です。
下記はLambdaのログです。1回目の実行は特に何のメッセージもありませんが、2回目の実行では重複を検知していることがわかります。
下記はStep Functionsの実行履歴で、1回目の呼び出しに対してのみStep Functionsを実行しており、2回目の実行はありませんね。
上記の方法で、重複したイベントに対して、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関数を変更すれば対応が可能ですので、ぜひ試してみてください。