AWS Step FunctionsのHello World テンプレートを読み解いて学ぶ

# AWS Step Functions
AWS Step Functionsというサービスが好きだったのですが最近触れておらず、設定や書き方を忘れてしまっていました。
久しぶりに触っていると、ステートマシンの作成時にHello Worldテンプレートが作成できるようになっており、気になって見ていると復習に最適な気がしたので今回コードを読み込んでみました。

![スクリーンショット 2024-12-28 23.29.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415169/cd535e37-e43d-7cad-d716-a928cf696bd5.png)


## テンプレート定義全文
```json
{
  "Comment": "A Hello World example that demonstrates various state types in the Amazon States Language, and showcases data flow and transformations using variables and JSONata expressions. This example consists solely of flow control states, so no additional resources are needed to run it.",
  "QueryLanguage": "JSONata",
  "StartAt": "Set Variables and State Output",
  "States": {
    "Set Variables and State Output": {
      "Type": "Pass",
      "Comment": "A Pass state passes its input to its output, without performing work. They can also generate static JSON output, or transform JSON input using JSONata expressions, and pass the transformed data to the next state. Pass states are useful when constructing and debugging state machines.",
      "Next": "Is Hello World Example?",
      "Output": {
        "IsHelloWorldExample": true,
        "ExecutionWaitTimeInSeconds": 3
      },
      "Assign": {
        "CheckpointCount": 0
      }
    },
    "Is Hello World Example?": {
      "Type": "Choice",
      "Comment": "A Choice state adds branching logic to a state machine. Choice rules use the Condition property to evaluate expressions with custom JSONata logic, allowing for flexible branching.",
      "Default": "Fail the Execution",
      "Choices": [
        {
          "Next": "Wait for X Seconds",
          "Condition": "{% $states.input.IsHelloWorldExample %}"
        }
      ]
    },
    "Fail the Execution": {
      "Type": "Fail",
      "Comment": "A Fail state stops the execution of the state machine and marks it as a failure, unless it is caught by a Catch block.",
      "Error": "Not a Hello World Example"
    },
    "Wait for X Seconds": {
      "Type": "Wait",
      "Comment": "A Wait state delays the state machine from continuing for a specified time.",
      "Seconds": "{% $states.input.ExecutionWaitTimeInSeconds %}",
      "Next": "Execute in Parallel",
      "Assign": {
        "CheckpointCount": "{% $CheckpointCount + 1 %}"
      }
    },
    "Execute in Parallel": {
      "Type": "Parallel",
      "Comment": "A Parallel state can be used to create parallel branches of execution in your state machine.",
      "Branches": [
        {
          "StartAt": "Format Execution Start Date",
          "States": {
            "Format Execution Start Date": {
              "Type": "Pass",
              "Output": {
                "FormattedExecutionStartDate": "{% $fromMillis($toMillis($states.context.State.EnteredTime), '[M01]/[D01]') %}"
              },
              "End": true
            }
          }
        },
        {
          "StartAt": "Snapshot Execution Elapsed Time",
          "States": {
            "Snapshot Execution Elapsed Time": {
              "Type": "Pass",
              "End": true,
              "Output": {
                "ElapsedTimeToSnapshot": "{% ($toMillis($now()) - $toMillis($states.context.Execution.StartTime)) / 1000 %}"
              }
            }
          }
        }
      ],
      "Next": "Set Checkpoint",
      "Catch": [
        {
          "ErrorEquals": [
            "States.QueryEvaluationError"
          ],
          "Next": "Set Checkpoint",
          "Output": {
            "ElapsedTimeToSnapshot": "Failed to calculate",
            "FormattedExecutionStartDate": "Failed to format"
          }
        }
      ]
    },
    "Set Checkpoint": {
      "Type": "Pass",
      "Next": "Summarize the Execution",
      "Assign": {
        "CheckpointCount": "{% $CheckpointCount + 1 %}"
      }
    },
    "Summarize the Execution": {
      "Type": "Succeed",
      "Comment": "A Succeed state indicates successful completion of the state machine.",
      "Output": {
        "Summary": "{% 'This Hello World execution began on ' & $states.input.FormattedExecutionStartDate & '. The state machine ran for ' & $states.input.ElapsedTimeToSnapshot & ' seconds before the snapshot was taken, passing through ' & $CheckpointCount & ' checkpoints, and has successfully completed.' %}"
      }
    }
  }
}
```

## テンプレート全体像
![stepfunctions_graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415169/79037d39-587f-b1a0-9d98-b48507597112.png)



## Topレベルの階層

### Comment
ステートマシンに対するコメント

