明日から活用できる JSONata版 Step Functions 入門
JSONata版のStep Functionsについて
2024/11にStep FunctionsはこれまでのAmazon States Languageだけでなく、JSONataという言語を利用できるようになりました。該当のAWS公式アップデート情報
JSONataは、JSON形式のデータを動的に変換・抽出・加工するための軽量なクエリ言語です。
既に1年ほど経過はしていますが、既存のASL資産があるなどの理由でまだJSONataでのStep Functionsを書いたことが無い、利用できていないという方もいるかと思います。
そこで本記事ではJSONataでStep Functionsを作成する際に役に立つ情報を、カスタマイズして活用できるサンプルと共にご紹介します。
JSONata vs ASL(従来の言語)
JSONataにはASLと比較して扱いやすい点が多くあります。
実際、AWSのドキュメントでも、JSONataの使用が推奨されています。
主な利点には以下があります。
- 入力・出力の処理が簡素化されて扱いやすい。特に変数によってデータの受け渡しがすっきりする。
- JSONataの組み込み関数が豊富でできることが多い
特に、一定のロジック(簡単なループや分岐)はすっきりと書けるようになったため、Lambdaの代替として活用しやすくなり、ランタイム管理対象の削減にもつながります。
一方、主な欠点は以下の通りです。
- リリースからの期間の関係で、構築例がまだ多くない
- できることが豊富ゆえに保守性の低い複雑なものも作れてしまう。
また、こちらはASLの時から引き続きではありますが、比較的特殊な記法/言語ではあるため、実プロジェクトでは、試行錯誤する時間を設けることが重要です。
JSONataでも注意すべき点
JSONataであっても、Step Functionsとして変わらずクォータなどに引き続き留意する必要があります。
代表的には以下ですが、以下に限らず最新のドキュメントの確認が必要です。
- (2025/11現在)入出力が256KiBに制限されるため、ListやDescribe系のAPIの呼び出し時には取得データ数を絞る等の考慮が必要。
- 各処理、及びステートマシン自体の再実行(Redrive)時の挙動について留意する必要がある。(冪等性の担保や、各ステートでのリトライを設定しておく)
どんなことができるか(サンプル)
JSONataの基本的な処理例をまとめたステートマシンを作成したので、これを元に説明します。
以下のステートマシンを応用することで、
- 特定条件を満たすEC2の一覧化や通知、起動停止
- S3のファイル一覧の取得、各ファイルに対しての処理
のような定常タスクを実現できます。
Lambdaでも可能ですが、Lambdaと比較した場合、
- Lambdaランタイムの維持に比べれば運用対応の頻度を少なくできる
- あまり意識しなくても各Stepごとにログが出るため、一定水準での監視が実現しやすい
- 実行時間の制約が緩い(Lambdaは15分だが、Step Functionsの標準ワークフローなら1年)
等利点があります。もちろん、統合先サービスとの相性などもありますが、特に運用系の処理についてはLambdaより使いやすいケースもあるのではないでしょうか。
全体像
ステートマシン全体像
ステートマシン定義
{
"Comment": "A description of my state machine",
"StartAt": "InitVariables",
"States": {
"InitVariables": {
"Type": "Pass",
"Next": "SetDummyExecutionStartTime",
"Assign": {
"loopCounter": 0,
"resultArr": []
}
},
"SetDummyExecutionStartTime": {
"Type": "Pass",
"Next": "DescribeInstances",
"Assign": {
"DummyStartTime": "2025-08-27T10:04:42Z"
}
},
"DescribeInstances": {
"Type": "Task",
"Arguments": "{% (\n $merge([\n {\"MaxResults\": 10},\n $exists($nextToken) ? {\"NextToken\": $nextToken} : {}\n ])\n) %}",
"Resource": "arn:aws:states:::aws-sdk:ec2:describeInstances",
"Next": "AccumLoopCounter"
},
"AccumLoopCounter": {
"Type": "Pass",
"Assign": {
"loopCounter": "{% $loopCounter := $loopCounter + 1 %}"
},
"Next": "HasNextTokenAndSaveNextToken"
},
"HasNextTokenAndSaveNextToken": {
"Type": "Pass",
"Next": "FilterEC2WithMultipleEBSAnsSaveToVariables",
"Assign": {
"hasNextToken": "{% $exists($states.input.NextToken) %}",
"nextToken": "{% $states.input.NextToken %}"
}
},
"FilterEC2WithMultipleEBSAnsSaveToVariables": {
"Type": "Pass",
"Next": "HasNextTokenAndLoopCounter<3",
"Assign": {
"resultArr": "{% (\n$func_filter_multiebs := function($instance){$count($instance.BlockDeviceMappings) > 1};\n$func_extract_onlyid := function($instance){$instance.InstanceId};\n$ret := $append($resultArr,$states.input.Reservations.Instances ~> $filter($func_filter_multiebs) ~> $map($func_extract_onlyid));\n$ret\n) %}"
}
},
"HasNextTokenAndLoopCounter<3": {
"Type": "Choice",
"Choices": [
{
"Next": "DescribeInstances",
"Condition": "{% $hasNextToken and ($loopCounter < 3) %}"
}
],
"Default": "Map"
},
"Map": {
"Type": "Map",
"ItemProcessor": {
"ProcessorConfig": {
"Mode": "INLINE"
},
"StartAt": "Dummy1",
"States": {
"Dummy1": {
"Type": "Pass",
"End": true,
"Output": {
"useCommonVariable": "{% $loopCounter %}"
}
}
}
},
"Next": "File Analysis",
"Items": "{% $resultArr %}",
"MaxConcurrency": 1
},
"File Analysis": {
"Type": "Map",
"ItemProcessor": {
"ProcessorConfig": {
"Mode": "DISTRIBUTED",
"ExecutionType": "STANDARD"
},
"StartAt": "Dummy2",
"States": {
"Dummy2": {
"Type": "Pass",
"End": true,
"Output": {
"test": "{% $states.input.name & $states.input.user %}"
}
}
}
},
"ItemReader": {
"Resource": "arn:aws:states:::s3:getObject",
"ReaderConfig": {
"InputType": "JSON"
},
"Arguments": {
"Bucket": "jsonata-demo-vbyjkdafwn",
"Key": "{% \"batch/\" & $fromMillis($toMillis($DummyStartTime),\"[Y0001][M01][D01]\",\"-0900\") & \".json\" %}"
}
},
"MaxConcurrency": 2,
"Label": "FileAnalysis",
"Next": "End"
},
"End": {
"Type": "Pass",
"End": true
}
},
"QueryLanguage": "JSONata"
}
1. 変数の設定
全体から参照・更新のできる変数を定義することができます。
今回は後段で時刻ベースでの処理もするため、ダミーとしてテスト用の時刻も設定しています。
変数設定処理
2. JSONataを利用したリクエストパラメタの組み立てとループ処理
JSONataを利用して、nextToken有/無のEC2 DescribeInstancesのSDK呼び出し用パラメタを組み立てています。
ループカウンタ用の変数を利用することで、最大ループ回数も制御しています。
また、DescribeInstancesの結果をJSONataでクエリ・整形し、EBSが2つ以上付与されているEC2インスタンスのインスタンスIDのリストを抽出しています。
EC2 Describe Instancesの呼び出し
ループカウンタの更新
EBSの条件に応じたインスタンスの絞り込み
3. API戻り値を利用したループ処理
JSONata以前から可能ではありますが、2で抽出したインスタンスIDのリストに対してさらにループ処理を行います。例えば対象のEC2についてSNS通知するなど、様々な処理を組み合わせることができます。
4. 実行日時に基づいたS3からのファイル取得とファイル内容に基づいたループ処理
3以前とは独立しています。
日付を元にファイル名を組み立てS3から取得し、ファイルの内容に応じてのループ処理をします。
今回はJSONファイルですが、JSONLやcsvなども利用可能です。
この処理を応用すれば、日次バッチ処理のような定常タスクを実現できます。
[
{"name":"target-1", "user" : "hoge", "price": 100},
{"name":"target-2", "user" : "fuga", "price": 500}
]
日時からファイル名を組み立てた上でのループ処理
まとめ
JSONata版のStep Functionsでは、これまでのASLでは複雑化した、もしくは書くことのできなかった処理をすっきりと書くことができる場合があります。
特に運用自動化では活用できる場面もあるかと思いますので、Lambdaで作るだけではなくStep Functionsでの実装も検討してみる価値があります。
記事の最後に附録として、JSONata版のStep Functionsを作成するにあたっての基本的な事項を日本語でまとめています。
現状日本語でのドキュメントはあまり多くないため、良ければ目を通していただけると幸いです。
仲間募集中です!
NTTデータ クラウド&データセンタ事業部では、以下の職種を募集しています。
- プライベートクラウドコンサル/エンジニア
- デジタルワークスペース構築/新規ソリューション開発におけるプロジェクトリーダー
- IT基盤(パブリッククラウド、プライベートクラウド)エンジニア
- パブリッククラウド/プライベートクラウドを用いた大規模プロジェクトをリードするインフラエンジニア
ソリューション紹介
(以下附録)JSONata版Step Functionsの基本
JSONata版のStep Functionsを作成していくにあたっては公式ドキュメント類を読むのが最も重要ですが、一方で、まず動かしてみるという視点では少しドキュメントは情報が多いため、以下にJSONata版Step Functionsを作成する上でのスタートポイントとなるような情報をまとめます。
基本的には公式の例に準拠しつつ、わかりやすさのために適宜JSONataや入力例を作成しています。
ステート入力の生データについて
JSONata版Step Functionsでは、各ステートは組み込み変数$statesを通して入力データを受け取ります。
$statesのうち、inputにはそのまま入力データが、contextにはExecutionId等のメタデータ関連が渡されます。
$states = {
"input": // Original input to the state
"result": // API or sub-workflow's result (if successful)
"errorOutput": // Error Output (only available in a Catch)
"context": // Context object
}
ステート入力値の変換について
その後、各ステートではArgumentsを利用して、入力値を変換・整形できます。
ここで、JSONata構文を利用することでJSONataの関数などを利用した高度な変換が可能になります。
"Arguments": {
"field1": 42,
"field2": "{% jsonata expression %}"
}
ステート出力の生データについて
SDK呼び出しなどの結果は、組み込み変数$statesに格納されます。
result及びerrorOutputがそれぞれ出力及びエラー出力です。
$states = {
"input": // Original input to the state
"result": // API or sub-workflow's result (if successful)
"errorOutput": // Error Output (only available in a Catch)
"context": // Context object
}
ステート出力値の変換及び変数の設定について
Outputにて出力値を変換した上で設定することができます。
また、Assignの構文で、ワークフロー全体で参照することのできる変数を宣言することができます。
"Output": "{% jsonata expression %}"
"Assign": {
"BucketName": "{% $states.input.BucketName %}"
}
JSONataの利用
JSONataは {% %} で囲むことで利用することができます。
"Assign": {
"BucketName": "{% $states.input.BucketName %}"
}
基本的な参照
下記の様に、ドット記法で階層的に値を参照できます。
入力例
省略
JSONata
Parent.Child.Grandchild
出力例
省略
尚、参照するパスのkeyに空白が含まれている場合はバッククォートでのクォーテーションが必要です。
入力例
省略
JSONata
`Some Key With Space`.hoge
出力例
省略
配列参照とクエリ
配列のインデックス参照ができます。また、Pythonチックにマイナスインデックスでの配列参照が可能です。インデックスは0-indexです。
入力例
省略
JSONata
ArrayOfData[-1]
出力例
省略
少し注意が必要な動きですが、配列の中にオブジェクトが入っている場合、子オブジェクトの特定のプロパティだけを指定すると、その指定したプロパティ値だけを集めた配列を取得します。
入力例:(以降この入力例を入力例Aとします)
{
"Staff": [
{
"type": "SE",
"name": "Cloud Taro"
},
{
"type": "SE",
"name": "Onpre Jiro"
},
{
"type": "SE",
"name": "App Saburo"
},
{
"type": "Manager",
"name": "Manager Hanako"
}
]
}
JSONata
Staff.name
出力例
[
"Cloud Taro",
"Onpre Jiro",
"App Saburo",
"Manager Hanako"
]
配列内にクエリを書くことで、オブジェクトを絞ることができます。
留意点として、この単純な書きかたの場合、クエリの結果が1件の時は単一のオブジェクト、複数件の時はオブジェクトの配列が返るため、返り値の型が変動します。回避するためには次の項の書き方をする必要があります。
この例ではmobileは1件しかないので、オブジェクトが返ります。
入力例
入力例A参照
JSONata
Staff[type="Manager"]
出力例
{
"type": "Manager",
"name": "Manager Hanako"
}
クエリの前に[]を付与することで、件数が1件であろうと配列で値を返すようにできます。
入力例
入力例A参照
JSONata
Staff[][type="Manager"]
出力例
[
{
"type": "Manager",
"name": "Manager Hanako"
}
]
ワイルドカードを利用した参照
ワイルドカードも利用可能です。単純なワイルドカード「*」は分かりやすく、パスを1階層代替することができます。末尾にも先頭にも指定可能です。
入力例
入力例A参照
JSONata
*.name
出力例
[
"Cloud Taro",
"Onpre Jiro",
"App Saburo",
"Manager Hanako"
]
ワイルドカード2連続(**)については、階層をまたいで探索します。
例えば、DevelopmentProgress.**では以下のようになります。
先頭に利用することで階層を問わず指定のプロパティを取得することもできます。
また、下記の例を見ていただくとわかる通り、いわゆるPathのWalkと捉える考え方も理解しやすいかと思います。
入力例
{
"DevelopmentProgress": {
"Infra": {
"Network": {
"VPC": "done",
"DNS": "doing"
}
},
"App": null
}
}
JSONata
DevelopmentProgress.**
出力例
[
{
"Infra": {
"Network": {
"VPC": "done",
"DNS": "doing"
}
},
"App": null
},
{
"Network": {
"VPC": "done",
"DNS": "doing"
}
},
{
"VPC": "done",
"DNS": "doing"
},
"done",
"doing",
null
]
各種演算
&演算子で文字列の結合が可能です。
JSONata
FirstName & ' ' & Surname
出力例
省略
「+」 「-」 「*」 「/」 「%」の基本的な算術演算子はすべて利用可能です。
JSONata
42*15
出力例
省略
「=」 「!=」 「<=」 「in」 等の基本的な比較演算子も利用可能です。
また、「and」、「or」も利用できます。 「=」は比較演算になるのに留意が必要です。
JSONata
(apple != orange) and (apple != pineapple)
出力例
省略
if文はありませんが、3項演算子は利用可能です。
JSONata
condition ? true_value : false_value
出力例
省略
後程関数定義のところでも利用しますが、「:=」を変数定義及び再代入に利用します。
再度になりますが「=」は比較演算になるので留意ください。
JSONata
val := 100
出力例
省略
また、いわゆるnull合体演算子もあります。特定の値がundefined(指定のキーが存在しない等)の場合に、デフォルト値などを設定する処理を簡単に書くことができます。
JSONata
val := nullable ?? 'fallback'
出力例
省略
オブジェクトの組み立て
参照に続けて、直接オブジェクトを組み立てることができます。
(公式ドキュメントでは、type: numberのようないかにも型宣言っぽい例になっているため、すこし混乱するかもしれませんが、要はkey: valueの形で宣言することで参照と同時にオブジェクトを組み立てることができます。)
入力例
{
"DevelopmentProgress": {
"Infra": {
"Network": {
"VPC": "done",
"DNS": "doing"
}
},
"App": null
}
}
JSONata
DevelopmentProgress.{
"InfraNWProgress": Infra.Network.VPC,
Infra.Network.VPC :Infra.Network.DNS
}
出力例
{
"InfraNWProgress": "done",
"done": "doing"
}
関数定義
JSONataでは関数定義ができます。Step Functions版でも可能です。
この関数はかなり高機能なのですが、やりすぎると途端にメンテナンス性が著しく落ちるので、一部の集約関数などに利用するスニペット程度の利用にとどめるのが無難と思います。
関数定義をする場合、2式/文以上になるので、「()」で全体を囲むのと、各処理の末尾に「;」を打つ必要があります。
尚、最後の式が全体の返り値として利用されます。
簡単な算術演算用の関数の例は以下の通りです。以下の例では2つの配列を要素ごとに掛け算して新しい配列を返す関数$elementwiseを作成しています。($mapは組み込み関数です)
JSONata
($elementwise := function($a, $b){
$map($a, function($x, $i){ $x * $b[$i] })
};
/*comment*/
$elementwise([0,1,2],[3,4,5])
)
出力例
[
0,
4,
10
]
オブジェクトの組み立てとグルーピング挙動(発展的)
オブジェクトの組み立ての時に指定する、keyに利用するプロパティについて、配列などで重複が発生するときは、グルーピングが行われます。
例えば、同じRoleでSizeの異なるサーバが複数ある入力例を考えると、グルーピングをすることで同RoleのSize一覧をまとめることができます。
ただ、$mapや$each,$mergeや$appendもJSONataにはありますので、これらの関数で明示的に処理を書くことで、JSONata特有の動きを減らし、様々な人にわかりやすくなる場合があるため、 本記事ではグルーピングに関連した仕様については発展的な仕様としてとらえています。
入力例
{
"Systems": {
"Company": "CompanyA",
"Subsystems": [
{
"SubsystemID": "sub1",
"Servers": [
{
"Role": "Web",
"Size": "t3.medium",
"CPUUsageAVG": 35,
"Autoscaling": true,
"NumberofInstances": 2
},
{
"Role": "App",
"Size": "t3.large",
"CPUUsageAVG": 20,
"Autoscaling": true,
"NumberofInstances": 4
}
]
},
{
"SubsystemID": "sub2",
"Servers": [
{
"Role": "DB",
"Size": "r6i.large",
"CPUUsageAVG": 3,
"Autoscaling": true,
"NumberofInstances": 1
},
{
"Role": "App",
"Size": "c5.xlarge",
"CPUUsageAVG": 80,
"Autoscaling": true,
"NumberofInstances": 6
}
]
}
]
}
}
JSONata
Systems.Subsystems.Servers{`Role`: Size[]}
出力例
{
"Web": [
"t3.medium"
],
"App": [
"t3.large",
"c5.xlarge"
],
"DB": [
"r6i.large"
]
}
例えば、CPUUsageAVGとNumberofInstancesをそれぞれ取り出す場合を考えてみます。この時、素直に書くと以下のような出力となります。少しイメージと違うなと思われた方も多いのではと思います。
JSONata
Systems.Subsystems.Servers{Role: {"CPU":CPUUsageAVG,"NumberofInstances":NumberofInstances}}
出力例
{
"Web": {
"CPU": 35,
"NumberofInstances": 2
},
"App": {
"CPU": [
20,
80
],
"NumberofInstances": [
4,
6
]
},
"DB": {
"CPU": 3,
"NumberofInstances": 1
}
}
この時、「$.{}」で明確に囲ってあげることで、各レコードを独立したオブジェクトとして扱うことができます。
JSONata
Systems.Subsystems.Servers{Role: $.{"CPU":CPUUsageAVG,"NumberofInstances":NumberofInstances}}
出力例
{
"Web": {
"CPU": 35,
"NumberofInstances": 2
},
"App": [
{
"CPU": 20,
"NumberofInstances": 4
},
{
"CPU": 80,
"NumberofInstances": 6
}
],
"DB": {
"CPU": 3,
"NumberofInstances": 1
}
}
尚、「$.{}」ではなく、「$.()」で囲むことで、各レコードを独立して処理することもできます。囲わない場合は、各要素ごとではなく各要素を集めた配列として処理されます。
JSONata
Systems.Subsystems.Servers{Role: $.{
"TotalCPU" : $.(CPUUsageAVG*NumberofInstances)}
}
出力例
{
"Web": {
"TotalCPU": 70
},
"App": [
{
"TotalCPU": 80
},
{
"TotalCPU": 480
}
],
"DB": {
"TotalCPU": 3
}
}
組み込み関数
文字列の包含を判定する$contains(<文字列>、<サブ文字列 or 正規表現>)が利用できます。
尚、正規表現の時は「"」で囲まず、「/」で囲むので留意が必要です。
JSONata
$contains("arn:aws:sns:us-east-1:123456789012:example-sns-topic-name", "example")
$contains("arn:aws:sns:us-east-1:123456789012:example-sns-topic-name", /arn.+us-east-1.*name/)
出力例
true
true
$split(<文字列>,<区切り文字>)も利用可能です。
JSONata
$split("arn:aws:sns:us-east-1:123456789012:example-sns-topic-name", ":")[-1]
出力例
"example-sns-topic-name"
そこまで利用機会は多くないかもしれませんが、base64エンコードも可能です。
JSONata
$base64encode("{\"lambda-payload\":\"hoge\"}")
出力例
"eyJsYW1iZGEtcGF5bG9hZCI6ImhvZ2UifQ=="
時間操作については、いくつかの関数をセットで紹介します。文字列からミリ秒に変換する$toMillis(),ミリ秒から文字列に変換する$fromMillis()をまとめて紹介します。(なお、現在時刻をミリ秒で取得する$millis()や、現在時刻を所定形式で取得できる$now()もありますが、おおよそ同じ流れで利用できるので割愛します)
例えば、アプリケーション独自の時刻形式をUTCに変換する場合は、文字列の結合でタイムスタンプを付与した上でtoMillisとfromMillisを挟むことで変換することができます。
JSONata
(
$app_timestamp := "2025/11/01 11:01:43.003";
$app_mill_utc :=$toMillis($app_timestamp & " +09:00", "[Y0001]/[M01]/[D01] [H01]:[m01]:[s01].[f001] [Z]");
$fromMillis($app_mill_utc)
)
出力例
"2025-11-01T02:01:43.003Z"
組み込みの高階関数
JSONataでは、いわゆるfor文はありませんが、ほとんど同様の処理が可能な高階関数がいくつか利用できます。
まず、Objectに対してループをすることのできる、$each関数です。
尚、each関数の第二引数は関数オブジェクトを取ります。関数オブジェクトは、function($value)もしくは、function($value,$key)のどちらでも利用可能です。keyが必要かどうかに合わせて選択ください。
JSONata
(
$data := {
"hoge":{"weight":0},
"fuga":{"weight":100}
};
$each($data, function($val, $key) { {$key : $val.weight*10} })
)
出力例
[
{
"hoge": 0
},
{
"fuga": 1000
}
]
配列ループの$map関数も利用可能です。
map関数では第二、第三関数にindex、配列全体をそれぞれ渡すことができるため、インデックスを用いた処理や、それらを組み合わせて自分のインデックスより前/後のデータを用いた処理なども可能です。
JSONata
(
$array_data := [1,2,3,4,5];
$map($array_data,function ($value, $index, $arr_itself) {$value*$index+$count($arr_itself)})
)
出力例
[
5,
7,
11,
17,
25
]
また、対象のリソースなどを絞り込む用途で利用頻度が高いと思われる$filter関数です。正規表現との組み合わせでリソースリストの絞り込みなどができます。
JSONata
(
$array_data := [1,2,3,4,5];
$filter($array_data,function ($value, $index, $arr_itself) {($value*$index+$count($arr_itself))>10})
)
出力例
[
3,
4,
5
]
$reduceです。いわゆる畳み込み的な演算ができます。EC2に付与されているボリュームサイズの合計を算出したりする際に利用可能ですが、$reduceは、$mapと$sum等代用できるケースもあり、必須度は少し落ちるかもしれません。
JSONata
(
$array_data := [1,2,3,4,5];
$reduce($array_data,function ($before, $value, $index, $arr_itself) {$before + $value*$index+$count($arr_itself)},10000)
)
出力例
10065
NTT DATA公式アカウントです。 技術を愛するNTT DATAの技術者が、気軽に楽しく発信していきます。 当社のサービスなどについてのお問い合わせは、 お問い合わせフォーム nttdata.com/jp/ja/contact-us/ へお願いします。