🛠️

AWS Step FunctionsのInput/Outputを紐解いてみる

2021/09/13に公開

おはこんばんちは、AWS大好き芸人の@ken11です
このネタをどこに投下しようか結構悩んだんですが、わりかしきちんとまとめたのでZennに掲載することにしました。
今日はStep Functionsの話を。

Step Functions

皆さんAWSのStep Functionsは使ってますか?
僕はとてもお世話になっています
Lambdaオーケストレーション的ななにかが欲しい時、SageMakerオーケストレーション的ななにかが欲しい時等々、非常に便利な存在ですよね。

ただ如何せん、Input/Outputの取り扱いがややこしい点がネックです。
初見殺しでは?と思えるような複雑性を兼ね備える存在なので、Inputを引き回したいのにうまくいかないとか、Outputをうまく構築したいのにやり方がわからんとか、そういった部分で心折れる方もいらっしゃるのではないでしょうか。

今日はそのStep FunctionsのInput/Outputの仕組みについてハンズオン風に解説していきたいと思います。
一緒に手を動かしながら試してもらうことをオススメします

通常のInput/Output

では、まず最初に通常のInput/Outputがどうなるのか確認していきましょう。
今回はわかりやすさ重視でLambdaを使っていきます。

Lambda関数を作成する

とりあえずLambdaを作っていきましょう。

関数名はこのあと使うのでわかりやすいものにしておきましょう。
今回は複雑なことはしないので、このまま「一から作成」でコードを書いていきます。
なお、例はPythonで書いてますが、Ruby等お好みの言語で問題ないと思います。

ここでやっているのは、単純に入ってきた event から input_text というkeyの値をreturnしているだけです。シンプルですね。
編集したら忘れずにデプロイをしておいてください。

Step Functionsを作成する

続いて本題のStep Functionsを作成していきます。

Step Functionsはコードを書いて作成することもできますが、今回はUIで作成していきましょう。また、タイプは標準で問題ありません。

作成するとこのような画面になります。
デフォルトで用意される Pass state は今回は不要なので、右下の「状態を削除」から削除してしまいます。
(Step Functionsでは各Stepのことを State(ステート) と表現しますが、日本語訳がそのまま 状態 になっているのでややわかりづらいかもしれません。 状態 と書いてあったら「ステートのことなんだな」と読み替えることをオススメします)

次に、Lambdaのステップを追加していきます。
左側の一覧から Lambda invoke を選択、ドラッグしてワークフロー上にセットします。
そして、右に表示されているLambdaの設定でFunction nameに先ほど作成した関数を指定します。自分は sfn-example という名前を付けたので、それを指定してあります。
今回は他の項目はデフォルトのままで大丈夫です。

Lambdaの設定が終わったら作成します。

ステートマシンの名前は適宜お好きなものをつけてください。
roleは今回は新しいものをつくる形で大丈夫です。

実行する


作成が完了したらこのような画面になると思います。
なにはともかく実行してみましょう。
右上の「実行の開始」をクリックします。

するとこのようなポップアップが出てくると思います。
名前はなんでも大丈夫です。(入れなくてもOK)
インプットはJSONで指定していきます。
今回は

{
  "input_text": "hello, world"
}

というインプットで実行します。
右下の「実行の開始」をクリックすると実行されます。

実行が完了したら、Lambda invokeのステップを選択し、右側の ステップ出力 を確認してみましょう。
Payloadhello, world が出力されていることがわかります。
Lambdaのeventには実行時に入力したものが入ってきているため、Lambdaのコードで書いた event['input_text'] が出力されている状態です。

このように、通常の実行ではLambda関数でreturnされた値がステップ出力の Payload に乗って出力されます
Lambda関数のreturnを以下のように

return {"hoge": "hello", "fuga": "ok"}

とdictにすると、ステップ出力のPayloadも

{
  "Payload": {
    "hoge": "hello",
    "fuga": "ok"
  }
}

となります。

まずはこれがStep Functions基本のInput/Outputです。

InputPathを使う

ここからが難しくなっていくところです。
Step Functionsには入力制御に InputPath 、結果制御に ResultSelectorResultPath 、出力制御に OutputPath があります。
1つ1つ説明していきます、まず最初にInputPathから。

Lambda関数を編集する

まず、先ほど作成したLambdaを編集します。

ここからはInput/Outputを確認したいので、入ってきたeventをそのままreturnするように変更します。
編集したら忘れずにデプロイをします。

ステートマシンを編集する

続いて、先ほど作成したステートマシンを編集していきます。

編集しようとするとこの画面になると思います。
今回は作成したときと同じくUIで編集したいので、右上の「Workflow Studio」をクリックします。これで作成時と同じUIで編集する画面になります。

