AWS Step Functions を使って時間ぴったりに処理を行うサーバーレススケジューラーを構築する
堀家 隆宏 (AWS Serverless HERO)
AWS で予約実行や定期実行などのスケジュール処理を実装する場合に、Amazon EventBridge を使用してスケジュールベースのルールを組むことはプラクティスの 1 つだと思います。しかしながら、Amazon EventBridge を使ったスケジュール処理には以下のような課題があります。
- 予約時刻から数分程度ズレで実行される場合がある
- AWS Lambda を実行する場合に非同期呼び出しとなるため、複数起動される場合がある
これらが許容される場合は特に問題がありませんが、許容できない場合には AWS Step Functions を使うことで正確な時刻での配信が出来るスケジューラーを構築することが出来ます。
なお、以下の GitHub でソースコードは公開していますので興味がある方は是非触ってみてください。
ご注意
本記事で紹介する AWS サービスを起動する際には、料金がかかります。builders.flash メールメンバー特典の、クラウドレシピ向けクレジットコードプレゼントの入手をお勧めします。
このクラウドレシピ (ハンズオン記事) を無料でお試しいただけます »
毎月提供されるクラウドレシピのアップデート情報とともに、クレジットコードを受け取ることができます。
1. アーキテクチャ
まずは構成図を見ていきましょう。構成は AWS Cloud Development Kit (CDK) v2.10.0 で管理して、AWS Lambda 自体は TypeScript でトランスパイルしたものを動かしています。
この構成は大きく3つのパーツから構成されています。
1. 実行時刻を登録するための API
左下にある Amazon API Gateway がそれにあたります。実行時刻と最終的にスケジュール実行される API 等へのパラメータを指定します (実システムで使う場合は必要に応じて認証認可もご検討ください)。Amazon API Gateway + AWS Lambda + Amazon DynamoDB というサーバーレスで API を構築するためのもっともポピュラーな構成になっています。
2. 予約スタンバイ
予約時刻が近づいてきたら DB 内のステータスを切り替えてスケジュール実行のための AWS Step Functions を実行します。 Amazon EventBridge のスケジュールルールで定期的に Amazon DynamoDB のデータを監視します。予約時間が近いデータが見つかれば、ステータスを更新することで DynamoDB Streams にデータを流して、実際の処理を行う AWS Step Functions をスタンバイ状態とします。
3. スケジュール処理の実行
実行時刻まで AWS Step Functions の Wait State で処理を待機します。時刻が来たら実行対象となる AWS Lambda をトリガーします。
2. スケジュール実行までの流れ
2-1. API によるスケジュール登録
実行時刻を登録するための API に以下のような POST リクエストを投げることで実行時刻と最終的に AWS Lambda 内で実行される API へのパラメータを指定します。
{
"publishTime": 1645670700, // 実行時刻(GMTのタイムスタンプ)
"channel": "invoker-alpha", // 実行する対象のLambdaファンクション名
"parameters": { // 対象となるLambdaの中で実行するAPIへのパラメータ
"message": "Invocation test from invoker-alpha"
}
}
2-2. 予約実行のスタンバイ
予約スタンバイが 1 時間に 1 回 AWS Lambda を起動して、Amazon DynamoDB のステータスが RESERVED かつ 1 時間以内に実行予定のスケジュールを検索し、ステータスを STANDBY に更新します。これにより DynamoDB Streams から AWS Step Functions を起動して、AWS Step Functions の Wait State により予約時刻までスタンバイ状態で停止します。
2-3. 実行
指定時刻になれば、AWS Step Functions が指定された AWS Lambda を実行します。成功したら Amazon DynamoDB のステータスを DONE に変更します。
3. Amazon DynamoDB のテーブル設計
テーブル設計も見ていきましょう。大きなデータの種別としては以下の 2 種類になっています。それぞれデータの PK の最初の文字列でデータの種別を表現しています。
- INVOKER#<invoker name> : スケジュール実行される AWS Lambda の情報を格納
- MESSAGE#<message ID> : 配信されるメッセージ。予約時刻やステータス、スケジュール実行時のパラメータ情報を格納
PR | ARN | GSI1PK | GSI1SK | Parameter | InvokerID | PublishDate |
INVOKER#invoker-alpha | arn:aws:lambda:ap-northeast-1:xxxxxxxxx:function:xxxxxx | |||||
INVOKER#invoker-beta | arn:aws:lambda:ap-northeast-1:xxxxxxxxx:function:yyyyyy | |||||
MESSAGE#a7ddf672-409d-4f1e-927d-2221aa0f5ba0 | MESSAGE#STANDBY | MESSAGE#1646815555 | {"message":"Invocation test from invoker-alpha"} | invoker-alpha | 1646815555 | |
MESSAGE#5f8d99e5-afa5-4268-976d-3a028396872e | MESSAGE#RESERVED | MESSAGE#1647127900 | {"message":"Invocation test from invoker-alpha"} | invoker-alpha | 1647127900 | |
MESSAGE#015eadee-1a2f-48e1-8f1e-debcd67ec08b | MESSAGE#DONE | MESSAGE#1646782800 | {"message":"Invocation test from invoker-alpha"} | invoker-alpha | 1646782800 |
更に予約スタンバイが RESERVED のメッセージを検索できるように Global Secondary Index を作っており、以下のような Query で RESERVED ステータスのデータを取得してステータスをアップデートしています。
const queryCmd = new QueryCommand({
TableName: this.reservationTableName,
IndexName: ‘GSI1',
KeyConditionExpression: '#gsi1pk = :gsi1pk AND #gsi1sk < :gsi1sk’,
ExpressionAttributeNames: {
'#gsi1pk': ‘GSI1PK',
'#gsi1sk': ‘GSI1SK',
},
ExpressionAttributeValues: {
':gsi1pk': { S: ‘MESSAGE#RESERVED’ },
':gsi1sk': { S: `MESSAGE#${searchTimestamp}`},
},
});
4. 動作確認
こちらのように指定時刻になったら Slack にメッセージを送信できる仕組みを構築して動作確認を行ってみましょう。
Curl で配信メッセージを登録します。2022/03/09 18:40 に Invocation test from invoker-alpha というメッセージが配信されるように設定しています。
curl --location --request POST '<https://xxxxxxx.execute-api.ap-northeast-1.amazonaws.com/messages>' \\
--header 'Content-Type: application/json' \\
--data-raw '{
"publishTime": 1646818800,
"channel": "invoker-alpha",
"parameters": {
"message": "Invocation test from invoker-alpha"
}
}'
こちらの通り、配信時刻前に AWS Step Functions がスタンバイ状態となり待機しています。
DB 内のステータスもこちらのように STANBY ステータスになっています。
スケジュール時刻である 2022/03/09 18:40 を過ぎると、こちらのように正しく配信されていることが確認されました。
AWS Lambda も正確な時刻で実行されていることが分かります。
5. まとめ
以上のように AWS Step Functions を使うことで時間丁度に実行が行えるスケジューラーを構築することが出来ました。この構成は Amazon EventBridge において配信時刻がずれる問題や冪等性への配慮を解決します。また、今回使用した AWS Step Functions のスタンダートタイプは東京リージョンで、状態遷移 1,000 回ごとの料金が 0.025 USD (無料枠 4,000 回の状態遷移/月, 2022/03/11 現在) であり、大量の予約配信を実施するような場合は Amazon EventBridge と比較するとコスト面での注意は必要です。
それを差し引いてもメリットがあるケースでは有効なアーキテクチャではないでしょうか。特に一度構築してしまえば、AWS Lambda 内のライブラリ等のアップデート以外はメンテナンスの必要はありませんし、ほとんど手離れで運用できるシステムとも言えるでしょう。これはサーバーレスアーキテクチャを採用する上で大きなメリットの 1 つと言えます。
筆者プロフィール
堀家 隆宏
株式会社Serverless Operations
2014 年に AWS Lambda がリリースされてから、手軽にコードがデプロイでき、素早く動くものが出来るサーバーレスのコンセプトに惹かれて、サーバーレスのファンに。それからは JAWS-UG やサーバーレスコミュニティなどの活動を経て、2021 年に AWS の Serverless Hero に認定されました。
仕事においても Serverless Operations という会社を 2018 年に設立して、エンタープライズ企業からスタートアップまで、様々な業種の企業様にサーバーレスを中心とした内製化・開発支援のサービスに従事しています。
AWS を無料でお試しいただけます