```json
"Comment": "A Hello World example that demonstrates various state types in the Amazon States Language, and showcases data flow and transformations using variables and JSONata expressions. This example consists solely of flow control states, so no additional resources are needed to run it."
```
ステートマシン自体に対するコメントを設定します。

### QueryLanguage
ステートマシンで使用するクエリ言語の設定
```json
"QueryLanguage": "JSONata"
```
現在は2種類から選べるようになっており、このHello Worldテンプレートでは`JSONata`が採用されていることからも、AWS的にはJSONataを推奨しているようです。
- JSONPath・・・以前のデフォルト言語
- JSONata・・・新しく追加された言語

### StartAt
ステート開始場所

```json
"StartAt": "Set Variables and State Output"
```

ワークフローの開始点となる状態を示します。コンソールの設定ではプルダウンで開始点を選択できるようになっており、起点を変えると下記のようにStartの位置アイコンが変化します。

`Set Variables and State Output`の場合

![スクリーンショット 2024-12-28 21.47.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415169/cb1d5fdd-b5dd-5ed7-7769-99366648ae84.png)

`Wait for X Seconds`に変更した場合
![スクリーンショット 2024-12-28 21.47.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415169/452c27eb-013d-8cf9-3c43-c9a0b36990be.png)


## ステート
ここからが実際のステートになります

### Set Variables and State Output
```json
"Set Variables and State Output": {
  "Type": "Pass",
  "Comment": "A Pass state passes its input to its output, without performing work. They can also generate static JSON output, or transform JSON input using JSONata expressions, and pass the transformed data to the next state. Pass states are useful when constructing and debugging state machines.",
  "Next": "Is Hello World Example?",
  "Output": {
    "IsHelloWorldExample": true,
    "ExecutionWaitTimeInSeconds": 3
  },
  "Assign": {
    "CheckpointCount": 0
  }
}
```
#### Type Pass
```json
"Type": "Pass"
```
`Type`でステートの種類を設定します。`Pass`や後述する`Choice`等があります。
ステートの種類は`Pass`で、入力を出力に渡すシンプルなステートです。
#### Comment
```json
"Comment": "A Pass state passes its input to its output, without performing work. They can also generate static JSON output, or transform JSON input using JSONata expressions, and pass the transformed data to the next state. Pass states are useful when constructing and debugging state machines."
```
このステートに対する説明を記載することができます。
#### Next
```json
"Next": "Is Hello World Example?"
```
Nextに、このステートの次に遷移・実行させたいステートを設定します。
#### Output
```json
"Output": {
  "IsHelloWorldExample": true,
  "ExecutionWaitTimeInSeconds": 3
}
```
Outputを使って、
- `"IsHelloWorldExample": true`
- `"ExecutionWaitTimeInSeconds": 3`
というKeyValueを設定し、後続のステートで使用できるようにしています。


#### Assign
```json
"Assign": {
  "CheckpointCount": 0
}
```
Assignは、2024年11月のアップデートによって登場した概念「変数」です。

参考
https://dev.classmethod.jp/articles/step-functions-variables/

`CheckpointCount`というKeyに`0`というValueを初期値として設定しています。
今後遷移した先のステートで扱われることになるので注目ポイントです。


### Is Hello World Example?
*ここからは既出の設定・要素については省略します
```json
"Is Hello World Example?": {
  "Type": "Choice",
  "Comment": "A Choice state adds branching logic to a state machine. Choice rules use the Condition property to evaluate expressions with custom JSONata logic, allowing for flexible branching.",
  "Default": "Fail the Execution",
  "Choices": [
    {
      "Next": "Wait for X Seconds",
      "Condition": "{% $states.input.IsHelloWorldExample %}"
    }
  ]
}
```

#### Type Choice
```json
"Type": "Choice"
```
ステートの中で条件分岐ができるようになります。

#### Default
```json
"Default": "Fail the Execution"
```
後述する`Choices`のいずれの移行も実行されない場合の移行先の状態の名前を指定します。
`Fail the Execution`のステートに遷移するよう設定されています。
#### Choices
```json
  "Choices": [
    {
      "Next": "Wait for X Seconds",
      "Condition": "{% $states.input.IsHelloWorldExample %}"
    }
  ]
```
`Choices`に配列形式で次のステートの名称`"Wait for X Seconds"`とそのステートに遷移する条件`"Condition": "{% $states.input.IsHelloWorldExample %}"`を記載しています。
条件は`Condition`をKeyにし、Value部分には`"{% $states.input.IsHelloWorldExample %}"`でこのChoicesステートにinputとして渡された情報を参照している記述になっています。この