Lambda invokeのステップを選択し、 入力 タブの InputPath を設定していきます。
今回はここに $.input というパスを指定します。
設定ができたら右上の「適用して終了」をクリックします。

この画面になるので、そのまま右上の「保存」をクリックし、編集を終えます。

実行する

Lambdaとステートマシンの編集が終わったら、最初と同じように実行をしていきます。

今回の実行では先ほどよりも多くの情報をインプットにします。

{
  "input_text": "hello, world",
  "input": {
    "text": "good morning"
  }
}

さて、この実行結果のステップ出力がどうなるか考えてみましょう。
Lambdaを編集したので普通に考えれば上のインプットがそっくりそのまま Payload に乗って返ってくるでしょう。
しかし、今回はInputPathを設定したのでそのような結果にはなりません。
では、どうなるのか見ていきましょう。


まず実行結果のステップ入力です。
上のインプットですね。


これがステップ出力です。
Payload には

{"text": "good morning"}

だけが乗ってきていますね。

これがInputPathの仕組みです。
どういうことかというと、InputPathはステップ入力から実際にステップ(ステート)に渡すものをフィルターする役割を持っています。
今回は $.input というInputPathを指定しました。
これは元々のステップ入力

{
  "input_text": "hello, world",
  "input": {
    "text": "good morning"
  }
}

をフィルターして

{
  "text": "good morning"
}

という内容をステップに渡しています。Lambdaのeventに入ってきたのはこれだけってことですね。

「Path」という名前からファイルパスなのかな?なんのパスなのかな?と思ってしまうのではないかと思いますが、これはJsonPathの「Path」で、ステップ入力のJSONのどこ(Path)を実入力として扱うかを指定しているというわけです。
$ はルート(最上位)なので、今回の $.input はルートの下層の input keyを指定している形になります。
その他、JsonPathの使い方はこちらを参照することをオススメします。

OutputPathを使う

さて、InputPathが入力のフィルターということがわかれば、OutputPathの役割もすぐにわかると思います。
そうです、OutputPathは出力のフィルターです。
実際の挙動を見ていきましょう。

ステートマシンを編集する

先ほど同様に、まずステートマシンを編集していきます。
最初に先ほど指定したInputPathは削除してチェックを外しておいてください
(今回はステップ入力は特にフィルターしません)

出力 タブでOutputPathを指定していきます。
今回は $.message と指定します。
設定できたら保存します。

実行する

ステートマシンの編集ができたら早速実行してみます。

今回はさらにインプットの情報を増やします。

{
  "input_text": "hello, world",
  "input": {
    "text": "good morning",
    "message": "こんにちは"
  }
}

OutputPathは出力のフィルターで、今回は $.message を指定したというのをふまえて、結果を見ていきましょう。


まずこちらはステップ入力です。
上のインプットのとおりですね。


こちらがステップ出力です。
$.message を指定したので message keyの こんにちは だけが残りましたね。
これがOutputPathの機能です。

このように、InputPath/OutputPathはフィルターとして機能するオプションです。

ResultSelectorを使う

続いて結果制御のオプションについて説明していきます。

ステートマシンを編集する

これまでと同じようにステートマシンの編集をしていきます。
最初に先ほど指定したOutputPathは削除してチェックを外しておいてください
(今回はステップ出力は特にフィルターしません)

ResultSelectorを以下のように指定します。

{
  "japanese.$": "$..Payload.input.message",
  "english.$": "$..Payload.input_text"
}

設定できたら保存します。

実行する

ステートマシンの編集ができたら実行していきます。
今回のインプットは先ほどと同じです。

{
  "input_text": "hello, world",
  "input": {
    "text": "good morning",
    "message": "こんにちは"
  }
}

先ほどの編集内容でなんとなくわかるかと思いますが、ResultSelectorは結果を再編集することができます。
先ほど指定した

{
  "japanese.$": "$..Payload.input.message",
  "english.$": "$..Payload.input_text"
}

という内容で、どのように結果が再編集されるのか見ていきましょう。


こちらがステップ入力です。


そしてこちらがステップ出力です。
japaneseenglish という今までなかったkeyに結果が再配置されていることがわかります。

このように、出力前に結果を再編集することができるのがResultSelectorです。
とても便利なので、使う機会は多いんじゃないかなと思います。

ResultPathを使う

続いてResultPathについて紹介します。
このResultPathだけ特殊で、使い方が2つあるのでそれぞれ紹介していきたいと思います。

ステートマシンを編集する

これまで同様にステートマシンを編集していきます。

先ほどのResultSelectorはそのままで大丈夫です。

ResultPathを指定していきますが、今回は Discard result and keep original input を選択します。
設定できたら保存します。

実行する

今回のインプットも先ほどと同じです。

{
  "input_text": "hello, world",
  "input": {
    "text": "good morning",
    "message": "こんにちは"
  }
}

