Open21

AWS Step Functionsで遊ぶ

inomotoinomoto

Webサービスでイベントに対して通知したりデータ転送したりみたいなユースケースを想定して遊ぶ

inomotoinomoto

何も作ったことなければStepFunctionsのトップページ?的なところからHello Worldワークフローが作れる。
これを作ってWorkflow Studioで遊ぶだけですでに楽しい。

inomotoinomoto

まずは全体のinputとoutputを任意に/簡単に用意できるようにして遊びやすくする。

inputとしてはEventBridgeに適当なイベントバスを用意してサンプルイベント作成からやるのがよかろう。
outputは...直接Cloudwatch Logsにでも流せれば楽だったが、無いようなのでLambdaをキックしてその実行ログをcw logsで見るようにしよう。

inomotoinomoto

とりあえずlambda

def lambda_handler(event:, context:)
    puts "lambda triggered!: event=#{event.to_json}"
    { statusCode: 200, body: "ok" }
end

cw logsになんか出ていればok

inomotoinomoto

適当に発火。inputは { "Comment": "Insert your JSON here" }で(デフォルトのまま)

logを見ると

おっけー。

inomotoinomoto

次に適当なEventBridgeのイベントバスとルールを生やす。
バスは適当に名前をつけるだけでいいとして、ルールでは

  • パターン: カスタムパターン({"source": ["app.sample_event"]})
  • ターゲット: さっき作ったステートマシン

とする。

作ったらイベントバスに適当にイベントを投げる。
イベントソースは上で指定したapp.sample_eventとし、詳細jsonは {"name": "hoge", "value": 3} とか入れておく。詳細タイプもなんか適当に入れる。なんでもいいと思う。

inomotoinomoto

そのまま送信すると、

  • ステートマシン詳細の「実行」タブに新しくエントリができている
  • cw logsにもログが出力されている

ことが確認できた。
ステートマシンの実行エントリを見ると、発火したときの入力と出力が見える。

実行入力
{
  "version": "0",
  "id": "de082f54-713e-084b-84be-08b38fa90471",
  "detail-type": "sample_event",
  "source": "app.sample_event",
  "account": "277803361278",
  "time": "2021-07-25T07:48:21Z",
  "region": "ap-northeast-1",
  "resources": [],
  "detail": {
    "name": "hoge",
    "value": 3
  }
}
実行出力
{
  "statusCode": 200,
  "body": "ok"
}

入力はEventBridgeでサンプル発火したイベント全体のまま。出力はlambdaのreturnと同じになっている。なるほどなー。

inomotoinomoto

テスト用にイベントを発火するためにポチポチするのが面倒なのでcliからやろうと思ったが、むしろそっちのがめんどくさそうだ。
↑の入力に近いjsonを用意するのだが、detail部分がobjectではなくjson-stringになってないといけない。テンプレートしつつ二重jsonするのは流石にめんどい...

inomotoinomoto

出力はlambdaのreturnと同じなのかーと思っていたが、stepの定義にちゃんと書いてあった。
"OutputPath": "$.Payload",となっているし、$全体の値の例はWorkflow StudioのInvokeの出力のところに丁寧に書いてある。

inomotoinomoto

InvokeのStateのInputPathをいじるとlambdaが受け取るeventが調整できる。

デフォルトの状態ではeventとして受け取る値(cw logsに出てくる値)は↑の実行入力と全く同じだが、"InputPath": "$.detail"と指定してやってみるとdetailの中身だけがlogに出てきた。

inomotoinomoto

そういえばEventBridgeでイベント作成するときのイベントソースと詳細タイプってどういうふうに使い分けるんだろ。

inomotoinomoto

https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/eb-event-patterns.html
あーうーんなるほど、sourceとdetail-typeなんだからそうだな。SNSとかでsourceをフィルタリングするsomethingだと認識してしまったから変になっていたけど、確かにsourceとtypeならそうか。

アプリケーションから投げるなら、sourceは例えば公開サービス・管理画面・バッチ処理みたいな分け方で、typeはユーザ登録完了・コンバージョン・エラー通知とかそんな感じか。

