[StepFunctions]ParametersやらResultPathやら…。ステート間のパラメータ受け渡しって結局どうなってるの?を1つの図にしてみた。

InputPath, OutputPath, ResultPath, Parametersの4つでパラメータがどう操作されるのか、一覧できる図を作ってみました。
2020.07.07

こんにちは、平野です。

StepFunctionsを使っていて、ステート間を流れていく変数を追っていたんですが、 ParametersとかResultPathって、何度理解したと思っても忘れてしまうんですよね。

出来るだけわかりやすい一覧の図にできればいいなぁ、と思ったので作ってみました。 その過程で理解もだいぶ深まったので自分的には決定版になったかなと思います。

取り上げる範囲

取り上げる機能

今回取り上げる機能は以下の4つのみです。 ので、かなり基本的なことしか解説していません。

  • InputPath
  • OutputPath
  • ResultPath
  • Parameters

説明に使用するステート

StepFunctionsのステートにはいくつかの種類がありますが、 今回はLambda関数を呼ぶTaskステートを使って説明します。

これは、Lambdaは使ったことがある人も多いだろうし、一番わかりやすいからです。 ちなみにStepFunctionsには何も動作をしないPassステートもあるんですが、 これはLambda関数で仕組みを理解すれば、それのちょっと特殊なケースという位置付けになるので、 とにかく最初はLambda関数で理解するのが良いと思います。

Lambda関数以外を呼ぶ場合でもステート間の変数の動きは変わらないので、 まずはステートの中身はLambda関数一択という前提で理解していきましょう。

完成図

早速ですが、まずは最終的な完成図を掲載します。

順を追って説明し、最後にもう一度この図を再掲します。

説明

デフォルト動作

今回説明する4つの機能は、全てが省略可能なので、まずは全て省略した場合です。

わざとらしく図がスカスカになっていますが、 デフォルトの動作として、 ステートへの入力がLambda関数にそのまま入り、Lambda関数の出力がそのままステートの出力になります。

ここで言っている「ステートへの入力」(および出力)は、連想配列で、こんな感じのものです。

{
  "aaa": {
    "AAA": "111"
  },
  "bbb": [
    "BBB",
    "BBBB"
  ]
}

なお、こんな構造になっているデータを何と呼ぶかは人によって色々かと思いますが、 この記事では(少し適切でないかもですが)"JSON"と呼ぶことにします。

さて、この図で注目して頂きたいのは、Lambdaを実行するステート(外枠)は 「ステートの中にLambda関数を持っている」ということです。 ステートとLambda関数は一体ではありません。 何が言いたいかというと、「ステートの入出力」と「Lambda関数の入出力」は全く別物として考える必要があるということです。 こう考える必要があるということはおいおいご理解頂けると思います。

デフォルト動作では、ステートに入力されたJSONがそのままLambda関数に入力され、 Lambda関数の出力がそのままステートの出力となります。

InputPath、OutputPath

次に、InputPathとOutputPathを図に入れます。

InputPath、OutputPathについては難しいことはありません。 これらについて知っておくべきことは InputPathとOutputPathはフィルターであるということです。 フィルターなので情報を減らすことしかできません。 もちろん省略時のデフォルト動作は、何もフィルターしない=全通しです。

ということで、Lambda関数を実行するステートの場合は、実際にはこれを使う機会はあまりないかと思います。 多は少を兼ねるので、Lambda関数に無駄に多く引数を与えても特に問題は起きないからです。 出力についても、特別情報を隠したいということでなければ、無駄な出力があっても困ることはないと思います。 よほど厳格に渡すパラメータを管理する場合でなければ、使うことはあまりなさそうです。

さて、図に書き入れるとこんな感じです。

ステートの中に配置され、ステートの入力に対してInputPathでフィルタリングした後の結果をLambda関数の入力とします。 同様に、Lambda関数からの出力に対してOutputPathでフィルタリングした結果をステートの出力とします。

ステートの入出力とLambda関数の入出力

さて、ここでステートとLambdaを分けて考えた意味が出てきます。 マネジメントコンソールでステートマシンを見た時、 各ステートの「入力」「出力」が確認できますが、 これはステートに対する入出力を表示しているのであり、Lambda関数の入出力ではありません。

Lambda関数の入力は、InputPathや後述するParameterによってステートの入力とは異なる場合がありますが、 残念ながらステートマシンのマネジメントコンソールからでは見ることはできません。 Lambda関数の入出力については各関数のログを見るしかないようです。

この辺りは、StepFunctionsを使い始めた頃には混乱する箇所かと思います。 ステートとLambda関数の包含関係をよく理解しましょう。

Parameters

ではいよいよParametersに入っていきます。Parametersは今回取り扱うものの中で一番重要です。

Parametersを言葉で説明すると、 「ステートの入力とは全く関係なく、Lambda関数の入力を定義するもの」です。 Lambda関数への入力に対して「全く新しいJSONを定義する」ことが本質 です。 (InputPathを通した)ステートへの入力は捨ててしまいますので、図にするとこんな感じです。

Parametersは(InputPath、OutputPath、ResultPathと違って、)JSONを設定します。 ステートへの入力がJSONだったので、それを代替するものがJSONを設定するのは当然と言えます。 図では、四角で囲ったものがJSONを表しています。

