AWS Step Functionsでネストされたwhileループを実装する方法

2018.08.23

はじめに

AWS Step Functionsが2016年12月に導入されてから約1年半経過しました。このサービスはサーバーレスアプリケーションの可能性を向上させたと言えます。以前、複数のAWS Lambda関数がお互いに関連する複雑なビジネスロジックを私達開発者が構築しようとした場合、自分達でそれを実装する必要がありました。しかし、AWS Step Functionsを利用することで、簡単なJSONドキュメント(Amazon States language)を使ってそのロジックを実装することができるようになりました。このサービスはサーバーレスの世界のおいて複雑なロジックをできるだけシンプルに構築するフレームワークを提供していると言えます。とても便利なサービスなので、私は日々自分達のプロジェクトで使っています。

このポストでは、このサービスについてのtips、特にある複数の真偽条件を元にループする方法をご紹介したいと思います。つまり、AWS Step Functionsにおけるネストされたwhileループの実装方法をご紹介します。

なんでネストされたループの話をしたいの?

なぜなら、便利にも関わらずインターネット上にあまり情報が存在しないからです。AWS公式ドキュメントではAWS Step Functionsでループを実装する方法を説明していますが、これは平坦なループについてであり、ネストされたループについてではありません。

とにかく、以下の画像を見てみましょう。

これは私達のプロジェクトで動いているAWS Step Functionsのステートマシーンです。ある条件が真と評価されている間、アラートを通知させ続けるようにしています。

このステートマシーンには2つの概念があります。

  • 通知回数: 何回ステートマシーンが繰り返すべきか
  • 通知ユーザ数: どのくらいユーザへ通知すべきか

通知回数は IsNotificationCountReached ステートで評価されます。もし評価の結果が真であれば、ループは継続し、そうでないならループが終了します。通知ユーザ数は IsNotificationUserCountReached ステートで評価されます。もし評価の結果が真であればループは NotifyAlerts taskに遷移し、そうでないなら IsNotificationCountReached choiceへ遷移します。

要するに、AWS Step Functionsに最大通知回数までユーザへ通知させているということです。

どのようにAWS Step Functionsでループを実装しているのか

ステートマシーンをより詳細に見てみましょう。

FetchNotificationCount task

この関数はAWS DynamoDBからループの初期値を取得し、それを後続のステートに渡します。例えば、もしその値がある変数(例: item )に存在する場合、このように値を返すことになります。

return {
'notification_count': item['notification_count'],
'notification_user_accounts': item['user_accounts'],
'notification_user_count': len(item['user_accounts']),
'is_first_alert': True
}

そうすると、後続のステートはこのようにデータを受け取れます。

{
"input": {
"notification_count": 3,
"notification_user_accounts": {
"1": "foo@example.com",
"2": "bar@example.com",
"3": "baz@example.com"
},
"notification_user_count": 3,
"is_first_alert": true
}
}

AWS Lambdaであれば event 変数を使ってこの値を参照可能ですし、 choice ステートであれば $ pathを使って参照できます。

IsNotificationCountReached choice

ループがどのくらい繰り返すかを評価する条件です。この回数は notification_count 変数に保存されているので、以下のようにロジックを実装してます。

"IsNotificationCountReached": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.notification_count",
"NumericGreaterThan": 0,
"Next": "IsFirstAlert"
}
],
"Default": "Done"
},

この場合、 notification_count が0よりも大きいとステートマシーンが次のステートに遷移します。そうでない場合はループを止めます。

IsFirstAlert choiceと Sleep wait

私達は初回のループを除き5分間ステートマシーンをスリープさせています。「ノイズアラート」を抑制させるためです。ロジックはこのように実装しています。

"IsFirstAlert": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.is_first_alert",
"BooleanEquals": true,
"Next": "DecrementNotificationUserCount"
}
],
"Default": "Sleep"
},
"Sleep": {
"Type": "Wait",
"Seconds": 300,
"Next": "DecrementNotificationUserCount"
},