inomotoinomoto

step functions想定なら

  • イベントバスがシステム全体(の環境ごと)で一つ
  • sourceの種類が上記な感じで数個程度ある
  • イベントルールとステートマシンがsourceの種類数x(1~N)ほどある
  • detail-typeはステートマシンの中で振り分ける

みたいな?

inomotoinomoto

ここからはイベントタイプを増やしたり分岐を入れたりしてみる。

inomotoinomoto

ともあれやっぱりコピペやテンプレ調整でイベント発火したいので、lambdaで実装した。

require 'json'
require 'aws-sdk-eventbridge'

def lambda_handler(event:, context:)
  put_event(
    "new_user", {
      name: "Bob"
    }
  )
  { statusCode: 200, body: 'ok' }
end

def put_event(type, detail)
  source = "app.sample_event"
  bus = "sbox_stepfunctions"

  Aws::EventBridge::Client.new
    .put_events({
      entries: [
        {
          time: Time.now,
          source: source,
          resources: [],
          detail_type: type,
          detail: JSON.generate(detail),
          event_bus_name: bus,
        },
      ],
    })
    .tap do |resp|
      puts resp
    end
end
inomotoinomoto

new_userbuyというtypeを想定して作ってみる。

まずは発火するlambda(というかイベント)

def lambda_handler(event:, context:)
  new_user
  buy
  
  { statusCode: 200, body: 'ok' }
end

def new_user(index = nil)
  index = 6.times.to_a.sample unless index
  put_event(
    "new_user", {
      name: ["Alice", "Bob", "Carol", "Dave", "Ellen", "Frank"][index]
    }
  )
end

def buy(index = nil)
  index = 6.times.to_a.sample unless index
  put_event(
    "buy", {
      user: ["Alice", "Bob", "Carol", "Dave", "Ellen", "Frank"][index],
      item: "awesome item"
    }
  )
end

そしてステートマシン側。Choiceというタイプを入れて分岐する。

State
    "Choice detail-type": {
      "Type": "Choice",
      "Choices": [
        {
          "Variable": "$.detail-type",
          "StringEquals": "new_user",
          "Next": "run for new_user"
        },
        {
          "Variable": "$.detail-type",
          "StringEquals": "buy",
          "Next": "run for buy"
        }
      ],
      "Default": "Fail"
    },

てかdetail_typeだと思ってたものがdetail-typeだった。まさかのケバブケース...

run for xxxは元々のlambdaのStateを複製して使いまわした。それぞれのStateに下記を入れて個別のアクションっぽくした。

      "ResultSelector": {
        "event_type": "new_user",
        "user": "$.detail"
      }
      "ResultSelector": {
        "event_type": "buy",
        "order": "$.detail"
      }
inomotoinomoto

横に出ている図はこう。これがデフォルトで出てくるのとても良い。

Choiceはデフォルトが必須なので、unexpected typeとしてfailにしてみた。

inomotoinomoto

冒頭のlambdaを走らせると実行結果が2つでてくる。

出力いじったけど、結果のインスペクタが超見やすいのでどっちでも良かった...
あと並べるためにブラウザのウィンドウ幅を縮めたらインスペクタ部がちょっと崩れてる。

inomotoinomoto

ところでステートマシンの詳細画面から「実行の開始」で任意のイベント入力を入れてキックできるな...まぁEventBridgeをlambdaでキックしたほうが本番に近いからいいや()

inomotoinomoto

そういえばStep Functionsの料金形態どうなってるのか確認。
https://aws.amazon.com/jp/step-functions/pricing/

Step Functions の無料利用枠には、1 か月あたり 4,000 回の状態遷移が含まれます。料金はすべて日単位で計算され、月ごとに請求されます。

Step Functions の無料利用枠は、12 か月間の AWS 無料利用枠の期間が終了しても自動的に期限切れになることはありません。既存および新規の AWS のお客様は、無期限で利用できます。

ポチポチして遊ぶだけなら無料と思って間違いない。