🎵

AWS Step Functions の JSONata と 変数 で FizzBuzz やってみた

に公開

概要

AWS Step Functions の JSONata と 変数 が便利でしたので、簡単な例を挙げつつ紹介していきたいと思います

想定読者

  • 以前 Step Functions を使ったことはあるが、JSONata と 変数 については知らない人
  • Step Functions で JSONata や 変数 を使った例を見たい人

JSONata とか 変数 って何?

簡単に説明すると2024年末頃に Step Functions がサポートした機能です
詳しくは AWS の 公式ドキュメント および公式ブログの記事 が分かりやすくてオススメです
JSONata 自体は Step Functions の独自技術ではなく、以前より存在する JSON クエリおよび変換を行うために開発された言語のようです
私は恥ずかしながら Step Functions がサポートするまで存在自体を知りませんでした…

これらの機能の何が嬉しいか、誤解を恐れず個人の見解を述べさせて頂きますと

  • JSONata によって、簡単な処理なら Step Functions だけで簡潔に書けるようになった
  • 変数 によって複数のステートを跨いでパラメータを渡すような処理をシンプルかつ直感的に行えるようになった

といった感じです

色々やってみる

それではここからは具体的な例を見ていきたいと思います

JSONata で親の顔より見た FizzBuzz をやる

今更説明するまでもないとは思いますが確認のため FizzBuzz の要件をまとめます
FizzBuzz は任意の数値を渡して以下の結果を得られるもののことです

3 と 5 両方で割り切れる数値の場合は FizzBuzz
3 で割り切れる数値の場合 Fizz
5 で割り切れる数値の場合 Buzz
それ以外の場合は数値そのまま

ここでは 1~100 の数字の FizzBuzz を返す ASL を作ってみます

{
  "Comment": "A description of my state machine",
  "StartAt": "FizzBuzz",
  "States": {
    "FizzBuzz": {
      "Type": "Pass",
      "End": true,
      "Output": {
        "FizzBuzz": "{% $map([1..100], function($v) {\n    ($v % 15 = 0) ? \"FizzBuzz\" :\n    ($v % 3 = 0) ? \"Fizz\" :\n    ($v % 5 = 0) ? \"Buzz\" :\n    $v\n}) %}"
      }
    }
  },
  "QueryLanguage": "JSONata"
}

FizzBuzz の中の JSONata を抜き出して見易くしたものがこちらです

$map([1..100], function($v) {
    ($v % 15 = 0) ? "FizzBuzz" :
    ($v % 3 = 0) ? "Fizz" :
    ($v % 5 = 0) ? "Buzz" :
    $v
})

$map 引数に [1..100] で 1 から 100 までの数値の配列を生成したものと、FizzBuzz の判
定を行う関数を渡して、1 から 100 までの数値を判定しています

如何でしょうか?
個人的にはややこしいフローを組んだり Lambda などを使わずともほぼ JSONata のみで簡潔に記述できるのが素晴らしいなと思いました

実際に実行した時の見た目はこのようになります

ワークフロー実行時に引数として渡した値の FizzBuzz を返す

前の例を少しだけ発展させて、引数に渡した数値の FizzBuzz を返す ASL を考えてみます

{
  "Comment": "A description of my state machine",
  "StartAt": "FizzBuzz",
  "States": {
    "FizzBuzz": {
      "Type": "Pass",
      "End": true,
      "Output": {
        "FizzBuzz": "{% (\n  $fizzBuzz := function($v) {\n      ($v % 15 = 0) ? \"FizzBuzz\" :\n      ($v % 3 = 0) ? \"Fizz\" :\n      ($v % 5 = 0) ? \"Buzz\" :\n      $v\n  };\n  $fizzBuzz($states.input.value);\n) %}"
      }
    }
  },
  "QueryLanguage": "JSONata"
}

FizzBuzz の中身の JSONata を抜き出して見易くしたものがこちらです

(
  $fizzBuzz := function($v) {
      ($v % 15 = 0) ? "FizzBuzz" :
      ($v % 3 = 0) ? "Fizz" :
      ($v % 5 = 0) ? "Buzz" :
      $v
  };
  $fizzBuzz($states.input.value);
)

今回、ワークフローの実行時に以下のような JSON で FizzBuzz の判定対象となる数値を渡すことを想定しています

{
  "value": 1
}

実行時に渡す引数のオブジェクトは JSONata では $states.input として取得できます
今回のケースでは $states.input.value として値を参照しています

また、注意点としまして、今回の JSONata は2行に分かれているため、全体を小括弧で囲んだ上でセミコロンで終端している点にご注意ください
こちらの内容は、変数と JSONata を使った AWS Step Functions での開発者エクスペリエンスの簡素化 の中で以下のように説明されています。

JSONata の複数行の式は有効な JSON ではありません。したがって、セミコロン “;” で区切られた文字列として1 行を使用し、最後の行で式を返すようにしてください。

S3 から取得した JSON 形式の config を複数の処理で使い回す

おまけで、少しだけ実用的(?)な例として、S3 から取得した JSON 形式の config を複数の処理で使い回す ASL を考えてみます

ワークフローの見た目は以下のようになります

このサンプルでは GetObject ステートで取得した JSON 形式のコンフィグファイルをパースした上で変数として定義し、後続の処理 exec1, exec2(サンプルのため実体は Pass で変数を Output に渡すだけ) で参照します

{
  "Comment": "A description of my state machine",
  "StartAt": "GetObject",
  "States": {
    "GetObject": {
      "Type": "Task",
      "Arguments": {
        "Bucket": "{% $states.input.bucketName %}",
        "Key": "{% $states.input.configPath %}"
      },
      "Resource": "arn:aws:states:::aws-sdk:s3:getObject",
      "Assign": {
        "Config": "{% $parse($states.result.Body) %}"
      },
      "Next": "exec1"
    },
    "exec1": {
      "Type": "Pass",
      "Next": "exec2",
      "Output": {
        "exec1Parameters": "{% $Config.exec1Parameters %}"
      }
    },
    "exec2": {
      "Type": "Pass",
      "End": true,
      "Output": {
        "exec2Parameters": "{% $Config.exec2Parameters %}"
      }
    }
  },
  "QueryLanguage": "JSONata"
}

今回、ワークフローの実行時に以下のような JSON でコンフィグファイルの参照先を渡すことを想定しています

{
  "bucketName": "<任意のバケット名>"
  "configPath": "<コンフィグの JSON ファイルのパス>"
}

S3 には以下のような JSON ファイルをアップロードしました

{
  "exec1Parameters": {
    "param1": "foo",
    "param2": "bar"
  },
  "exec2Parameters": {
    "param1": "baz",
    "param2": "qux"
  }
}

注意点として JSON をパースしたい場合、JSONata には $eval という関数がありますが、Step Functions で JSON をパースしたい場合にはこの関数は使えず、代わりに $parse という関数が用意されているのでこちらを使います

実行した時の exec1, exec2 それぞれの Output は以下のようになります

おわりに

Step Functions で昨年サポートされた JSONata と変数について具体例を挙げつつ紹介してみました
最近の Step Functions は非常に便利になってますので、そもそもまだ使ったことがない方はもちろん、以前使ってみたけど使い勝手を理由に結局採用を見送ってしまったような方はこれを機会に是非触ってみてください

この記事が誰かのお役に立てば幸いです

あしたのチーム Tech Blog

Discussion