Stepfunctionsの「Variables」機能を触ってみた

# はじめに

初めまして、株式会社クラベスでエンジニアをしている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変換で開発者体験をシンプルにする](https://aws.amazon.com/about-aws/whats-new/2024/11/aws-step-functions-variables-jsonata-transformations/)

この新機能には、「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を利用する多くの方々の助けになれば嬉しいです。

---

# 参考資料

- [AWS Step FunctionsがVariablesとJSONata変換で開発者体験をシンプルにする](https://aws.amazon.com/about-aws/whats-new/2024/11/aws-step-functions-variables-jsonata-transformations/)
- [[アップデート] AWS Step Functions で変数が使えるようになりました](https://dev.classmethod.jp/articles/step-functions-variables/)
- [[StepFunctions]ParametersやらResultPathやら…。ステート間のパラメータ受け渡しって結局どうなってるの？を１つの図にしてみた](https://dev.classmethod.jp/articles/stepfunctions-parameters-inter-states/)