はじめに
初めまして、株式会社クラベスでエンジニアをしているDandeと申します!
今回、初めてテックブログを書くことになり、何を書けば良いか悩んでいましたが、最近実案件でStep Functionsの新機能を試したので、その感想をまとめることにしました。
拙い文章かもしれませんが、どなたかの参考になれば幸いです。
また、説明に誤りなどあればご指摘いただければ幸いです。
新機能を使用する前の経緯
あるプロジェクトで、Step FunctionsをTerraformで構築する依頼を受けました。
具体的な内容はお伝えできませんが、ステートマシン内で複数のECSタスクを同期的に実行する仕様で、それぞれが別々のバッチ処理を実行するというものです。
私自身、Step Functionsを触るのも、Terraformを本格的に使うのも初めての状況でした。
ドキュメントと格闘しながら実装を進めていましたが、中でも一番大変だったのがステート間の値の受け渡しでした。
例えば、以下のように4つのステートがあるとします。
ステート1 ← 入力が渡される
↓
ステート2
↓
ステート3
↓
ステート4 ← ステート1に入力として渡された値を使用
この場合、ステート1で渡された入力値をステート4でも使用するには、ResultPath
を利用してデータを次のステートに渡していく必要があります。
しかし、入力値を使用しないステート(ステート2やステート3)にも値を渡さなければならないため、この処理が煩雑になります。
バケツリレーの課題
簡単な例で見てみましょう。
definition = jsonencode({
StartAt = "State1",
State1 = {
Type = "Task"
・
・
ResultPath = null
Next = "State2"
},
State2 = {
Type = "Task"
・
・
ResultPath = null
Next = "State3"
},
State3 = {
Type = "Task"
・
・
ResultPath = null
Next = "State4"
},
State4 = {
Type = "Task"
・
・
Parameters = {
LaunchType = "FARGATE"
Cluster = "XXXXXXXX"
・
・
Overrides = {
ContainerOverrides = [
{
"Name" : "app",
"Environment" : [
{
"Name" : "EVENT_TIMESTAMP",
"Value.$" : "$.event_time"
}
]
}
]
}
}
End = true
}
})
上記では、ResultPath = null
を設定することで、入力データをそのまま次のステートに渡しています。
これを全てのステートで繰り返すことで、最終的にState4までデータを渡せます。
しかし、次のようなケースになると複雑さが増します。例えば、ステート2のタスクが失敗した場合にエラー通知用のステートを経由して、ステート3に戻るようにしたいとします。
State2 = {
Type = "Task"
・
・
ResultPath = null
Next = "State3"
Catch = [{
ErrorEquals = ["States.ALL"],
Next = "ErrorNotifyState"
ResultPath = null
}]
},
ErrorNotifyState = {
Type = "Task"
・
・
ResultPath = null
Next = "State3"
}
このように、エラーハンドリングを加えると、さらにResultPath = null
の記述が増えます。
記述ミスが発生しやすく、例えばCatch句で値の受け渡しを忘れた場合、State4に必要なデータが渡らずエラーになることもあります。
また、入力データだけでなくタスク結果の結合が必要な場合は、さらにコードが煩雑になります。
実装中は、
- 「どのステートにどのデータを渡しているのか?」
- 「受け取るデータの構造はどうなっているのか?」
といった疑問が頻繁に生じ、開発効率が下がっていました。
待望の新機能「Variables」の登場
2024年11月、私の願いが通じたのか、AWSから待望の新機能が発表されました!
AWS Step FunctionsがVariablesとJSONata変換で開発者体験をシンプルにする
この新機能には、「Variables」と「JSONata変換」の2つがあります。
今回は主に「Variables」について紹介します(JSONata変換についてはまた別の機会に記事にしたいと思います)。
Variablesでの実装例
Variablesを使うことで、バケツリレーを廃止できます。
以下に修正版のコードを示します。
definition = jsonencode({
StartAt = "State1",
State1 = {
QueryLanguage = "JSONata"
Type = "Task"
・
・
Assign = {
"eventTime" : "{% $states.input.event_time %}"
}
Next = "State2"
},
State2 = {
Type = "Task"
・
・
Next = "State3"
Catch = [{
ErrorEquals = ["States.ALL"],
Next = "ErrorNotifyState"
}]
},
State3 = {
Type = "Task"
・
・
Next = "State4"
},
State4 = {
Type = "Task"
・
・
Parameters = {
Overrides = {
ContainerOverrides = [
{
"Name" : "app",
"Environment" : [
{
"Name" : "EVENT_TIMESTAMP",
"Value.$" : "$eventTime"
}
]
}
]
}
}
End = true
}
})
ここで注目すべきは、以下のポイントです
-
Assignによる変数定義
State1でeventTime
という変数を定義しています。この変数には、入力データからevent_time
を取得して格納しています。Assign = { "eventTime" : "{% $states.input.event_time %}" }
-
後続ステートでの利用
変数は、必要なステートで$変数名
として簡単に使用できます。例えば、State4で$eventTime
をコンテナの環境変数として設定しています。
これにより、バケツリレーが不要となり、コードが大幅に簡潔になります。
必要な値だけを変数に格納すれば、後続のステートで自由に利用できるため、実装ミスも減らせます。
終わりに
Variables機能の追加は、Step Functionsの利便性を飛躍的に向上させました。
もし現行のシステムでバケツリレーによる値の受け渡しに苦労しているなら、ぜひこの機能を試してみてください。
この記事が、Step Functionsを利用する多くの方々の助けになれば嬉しいです。