これは、なにをしたくて書いたもの?
JSONata?
JSONataは、JSON用のクエリーおよび変換言語です。
JSONata Documentation · JSONata
AWS Step FunctionsのJSONata対応
AWS Step Functionsではデータ操作を行うのにJSONPathを使っていましたが、2024年11月からJSONataも使えるように なりました。
変数と JSONata を使った AWS Step Functions での開発者エクスペリエンスの簡素化 | Amazon Web Services ブログ
Step Functions での JSONata を使用したデータの変換 - AWS Step Functions
AWS Step FunctionsでJSONataを使うと、JSONPathを利用していた時よりも簡単にデータ操作を記述できるようになります。
AWS Step FunctionsでJSONataを使うには、ステートマシン全体もしくはステートのQueryLanguage
にJSONata
を指定します。
{ "QueryLanguage": "JSONata", // Set explicitly; could be set and inherited from top-level "Type": "Task", ... "Arguments": { // JSONata states do not have Parameters "static": "Hello", "title": "{% $states.input.title %}", "name": "{% $customerName %}", // With $customerName declared as a variable "not-evaluated": "$customerName" } }
Step Functions ワークフローの Amazon States Language でのステートマシン構造 - AWS Step Functions
これで対象のステートでJSONataが使えるようになり、JSONPathは利用できなくなります。ワークフロー全体で指定した
場合は、個々のステートでQueryLanguage
を再度指定しない限りはJSONataで記述することになります。
JSONPathからJSONataに切り替えると、ステートの記述方法に以下のような変化があります。
- InputPath、Parameters → Arguments
- ResultSelector、ResultPath、OutputPath → Output
ASL上でJSONataを記述するには、"{% <JSONata expression> %}"
と書く必要があります。そしてJSONPathを使っている時は
フィールド名の最後に.$
を付けていましたが、これは必要なくなります。
また$states
という予約変数も追加されます。
# Reserved $states variable in JSONata states $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 }
Step Functions での JSONata を使用したデータの変換 / 予約変数: $states
この予約変数がなかなか便利で、JSONPathを使う時は入出力の一部しか参照できないことが多かったのですが、JSONataに
すると扱える幅が大きく広がります。
JSONataの演算子や関数も利用できるようになります。
参考)
[アップデート] AWS Step Functions で変数が使えるようになりました | DevelopersIO
これでLambdaが不要に?!Step FunctionsのJSONata対応について - Speaker Deck
AWS Step FunctionsのPassステートで入力内容を保ったまま出力を追加・編集する(JSONata形式) – Kiwi blog
LocalStackでのAWS Step Functions
LocalStackでは3.0でAWS Step Functionsの実装が新しくなったようです。
New StepFunctions implementation in LocalStack 3.0
JSONataへの対応も入ったみたいです。
しかもAWSと提携して実現しているとか。
AWS Step Functions が Variables と JSONata の変換で開発者のエクスペリエンスを簡素化 - AWS
情報を見るのはこれくらいにして、AWS Step FunctionsとJSONataを使ってみましょう。
環境
今回の環境はこちら。
$ python3 --version Python 3.12.3 $ pip3 --version pip 24.0 from /usr/lib/python3/dist-packages/pip (python 3.12) $ awslocal --version aws-cli/2.25.0 Python/3.12.9 Linux/6.8.0-55-generic exe/x86_64.ubuntu.24 $ samlocal --version SAM CLI, version 1.135.0 $ localstack --version LocalStack CLI 4.2.0
LocalStackを起動。
$ localstack start
お題
今回のお題は、前にやったこちらのエントリーをJSONPathからJSONataで書き直すものにします。
LocalStack × AWS SAMで、AWS Lambda関数を使った入出力を扱うAWS Step Functionsのワークフローを書いてみる - CLOVER🍀
つまり、こういう入力を与えると
{ "message": "Hello World" }
こうなって
{ "result": "★★★Hello World★★★", "message": "★★★Hello World★★★", "inputOriginalMessage": "Hello World", "withStar": { "result": "★★★Hello World★★★" } }
最後にこうなるワークフローを組みます。
{ "result": "***★★★Hello World★★★***", "inputOriginalMessage": "Hello World", "withStar": { "result": "★★★Hello World★★★" }, "wishAsterisk": { "result": "***★★★Hello World★★★***" } }
ちなみに、この時のASLの定義はこんな感じでした。
{ "Comment": "My Input Output State Machine", "StartAt": "WithStar", "States": { "WithStar": { "Type": "Task", "Resource": "${StarFunctionArn}", "ResultPath": "$.withStar", "Next": "FormatStarOutput" }, "FormatStarOutput": { "Type": "Pass", "Parameters": { "message.$": "$.withStar.result", "inputOriginalMessage.$": "$.message", "result.$": "$.withStar.result", "withStar.$": "$.withStar" }, "Next": "WithAsterisk" }, "WithAsterisk": { "Type": "Task", "Resource": "${AsteriskFunctionArn}", "ResultPath": "$.withAsterisk", "Next": "FormatAsteriskOutput" }, "FormatAsteriskOutput": { "Type": "Pass", "Parameters": { "result.$": "$.withAsterisk.result", "inputOriginalMessage.$": "$.inputOriginalMessage", "withStar.$": "$.withStar", "withAsterisk.$": "$.withAsterisk" }, "End": true } } }
これがJSONataでどれくらい書きやすくなったかを比べたいと思います。
AWS Lambda関数を作成する
まずはAWS Lambda関数を作成していきます。この内容だと、もはやAWS Lambda関数は要らない気もしますが、まずは前回の
置き換えということで。
$ samlocal init --name sfn-localstack-input-output-jsonata --runtime python3.12 --app-template hello-world --package-type Zip --no-tracing --no-application-insights --structured-logging $ cd sfn-localstack-input-output-jsonata
使わないものを削除。
$ rm -rf events hello_world tests
AWS Lambda関数を作成します。
渡された文字列に「★」を加えるAWS Lambda関数。
star/app.py
def lambda_handler(event, context) -> dict: print(f"star function input = {event}") message = event["message"] return { "result": ("★" * 3) + message + ("★" * 3) }
渡された文字列に「*」を加えるAWS Lambda関数。
asterisk/app.py
def lambda_handler(event, context) -> dict: print(f"asterisk function input = {event}") message = event["message"] return { "result": ("*" * 3) + message + ("*" * 3), }
どちらも入力を標準出力に書き出すようにしています。
AWS Step Functionsのステートマシンの定義。今回は2つのステートで構成していて、AWS Lambda関数のARNは
パラメーター化しています。
statemachine/input-output-state-machine.asl.json
{{ "Comment": "My Input Output State Machine, Using JSONata", "QueryLanguage": "JSONata", "StartAt": "WithStar", "States": { "WithStar": { "Type": "Task", "Resource": "${StarFunctionArn}", "Output": "{% {'message': $states.result.result, 'inputOriginalMessage': $states.input.message, 'result': $states.result.result, 'withStar': $states.result} %}", "Next": "WithAsterisk" }, "WithAsterisk": { "Type": "Task", "Resource": "${AsteriskFunctionArn}", "Output": "{% {'result': $states.result.result, 'inputOriginalMessage': $states.input.inputOriginalMessage, 'withStar': $states.input.withStar, 'withAsterisk': $states.result} %}", "End": true } } }
ポイントは、まずは"QueryLanguage": "JSONata"
の指定ですね。今回はステートマシン全体で指定しました。
"QueryLanguage": "JSONata",
そして、今回はOutput
で変数操作が全部できてしまったので、JSONPathの時に使っていた変数操作のためだけのステートが
なくなりました。
JSONataを使う時は、{% <JSONata expression> %}
という表記にする必要があります。またJSONPathの時は動的に評価する
項目は末尾につけていた.$
(下の例でいえばOutput.$
としていた)が不要になります。
"Output": "{% {'message': $states.result.result, 'inputOriginalMessage': $states.input.message, 'result': $states.result.result, 'withStar': $states.result} %}",
AWS SAMテンプレート。
template.yaml
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > sfn-localstack-input-output-jsonata Sample SAM Template for sfn-localstack-input-output-jsonata Globals: Function: Timeout: 3 LoggingConfig: LogFormat: JSON Resources: Resources: StarFunction: Type: AWS::Serverless::Function Properties: FunctionName: star-function CodeUri: star/ Handler: app.lambda_handler Runtime: python3.10 Architectures: - x86_64 AsteriskFunction: Type: AWS::Serverless::Function Properties: FunctionName: asterisk-function CodeUri: asterisk/ Handler: app.lambda_handler Runtime: python3.10 Architectures: - x86_64 InputOutputStateMachine: Type: AWS::Serverless::StateMachine Properties: Name: input-output-state-machine DefinitionUri: statemachine/input-output-state-machine.asl.json DefinitionSubstitutions: StarFunctionArn: !GetAtt StarFunction.Arn AsteriskFunctionArn: !GetAtt AsteriskFunction.Arn Outputs: StarFunction: Description: Star Lambda Function ARN Value: !GetAtt StarFunction.Arn StarFunctionIamRole: Description: Implicit IAM Role created for Star function Value: !GetAtt StarFunctionRole.Arn AsteriskFunction: Description: Asterisk Lambda Function ARN Value: !GetAtt AsteriskFunction.Arn AsteriskFunctionIamRole: Description: Implicit IAM Role created for Asterisk function Value: !GetAtt AsteriskFunctionRole.Arn InputOutputStateMachineArn: Description: "Input Output State machine ARN" Value: !Ref InputOutputStateMachine InputOutputStateMachineRoleArn: Description: "IAM Role created for Input Output State machine based on the specified SAM Policy Templates" Value: !GetAtt InputOutputStateMachineRole.Arn
デプロイ。
$ samlocal deploy --region us-east-1
完了しました。
CloudFormation events from stack operations (refresh every 5.0 seconds) ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ResourceStatus ResourceType LogicalResourceId ResourceStatusReason ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- CREATE_IN_PROGRESS AWS::CloudFormation::Stack sfn-localstack-input-output-jsonata - CREATE_IN_PROGRESS AWS::IAM::Role StarFunctionRole - CREATE_COMPLETE AWS::IAM::Role StarFunctionRole - CREATE_IN_PROGRESS AWS::IAM::Role AsteriskFunctionRole - CREATE_COMPLETE AWS::IAM::Role AsteriskFunctionRole - CREATE_IN_PROGRESS AWS::IAM::Role InputOutputStateMachineRole - CREATE_COMPLETE AWS::IAM::Role InputOutputStateMachineRole - CREATE_IN_PROGRESS AWS::Lambda::Function StarFunction - CREATE_COMPLETE AWS::Lambda::Function StarFunction - CREATE_IN_PROGRESS AWS::Lambda::Function AsteriskFunction - CREATE_COMPLETE AWS::Lambda::Function AsteriskFunction - CREATE_IN_PROGRESS AWS::StepFunctions::StateMachine InputOutputStateMachine - CREATE_COMPLETE AWS::StepFunctions::StateMachine InputOutputStateMachine - CREATE_COMPLETE AWS::CloudFormation::Stack sfn-localstack-input-output-jsonata - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- CloudFormation outputs from deployed stack --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Outputs --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Key StarFunction Description Star Lambda Function ARN Value arn:aws:lambda:us-east-1:000000000000:function:star-function Key StarFunctionIamRole Description Implicit IAM Role created for Star function Value arn:aws:iam::000000000000:role/sfn-localstack-input-output-jsonata-StarFunctionRole-c8227ab2 Key AsteriskFunction Description Asterisk Lambda Function ARN Value arn:aws:lambda:us-east-1:000000000000:function:asterisk-function Key AsteriskFunctionIamRole Description Implicit IAM Role created for Asterisk function Value arn:aws:iam::000000000000:role/sfn-localstack-input-output-jsona-AsteriskFunctionRole-c6d4eabe Key InputOutputStateMachineArn Description Input Output State machine ARN Value arn:aws:states:us-east-1:000000000000:stateMachine:input-output-state-machine Key InputOutputStateMachineRoleArn Description IAM Role created for Input Output State machine based on the specified SAM Policy Templates Value arn:aws:iam::000000000000:role/sfn-localstack-input-output-j-InputOutputStateMachineR-9e5607a0 --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Successfully created/updated stack - sfn-localstack-input-output-jsonata in us-east-1
では、実行してみます。
$ awslocal stepfunctions start-execution --state-machine arn:aws:states:us-east-1:000000000000:stateMachine:input-output-state-machine --name input-output --input '{"message": "Hello World"}' { "executionArn": "arn:aws:states:us-east-1:000000000000:execution:input-output-state-machine:input-output", "startDate": "2025-03-21T16:44:26.881388+09:00" }
結果はこちら。
$ awslocal stepfunctions describe-execution --execution-arn arn:aws:states:us-east-1:000000000000:execution:input-output-state-machine:input-output { "executionArn": "arn:aws:states:us-east-1:000000000000:execution:input-output-state-machine:input-output", "stateMachineArn": "arn:aws:states:us-east-1:000000000000:stateMachine:input-output-state-machine", "name": "input-output", "status": "SUCCEEDED", "startDate": "2025-03-21T16:44:26.881388+09:00", "stopDate": "2025-03-21T16:44:29.008236+09:00", "input": "{\"message\":\"Hello World\"}", "inputDetails": { "included": true }, "output": "{\"result\":\"***\\u2605\\u2605\\u2605Hello World\\u2605\\u2605\\u2605***\",\"inputOriginalMessage\":\"Hello World\",\"withStar\":{\"result\":\"\\u2605\\u2605\\u2605Hello World\\u2605\\u2605\\u2605\"},\"withAsterisk\":{\"result\":\"***\\u2605\\u2605\\u2605Hello World\\u2605\\u2605\\u2605***\"}}", "outputDetails": { "included": true } }
わかりにくいですが、狙った結果が得られています。JSONPathで作った時と同じ結果です。
"output": "{\"result\":\"***\\u2605\\u2605\\u2605Hello World\\u2605\\u2605\\u2605***\",\"inputOriginalMessage\":\"Hello World\",\"withStar\":{\"result\":\"\\u2605\\u2605\\u2605Hello World\\u2605\\u2605\\u2605\"},\"withAsterisk\":{\"result\":\"***\\u2605\\u2605\\u2605Hello World\\u2605\\u2605\\u2605***\"}}",
ここからは、前回同様にステートマシンの定義とAWS Lambda関数のログを突き合わせながら見ていきます。
最初に、入力はこれでしたね。
{ "message": "Hello World" }
最初のステートは、「★」を追加するAWS Lambda関数の呼び出しです。
"WithStar": { "Type": "Task", "Resource": "${StarFunctionArn}", "Output": "{% {'message': $states.result.result, 'inputOriginalMessage': $states.input.message, 'result': $states.result.result, 'withStar': $states.result} %}", "Next": "WithAsterisk" },
入力データのログを確認してみます。
$ awslocal logs tail /aws/lambda/star-function
message
が渡ってきています。
2025-03-21T07:44:27.635000+00:00 2025/03/21/[$LATEST]8bb44bd9f2a6dd1c304357a2f50b02fb star function input = {'message': 'Hello World'}
AWS Lambda関数は入力値の前後に「★」を3つつけるだけでした。
message = event["message"] return { "result": ("★" * 3) + message + ("★" * 3) }
つまり、この関数の戻り値はこうなります。
{ "result": "★★★Hello World★★★" }
ここで、Output
でこのステートの入力と出力を組み合わせます。
"Output": "{% {'message': $states.result.result, 'inputOriginalMessage': $states.input.message, 'result': $states.result.result, 'withStar': $states.result} %}",
$states
はAWS Step FunctionsのASLでJSONataを使った時の予約語です。
$states.input
でステートの入力を、$states.result
でステートの出力を扱えます。今回は使っていませんが、他には
$states.errorOutput
、$states.context
もあります。
Step Functions での JSONata を使用したデータの変換 / 予約変数: $states
Step Functions の Context オブジェクトからの実行データへのアクセス - AWS Step Functions
結果はこちら。
{ "message": "★★★Hello World★★★", "inputOriginalMessage": "Hello World", "result": "★★★Hello World★★★", "withStar": { "result": "★★★Hello World★★★" } }
JSONPathではAWS Lambda関数の呼び出しの後にPass
ステートとParameters
で変数操作をしていたのですが、それが
不要になりました。
そしてこれが次のステートの入力になります。
"WithAsterisk": { "Type": "Task", "Resource": "${AsteriskFunctionArn}", "Output": "{% {'result': $states.result.result, 'inputOriginalMessage': $states.input.inputOriginalMessage, 'withStar': $states.input.withStar, 'withAsterisk': $states.result} %}", "End": true }
ログを確認してみます。
$ awslocal logs tail /aws/lambda/asterisk-function
WithStar
ステートの結果がそのまま渡されていますね。
2025-03-21T07:44:28.981000+00:00 2025/03/21/[$LATEST]b49aea79c9b924026e956d45a74001fe asterisk function input = {'message': '★★★Hello World★★★', 'inputOriginalMessage': 'Hello World', 'result': '★★★Hello World★★★', 'withStar': {'result': '★★★Hello World★★★'}}
AWS Lambda関数は、「*」を3つつけるようになっていました。
message = event["message"] return { "result": ("*" * 3) + message + ("*" * 3), }
よって、AWS Lambda関数の戻り値としてはこうなります。
{ "result": "***★★★Hello World★★★***" }
ステートの出力は、Output
で決定します。
"Output": "{% {'result': $states.result.result, 'inputOriginalMessage': $states.input.inputOriginalMessage, 'withStar': $states.input.withStar, 'withAsterisk': $states.result} %}",
つまり、この結果になるわけです。
{ "result": "***★★★Hello World★★★***", "inputOriginalMessage": "Hello World", "withStar": { "result": "★★★Hello World★★★" }, "withAsterisk": { "result": "***★★★Hello World★★★***" } }
1度JSONPathでベースを組んでいるからというのもありますが、JSONPathを使っている時はResultSelector
やResultPath
、
タスクの出力結果をいろいろと使いたい場合などにだいぶ苦戦しましたが、JSONataだと入出力全体で扱うのでかなり
簡単に済ませることができました。
今回は登場しませんでしたが、(JSONPathの時はParameters
を使ってPass
ステートで変数を加工していたのがそもそも
不要になったので)Arguments
を使うとタスクの入力も柔軟に扱えます。
入出力がArguments
、Output
とシンプルに使えるようになったのでかなりわかりやすくなったと思います。
おわりに
LocalStackとAWS SAMで、AWS Lambda関数を使った入出力を扱うAWS Step FunctionsのワークフローをJSONataを
使って書き直してみました。
JSONPathの時にとてもとても苦労したので、理解度が上がったのもあるはずですがかなりあっさり書けるようになって
ちょっと嬉しいです(笑)。
今後は積極的に使ってよい気がしますね。