- `states.input.IsHelloWorldExample`が`true`だった場合、`Wait for X Seconds`ステートに遷移
- `states.input.IsHelloWorldExample`が`false`だった場合Defaultの`Fail the Execution`に遷移

する設定になっています。
### Fail the Execution
```json
"Fail the Execution": {
  "Type": "Fail",
  "Comment": "A Fail state stops the execution of the state machine and marks it as a failure, unless it is caught by a Catch block.",
  "Error": "Not a Hello World Example"
}
```

#### Type Fail
```json
"Type": "Fail"
```
ステートマシンの実行を停止するステートです
#### Error
```json
"Error": "Not a Hello World Example"
```
停止時のError内容をオプションとして定義できます

### Wait for X Seconds
```json
"Wait for X Seconds": {
  "Type": "Wait",
  "Comment": "A Wait state delays the state machine from continuing for a specified time.",
  "Seconds": "{% $states.input.ExecutionWaitTimeInSeconds %}",
  "Next": "Execute in Parallel",
  "Assign": {
    "CheckpointCount": "{% $CheckpointCount + 1 %}"
  }
}
```
#### Type Wait
```json
"Type": "Wait"
```
ステートマシンの続行を、指定された時間遅延させるステートです。

#### Seconds
```json
"Seconds": "{% $states.input.ExecutionWaitTimeInSeconds %}"
```
Waitで待機する時間 (秒)を指定します。
ここではJSONataの読み込み方式で`Set Variables and State Output`のステートでOutputに設定した`ExecutionWaitTimeInSeconds`を`Seconds`のValueとして読み込んでいます。

#### Assign
```json
  "Assign": {
    "CheckpointCount": "{% $CheckpointCount + 1 %}"
  }
```
`Set Variables and State Output`のステートで定義した変数`CheckpointCount`を参照し、`+1`インクリメントしています。このように、変数は途中のステートで参照・操作できるようになっています。

### Execute in Parallel
```json
"Execute in Parallel": {
  "Type": "Parallel",
  "Comment": "A Parallel state can be used to create parallel branches of execution in your state machine.",
  "Branches": [
    {
      "StartAt": "Format Execution Start Date",
      "States": {
        "Format Execution Start Date": {
          "Type": "Pass",
          "Output": {
            "FormattedExecutionStartDate": "{% $fromMillis($toMillis($states.context.State.EnteredTime), '[M01]/[D01]') %}"
          },
          "End": true
        }
      }
    },
    {
      "StartAt": "Snapshot Execution Elapsed Time",
      "States": {
        "Snapshot Execution Elapsed Time": {
          "Type": "Pass",
          "End": true,
          "Output": {
            "ElapsedTimeToSnapshot": "{% ($toMillis($now()) - $toMillis($states.context.Execution.StartTime)) / 1000 %}"
          }
        }
      }
    }
  ],
  "Next": "Set Checkpoint",
  "Catch": [
    {
      "ErrorEquals": [
        "States.QueryEvaluationError"
      ],
      "Next": "Set Checkpoint",
      "Output": {
        "ElapsedTimeToSnapshot": "Failed to calculate",
        "FormattedExecutionStartDate": "Failed to format"
      }
    }
  ]
}
```

#### Type Parallel
```json
"Type": "Parallel"
```
並列に複数ステートを実行させる場合に使用します。Parallel内で設定したすべてのステートの実行が終了するまで、Parallelの次のステートには遷移しません。

#### Branches
```json
"Branches": [
    {
      "StartAt": "Format Execution Start Date",
      "States": {
        "Format Execution Start Date": {
          "Type": "Pass",
          "Output": {
            "FormattedExecutionStartDate": "{% $fromMillis($toMillis($states.context.State.EnteredTime), '[M01]/[D01]') %}"
          },
          "End": true
        }
      }
    },
    {
      "StartAt": "Snapshot Execution Elapsed Time",
      "States": {
        "Snapshot Execution Elapsed Time": {
          "Type": "Pass",
          "End": true,
          "Output": {
            "ElapsedTimeToSnapshot": "{% ($toMillis($now()) - $toMillis($states.context.Execution.StartTime)) / 1000 %}"
          }
        }
      }
    }
  ]
```
typeが`Parallel`の場合の必須項目です。配列のオブジェクトに、並列に動かしたいステートを記述します。

`states.context.State.EnteredTime`・・・ステートマシンの実行が開始された時刻を参照します

それぞれのステートで下記の処理を行ってOutputの出力にしています。

- (states.context.State.EnteredTime)をミリ秒に変換し、"MM/DD"形式の文字列に変換して、この変数に格納
- 現在時刻から実行開始時刻を差し引き、ミリ秒から秒に変換して、ここまでの経過時間を計算し、変数に格納