省略時はnullであり、InputPathを通った後のJSONがLambda関数に入力されます。

Parametersはステートへの入力を加工する、という勘違い

Parametersは「全く新しいJSONを定義する」と書きましたが、 ご存知の通り、 実際にはParametersにはステートへの入力(のInputPathを通った後のもの)を使うことができます。 例えば、ステートの入力をStateInputというKeyに紐付け、 別途SomeParameterというKeyを追加したければ

"Parameters": {
    "StateInput.$": "$",
    "SomeParameter": "hogehoge"
}

のように書きます。 実際はこのようにステートへの入力を使用するケースの方が多いので、 Parametersは入力のデータを「加工する」というような勘違いをしてしまいがちですが、 そのように考えてしまうのは混乱の元です。 何度も繰り返しますが、Parametersは「全く新しいJSONを定義する」ものです。 たまたまステートへの入力も参照できるだけと考えましょう。

さて、Parametersは、それが指定されるとLambda関数への入力系統が置き換えられる動作になります。 ということで、図はこんな感じになるかと思います。

InputPathからParametersへの矢印は、 ParametersからはInputPathを通った後の情報が 一応 参照できるよ、という意味です。

ResultPath

最後にResultPathです。 こちらも言葉で説明すると 「Lambda関数の出力をステートへの入力JSONのどこに埋め込むかを決める」です。

ResultPathは(JSONではなく)ただ1つの値を指定します。

"ResultPath": "$.result"

と指定すると、resultというKeyにLambda関数の出力を紐づき、 この項目が ステートへの入力のJSONに埋め込まれます

{                                {
  "aaa": {                         "aaa": {
    "AAA": "111"                     "AAA": "111"
  },                               },
  "bbb": [                         "bbb": [
    "BBB",                           "BBB",
    "BBBB"            ==>            "BBBB"
  ]                                ],
}                                  "result": {
                                     "message": "ReturnedMessage"
                                   }
                                 }

ここに来て、ステートへの入力が使えるという点がポイントです。

ResultPathという機能が存在することを考えると、ステートの変数の取り扱いの基本思想は、 「原則として入力されたJSONをそのまま次へ渡す。Lambda関数から出力があればそれも追加する」 ということなんだと思います(予想)。

ただ、これにはすぐ納得がいかないという方もいらっしゃることと思います。 その理由は、ResultPathのデフォルト動作は「Lambda関数の出力だけをステートの出力とする」 だからだと思います。

ここが、ステート間の変数受け渡しをちょっとややこしくしているところかと思いますが、 ResultPathには、2つの特別な指定方法があります。

ResultPath: "$" (デフォルト動作)

"ResultPath": "$"

と書いた場合、ResultPathは前述のデフォルト動作をします。 つまり、Lambda関数の出力だけがステートの出力となります(正確にはOutputPathフィルターを通る)。

この動作は、ステートの存在があまり意識されず、 Lambda関数の出力が次のステートに渡されることが自然と感じられるような使い方には非常にマッチします。 が、ステートの数が増えたりなど変数の取り回しパターンが増えてくると、 このデフォルト動作が理解の妨げになってしまうように感じます。 デフォルト動作とはいえ、特殊な動作だと心得ましょう!

ちなみに考え方として、 「どこに埋め込むか」の対象が"$"であり、これを「ステートの入力JSONの起点(root)」と読めば、 埋め込み先がrootであり、元のステートの入力全体が上書きされるという風に理解するのが良さそうです。

ResultPath: null

"ResultPath": null

と書いた場合、"$"とは逆で、Lambda関数の出力を捨てて、ステートへの入力JSONがそのままステートの出力となります。 「どこに埋め込むか」がnullなので、「どこにも埋め込まない」と考えればこちらも違和感はないです。

Lambda関数からの出力をわざわざ捨てているので、別にあっても困らないという意味では、 敢えてこういう書き方をする機会というのは少ないのかなと思います。

完成図(再掲)

ResultPathの挙動を書き加えれば、最初に掲載した図が完成します。

処理の流れの全体像がコンパクトにまとまっているつもりです。

各機能のまとめ

各機能について、できるだけ端的にまとめます。

InputPath、OutputPath

  • フィルターとしてはたらく
    • 情報を減らすことしかできないので、あまり活用の機会は多くない
  • 採用する1つのKeyを書く

Parameters

  • Lambda関数への入力をまったく新しく作る
    • ステートへの入力も使えるが、仕組みを理解するための本質ではない
  • 連想配列(JSON)で書く

ResultPath

  • Lambda関数の出力を、ステートへの入力に対して何という名前で埋め込むかを決める
  • デフォルト動作は特殊動作だと思って、いったん脇に置いておく。
  • 埋め込む名前のKeyを1つ指定する

Passタスクの挙動(おまけ)

冒頭でちょと触れた、何もしないPassタスクですが、 これはLambda関数で「入力されたJSONをそのまま返す」関数を実装した場合と同じ挙動となります。

まとめ

StepFunctionsステートマシンのステート間の変数受け渡しの全体像を1つの図にまとめてみました。 最初もやもやして統合的に理解できなかったものが、 調べることによって後ろにある構造が見えてくる瞬間は楽しいですね!

誰かの参考になれば幸いです!