このステートマシーンでは、もし is_first_alert 変数が偽の場合、スリープするということを表しています。もしそうでない場合、次のステートへすぐに移動します。

DecrementNotificationUserCount task

このtaskの主な機能は前段のステートから渡された notification_user_count 変数から1を引き、 is_first_alert の値を偽へ変えることです。コードはこのようになります。

return {
'notification_count': event['notification_count'],
'notification_user_accounts': event['notification_user_accounts'],
'notification_user_count': len(event['notification_user_accounts']) - 1,
'is_first_alert': False
}

後段のステートは以下のデータを受け取ることが可能です。

{
"input": {
"notification_count": 3,
"notification_user_accounts": {
"1": "foo@example.com",
"2": "bar@example.com",
"3": "baz@example.com"
},
"notification_user_count": 2,
"is_first_alert": false
}
}

実際のところ、この機能をこのtaskで実装する必要性は必ずしもありません(例えば、 NotifyAlerts taskが同じ役割を取ることは可能です)。しかし、複雑な関数よりもシンプルな関数の方が好ましいです。そのため、現在のところ、ステートマシーンをあのように設計しています。

NotifyAlerts taskと IsNotificationUserCountReached choice

NotifyAlerts taskはアラートを通知し、 IsNotificationUserCountReached choiceはそのtaskがどのくらいの回数ユーザへ通知するかを制御しています。

主な機能を見てみましょう。このタスクにSlackへの通知を行わせていますが、この件はおいておきます。このタスクは通知する度に notification_user_count 変数から1を引きます。もしその値が-1になった場合、すべての通知がユーザへ送られたと判断し、 notification_count 変数から1を引きます。主要なコードはこのようになるでしょう。

notification_user_count = event['notification_user_count']
notification_count = event['notification_count']

if notification_user_count == -1:
notification_count -= 1

output = event
output['notification_count'] = notification_count
output['notification_user_count'] = notification_user_count - 1

return output

続いて、 IsNotificationUserCountReached choiceについて。 notification_user_count 変数はステートマシーンがどのくらいのユーザに通知すべきかという情報を持っているので、この変数を利用しこのようにロジックを実装しています。

"IsNotificationUserCountReached": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.notification_user_count",
"NumericGreaterThanEquals": 0,
"Next": "NotifyOverflow"
}
],
"Default": "IsNotificationCountReached"
},

ステートマシーンの全容

これが上述したステートマシーンです。AWS LambdaのARNはマスクしています。

{
"Comment": "the state machine notifying alerts",
"StartAt": "FetchNotificationCount",
"States": {
"FetchNotificationCount": {
"Type": "Task",
"Resource": "",
"Next": "IsNotificationCountReached"
},
"IsNotificationCountReached": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.notification_count",
"NumericGreaterThan": 0,
"Next": "IsFirstAlert"
}
],
"Default": "Done"
},
"IsFirstAlert": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.is_first_alert",
"BooleanEquals": true,
"Next": "DecrementNotificationUserCount"
}
],
"Default": "Sleep"
},
"Sleep": {
"Type": "Wait",
"Seconds": 300,
"Next": "DecrementNotificationUserCount"
},
"DecrementNotificationUserCount": {
"Type": "Task",
"Resource": "",
"Next": "IsNotificationUserCountReached"
},
"IsNotificationUserCountReached": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.notification_user_count",
"NumericGreaterThanEquals": 0,
"Next": "NotifyOverflow"
}
],
"Default": "IsNotificationCountReached"
},
"NotifyAlerts": {
"Type": "Task",
"Resource": "",
"Next": "IsNotificationUserCountReached"
},
"Done": {
"Type": "Pass",
"End": true
}
}
}

まとめ

このポストではAWS Step Functionsにおけるネストされたwhileループの実装方法について議論しました。私は多くの状況で適用可能だと考えています。このポストが役立てば幸いです。