#### End
`Next`で次のステートを明示的に指定せず、そのステートで終了させたい場合に、`"End": true`を指定します。

#### Catch
```json
  "Catch": [
    {
      "ErrorEquals": [
        "States.QueryEvaluationError"
      ],
      "Next": "Set Checkpoint",
      "Output": {
        "ElapsedTimeToSnapshot": "Failed to calculate",
        "FormattedExecutionStartDate": "Failed to format"
      }
    }
  ]
```

エラーが発生した場合→`ErrorEquals`が`States.QueryEvaluationError`だった場合に`Next`で定義したステート（ここでは`Set Checkpoint`）に遷移させる設定です。


### Set Checkpoint
```json
"Set Checkpoint": {
  "Type": "Pass",
  "Next": "Summarize the Execution",
  "Assign": {
    "CheckpointCount": "{% $CheckpointCount + 1 %}"
  }
}
```
#### Assign
変数`CheckpointCount`を1インクリメントしています。

### Summarize the Execution
```json
"Summarize the Execution": {
  "Type": "Succeed",
  "Comment": "A Succeed state indicates successful completion of the state machine.",
  "Output": {
    "Summary": "{% 'This Hello World execution began on ' & $states.input.FormattedExecutionStartDate & '. The state machine ran for ' & $states.input.ElapsedTimeToSnapshot & ' seconds before the snapshot was taken, passing through ' & $CheckpointCount & ' checkpoints, and has successfully completed.' %}"
  }
}
```
#### Type Succeed
`Succeed`という成功の状態の定義で、ここに到達した場合、ステートマシンが成功とみなされます。
（逆に`Fail`に到達した場合はステートマシンが失敗とみなされます。）

#### Output
ステートマシンの実行結果を出力します
JSOnataの埋め込み式を使っています
```json
"{% 'This Hello World execution began on ' & $states.input.FormattedExecutionStartDate & '. The state machine ran for ' & $states.input.ElapsedTimeToSnapshot & ' seconds before the snapshot was taken, passing through ' & $CheckpointCount & ' checkpoints, and has successfully completed.' %}"
```

## ステートマシンを実行してみる

![スクリーンショット 2024-12-28 23.32.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415169/c5fe5b6f-88d8-8630-7f31-0b6544c346b4.png)

入力には何も指定せず、`実行を開始`を押下します。

ステートマシンが起動し、

最終的なoutputの出力はこちらです。

```json
{
  "output": {
    "Summary": "This Hello World execution began on 12/28. The state machine ran for 3.094 seconds before the snapshot was taken, passing through 12 checkpoints, and has successfully completed."
  },
  "outputDetails": {
    "truncated": false
  }
}
```

- 実行された日付
- 実行時間
- チェックポイントを通過した数

がSummaryに出力されていることが分かります。

### 数値の検証
`Set Variables and State Output`で定義されている設定値を変えて実行してみます。
`"ExecutionWaitTimeInSeconds": 3`を`"ExecutionWaitTimeInSeconds": 10`に変えて実行します。

```diff_json
{
  "IsHelloWorldExample": true,
- "ExecutionWaitTimeInSeconds": 3
+ "ExecutionWaitTimeInSeconds": 10
}
```
Wait For X seconds実行部分で先程よりも長く待機時間があり、

![スクリーンショット 2024-12-28 22.54.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415169/b59f6595-60fa-b6fc-4b4e-a880d61d5a1a.png)


`The state machine ran for 10.096 seconds`部分も変化していることが分かります。
```json
{
  "output": {
    "Summary": "This Hello World execution began on 12/28. The state machine ran for 10.096 seconds before the snapshot was taken, passing through 12 checkpoints, and has successfully completed."
  },
  "outputDetails": {
    "truncated": false
  }
}
```

### Failの検証
`Set Variables and State Output`で定義されている設定値を変えて実行してみます。
```diff_json
{
- "IsHelloWorldExample": true,
+ "IsHelloWorldExample": false,
  "ExecutionWaitTimeInSeconds": 3
}
```

に変更して実行してみます。すると、期待通りFailステートに遷移してステートマシンが失敗に終わりました。

![スクリーンショット 2024-12-28 23.12.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/415169/ed8cb454-3267-0fea-7a16-c136f3f95ec6.png)


以上、Step FunctionsのHello Worldテンプレートを読み込んでみました。

基本的なStateの使い方やOutput・エラーハンドリングなどを組み込んであるシンプルないいテンプレートで、学習に最適でした。

2024年秋にアップデートしたJSONata対応や変数の詳細については深く踏み込まなかったため、別でまた内容を整理したいと思います。