この状態で実行すると、ステップ出力はどうなるでしょうか?
見ていきましょう。


これがステップ出力です。
そうです、ステップ入力がそのままステップ出力になっています。

このようにResultPathの1つ目の機能としては、ステップ入力をそのままステップ出力にすることです。
これはこのステップの出力自体は必要ない(あるいは特に出力がないステップ)場合に、元のインプットを引き回すために利用することになるかと思います。

UIで設定する場合は上述の通り Discard result and keep original input を選ぶことで利用できますが、SDKなどコードで作成・編集している場合はResultPathにnullを指定することで実現できます。

ステートマシンを編集する

ResultPathのもう1つの機能を確認するため再度編集していきます。
引き続きResultSelectorはそのままで大丈夫です。

今度は Combine original input with result を選択します。
フィールドには $.MyOutputResult を入力しておきます。

設定できたら保存します。

実行する

今回のインプットも先ほどと同じです。

{
  "input_text": "hello, world",
  "input": {
    "text": "good morning",
    "message": "こんにちは"
  }
}

ステップ出力がどうなるか見ていきましょう


なにやら複雑なステップ出力となりました。
ResultPathの2つ目の機能は結果と元のインプットをセットで出力できるようにすることです。
今回は $.MyOutputResult と指定したので、ステップ出力に MyOutputResult というkeyが追加され、その下に元の結果が追加されているのがわかります。(これをわかりやすくするためにResultSelectorをそのままにしていました)

このように、指定したPathに結果を追加し、元のインプットと一緒に出力することができるのがResultPathです。
非常に強力な機能で、複数のステップでインプットを引き回したいときなど、かなり利用頻度の高いオプションかと思います。

※ちなみに、わかりやすくしようと機能が2つあるような書き方をしましたが、実は単純にステップの結果を出力のどこに追加するかを指定する機能というだけです。nullを指定した場合は結果はどこにも追加されないので破棄され、指定した場合はそのPathに追加されるという仕組みです。ただ、デフォルトは $ が指定されており、元のインプットを破棄する(上書きする)特殊な挙動なので、今回のように2つの機能があると思っていたほうが理解が楽かなと思います。

組み合わせて使う

最後に、複数ステップでこれらのオプションを組み合わせて使う例を紹介します。

ステートマシンを編集する

Lambdaステップを2つに増やして、それぞれ設定していきます。
(2つ目のLambdaステップも関数は1つ目のものと同じものを使っています)

まず1つ目のLambdaのInputPathに $.inputを指定し、

ResultPathに $.MyOutputResult を指定します。


続いて2つ目のResultSelectorに

{
  "result1.$": "$..MyOutputResult.Payload",
  "result2.$": "$.Payload.input_text"
}

と指定します。

実行する

今回のインプットも先ほどと同じです。

{
  "input_text": "hello, world",
  "input": {
    "text": "good morning",
    "message": "こんにちは"
  }
}

それぞれのステップでステップ入力とステップ出力がどうなるか見ていきましょう。


まず1つ目のLambdaステップのステップ入力です。
上のインプットそのままですね。
InputPathで $.input を指定しているので、ここから実際にLambdaに渡されるのは

{
  "text": "good morning",
  "message": "こんにちは"
}

というインプットになります。


続いて1つ目のLambdaのステップ出力です。
ResultPathを指定しているので、元のインプットが引き回されていることが確認できます。
Lambdaの結果自体は MyOutputResultPayload に追加されていることも確認できます。
上述の通り、Lambdaのインプットがフィルターされているので、当然ながら結果も

{
  "text": "good morning",
  "message": "こんにちは"
}

となっていますね。

次に2つ目のLambdaの方を確認していきましょう。

こちらが2つ目のLambdaステップのステップ入力です。
1つ目の出力結果がそのまま渡されていますね。


そしてこれがステップ出力です。
ResultSelectorで

{
  "result1.$": "$..MyOutputResult.Payload",
  "result2.$": "$.Payload.input_text"
}

と指定しているので、それぞれ result1result2 に再配置されていることが確認できます。

ステップ間でのインプットの引き回し等、参考になりましたら幸いです。

まとめ

Step FunctionsのInput/Outputの挙動について実際の画面を見ながら紹介しました。

  • InputPath
  • OutputPath
  • ResultSelector
  • ResultPath

と機能があって複雑ですが、その分うまく組み合わせると自由度がとても高いということがよくわかります。
JsonPathについての理解が必須になりますが、これらの機能を使うと任意のインプットを複数ステップで引き回すとか結果を整形・マージして最終出力とするといったことも可能ですので、是非有効活用していただけたらと思います。

※本当はExecutionInputは引き回さなくても直接呼べるとかそういうのは今回は触れませんでした。また機会があればその辺も書こうと思います。

Discussion