Amazon Web Services ブログ

変数と JSONata を使った AWS Step Functions での開発者エクスペリエンスの簡素化

本記事は、2024 年 11 月 22 日に公開された “Simplifying developer experience with variables and JSONata in AWS Ste…”を翻訳したものです。

この投稿は、Uma Ramadoss (サーバーレス担当 Principal Specialist SA) と Dhiraj Mahapatro (Amazon Bedrock 担当Principal Specialist SA) によって執筆されたものです。

AWS Step Functions において、変数と JSONata データ変換が導入されました。変数により、開発者は 1 つのステートでデータを割り当て、その後のステップで参照できるようになり、複数の中間ステートを経由してデータを受け渡す必要がなくなったため、ステートのペイロード管理が簡素になります。オープンソースのクエリおよび変換言語である JSONata により、日付と時刻の書式設定や数学的演算などの高度なデータ操作と変換できるようになりました。

この記事では、これらの新機能の強力な機能について詳しく説明します。具体的には、変数を使用したステート間のデータ共有を簡素にする方法と、高度な JSONata 式によるデータ操作の複雑さを軽減する方法について深く掘り下げていきます。

概要

お客様は、AWS LambdaAWS FargateAmazon Bedrock、HTTP API 統合など、複数のサービスを含む複雑なワークフローを構築するために Step Functions を利用します。 これらのワークフローの中で、さまざまなサービスとやり取りするためのステートを構築し、入力データを渡し、出力としてレスポンスを受け取ります。 Step Functions 自体の機能を超えた、日付、時刻、数値の操作には Lambda 関数を使用できますが、この方法では複雑になるにつれて、ペイロードの制限、データ変換の手間、さらなるステート変更などの課題が生じます。 これは、ソリューション全体のコストに影響を与えます。 この問題に対処するために、変数と JSONata を使用します。

これらの新機能を説明するために、JSONPath ブログで取り上げた保険業界の顧客オンボーディングプロセスのユースケースを考えてみましょう。 潜在顧客は、サインアップ時に名前、住所、保険への関心事項などの基本情報を提供します。 この Know-Your-Customer (KYC) プロセスでは、これらの詳細な情報を含むペイロードとともに Step Functions ワークフローが開始されます。 ワークフローでは、顧客の承認または拒否を判断し、通知します。

{
  "data": {
    "firstname": "Jane",
    "lastname": "Doe",
    "identity": {
      "email": "jdoe@example.com",
      "ssn": "123-45-6789"
    },
    "address": {
      "street": "123 Main St",
      "city": "Columbus",
      "state": "OH",
      "zip": "43219"
    },
    "interests": [ 
      {"category": "home", "type": "own", "yearBuilt": 2004, "estimatedValue": 800000},
      {"category": "auto", "type": "car", "yearBuilt": 2012, "estimatedValue": 8000},
      {"category": "boat", "type": "snowmobile", "yearBuilt": 2020, "estimatedValue": 15000},
      {"category": "auto", "type": "motorcycle", "yearBuilt": 2018, "estimatedValue": 25000},
      {"category": "auto", "type": "RV", "yearBuilt": 2015, "estimatedValue": 102000},
      {"category": "home", "type": "business", "yearBuilt": 2009, "estimatedValue": 500000}
    ] 
  }
}

従来のワークフロー図は新機能を適用する前のワークフローを示しており、新しいワークフロー図は変数と JSONata を適用して構築されたワークフローを示しています。 このワークフローは、GitHub リポジトリmain ブランチ (従来のワークフロー) と jsonata-variables ブランチ (新しいワークフロー) からアクセスできます。


図 1 : 従来のワークフロー


図 2: 新しいワークフロー

セットアップ

README の手順に従ってこのステートマシンを作成し、テストが完了したら後片付けを行ってください。

変数によるデータ共有の簡素化

変数を使用することで、後続のステートで参照される変数に、ステートの結果を宣言または代入することができます。1 つのステートで、静的データ、ステートの結果、JSONPath や JSONata 式、組み込み関数など、さまざまな値を持つ複数の変数を割り当てることができます。次の図は、ステートマシン内で変数がどのように割り当てられ、使用されるかを示しています。
図 3: 変数の割り当てとスコープ

変数のスコープ

Step Functions における変数は、プログラミング言語と同様のスコープを持ちます。内部スコープと外部スコープがあり、それぞれのレベルで変数を定義します。内部スコープの変数は map、parallel、ネストされたワークフロー内で定義され、その特定のスコープ内でのみアクセス可能です。一方、外部スコープの変数はトップレベルで設定されます。一度変数が割り当てられると、実行順序に関係なく、後続のどのステートからでもこれらの変数にアクセスできます。しかし、このブログのリリース時点では、Distributed Map は外部スコープの変数を参照できません。変数のスコープに関するユーザーガイドでは、このようなエッジケースについて詳しく説明されています。

変数の割り当てと使用
変数の値を設定するには、特別なフィールドである Assign を使用します。このブログの後半にある JSONata の部分で、{%%} の目的について説明しています。

"Assign": {
  "inputPayload": "{% $states.context.Execution.Input %}",
  "isCustomerValid": "{% $states.result.isIdentityValid and $states.result.isAddressValid %}"
}

変数を使用するには、変数名の前にドル記号 ($) を付けて記述します。

{
  "TableName": "AccountTable",
  "Item": {
    "email": {
      "S": "{% $inputPayload.data.email %}"
    },
    "firstname": {
      "S": "{% $inputPayload.data.firstname %}"
    },....
}

JSONata によるデータ操作の簡素化

JSONata は、Json データ用の軽量なクエリおよび変換言語です。JSONata は、Step Functions 内の JSONPath と比較してより多くの機能を提供します。

QueryLanguage"JSONata" に設定し、JSONata 式に {%%} タグを使用することで、ステートマシン内で JSONata を利用できます。この設定はステートマシンのトップレベルまたは各タスクレベルで適用できます。タスクレベルの JSONata を利用することで、JSONata と JSONPath の選択を細かく制御できます。この方法は、一部のステートを JSONata で簡素化し、残りのステートでは JSONPath を使い続けたい複雑なワークフローに有用です。JSONata は、JSONPath や Step Functions の組み込み関数よりも多くの関数と演算子を提供します。ステートマシンレベルで QueryLanguage 属性を JSONata に設定すると、JSONPath が無効になり、InputPathParametersResultPathResultSelectorOutputPath の使用が制限されます。代わりに JSONata では ArgumentsOutput を使用します。

シンプルなステートの最適化

新しいステートマシンで最初に気づく点の 1 つは、以下の比較で示されるように、Verification プロセスが Lambda 関数を使用していないことです。
図 4: Pass ステートに置き換えられた Lambda 関数

従来のアプローチでは、正規表現を使用してメールアドレスとソーシャルセキュリティナンバー (SSN) を検証する Lambda 関数が使用されていました。

const ssnRegex = /^\d{3}-?\d{2}-?\d{4}$/;
 const emailRegex = /^[a-zA-Z0-9._-] +@[a-zA-Z0-9.-] + \.[a-zA-Z]{2,4}$/;

 exports.lambdaHandler = async event => {
  const { ssn, email } = event ;
  const approved = ssnRegex.test(ssn) && emailRegex.test(email);

  return {
    statusCode: 200,
    body: JSON.stringify({ 
      approved,
      message: `identity validation ${approved ? 'passed' : 'failed'}` 
    })
  }
};

JSONata を使用すると、ステートマシンの Amazon States Language (ASL) を利用して正規表現を直接定義できます。 Pass ステートと JSONata の$match()を使用して、メールアドレスと SSN を検証します。

{
  "StartAt": "Check Identity",
   "States": {
    "Check Identity": {
      "Type": "Pass",
      "QueryLanguage": "JSONata",
      "End": true,
      "Output": {
        "isIdentityValid": "{% $match($states.input.data.identity.email, /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/) and $match($states.input.data.identity.ssn, /^(\d{3}-?\d{2}-?\d{4}|XXX-XX-XXXX)$/) %}"
      }
    }
   }
}

同様に、JSONata の高度な文字列関数 $length$trim$each$not を使って、Pass ステートの中で住所を検証できます。

{
  "StartAt": "Check Address",
  "States": {
    "Check Address": {
      "Type": "Pass",
      "QueryLanguage": "JSONata",
      "End": true,
      "Output": {
        "isAddressValid": "{% $not(null in $each($states.input.data.address, function($v) { $length($trim($v)) > 0 ? $v : null })) %}"
      }
    }
  }
}

JSONata を使用する場合、$states予約変数になります。

結果の集計

以前は JSONPath では、Choice ステート以外で式は使用できませんでした。しかし JSONata ではそのような制限はありません。この例では、 parallel ステートで各サブステップからの本人確認と住所確認の結果を収集しています。それらの結果を boolean 変数 isCustomerValid に集約しています。

"Verification": {
  "Type": "Parallel",
  "QueryLanguage": "JSONata",
  ...
  "Assign": {
    "inputPayload": "{% $states.context.Execution.Input %}",
    "isCustomerValid": "{% $states.result.isIdentityValid and $states.result.isAddressValid %}"
  },
  "Next": "Approve or Deny?"
}

ここで重要な点は、$states.result を介した結果へのアクセスと、{%%} 内での AND ブール演算子の使用です。これにより、この変数を使用する後の Choice ステートがシンプルになります。JSONata の Operators を使用することで、可能な限りこのような式を柔軟に記述でき、単純なデータ変換するをためのコンピュート層を削減できます。

さらに、{%%} 内の式が true または false の値を返す限り、柔軟な JSONata 演算子および式を利用することで、Choice ステートが簡単に使えるようになります。

JSONata 関数としての組み込み関数

Step Functions は、Step Functions の組み込み関数と同等の機能を提供するために、JSONata の組み込み関数が提供されています。 DynamoDB の putItem ステップでは、States.UUID() 組み込み関数と同じ機能を持つ $uuid() の使用方法を示しています。 また、日付と時刻に関する JSONata 固有の関数も利用できます。 以下のステートは、DynamoDB テーブルにアイテムを挿入する前に、 $now() を使用して現在のタイムスタンプを ISO-8601 形式の文字列として取得する例を示しています。

"Add Account": {
  "Type": "Task",
  "QueryLanguage": "JSONata",
  "Resource": "arn:aws:states:::dynamodb:putItem",
  "Arguments": {
    "TableName": "AccountTable",
    "Item": {
      "PK": {
        "S": "{% $uuid() %}"
      },
      "email": {
        "S": "{% $inputPayload.data.identity.email %}"
      },
      "name": {
        "S": "{% $inputPayload.data.firstname & ' ' & $inputPayload.data.lastname  %}"
      },
      "address": {
        "S": "{% $join($each($inputPayload.data.address, function($v) { $v }), ', ') %}"
      },
      "timestamp": {
        "S": "{% $now() %}"
      }
    }
  },
  "Next": "Interests"
}

JSONata 式が ASL を利用してステートマシンを定義する際の開発者の負担を軽減するため、 S.$内で.$ 表記が不要になったことに注目してください。Step Functions 内で利用可能な追加の JSONata 関数についても調べてみてください。

高度な JSONata

JSONata の柔軟性は、組み込み関数、高階関数のサポート、関数型プログラミング構文に由来します。JSONPath では、高度な式 "InputPath": "$..interests[?(@.category ==home) ]" を使用して、配列 interests から住宅保険関連の興味関心をフィルタリングしていました。JSONata はフィルタリング以上の機能を提供します。たとえば、住宅保険の関心事を探し、カテゴリタイプが home の totalAssetValue を参照し、name や email などの既存のフィールドを JSONata 変数として参照できます。

(
    $e := data.identity.email ;
    $n := data.firstname & ' ' & data.lastname ;
    
    data.interests[category='home']{
      'customer': $n,
      'email': $e,
      'totalAssetValue': $sum(estimatedValue),
      category: {type: yearBuilt}
    }
)

結果の JSON は次のようになります。

`{ 
"customer": "Jane Doe", 
"email": "jdoe@example.com", 
"totalAssetValue": 1400000, 
"home": { 
    "own": 2004, 
    "business": 2009 
    } 
}`

これらのステップに従うことで、すべての保険に関する関心事とその集約結果を収集し、1 つ上のレベルに進みます。カテゴリフィルターがもう存在しないことに注目してください。

(
    $e := data.identity.email ;
    $n := data.firstname & ' ' & data.lastname ;
    
    data.interests{
      'customer': $n,
      'email': $e,
      'totalAssetValue': $sum(estimatedValue),
      category: {type: yearBuilt}
    }
)

結果は次のようになります。

{
  "customer": "Jane Doe",
  "email": "jdoe@example.com",
  "totalAssetValue": 1549000,
  "home": {
    "own": 2004,
    "business": 2009
  },
  "auto": {
    "car": 2012,
    "motorcycle": 2018,
    "RV": 2015
  },
  "boat": {
    "snowmobile": 2020
  }
}

複雑な式の探索

サンプルデータと JSONata プレイグラウンドを利用して、要件に合った詳細で複雑な式を見つけてください。以下は JSONata プレイグラウンドの使用例です。

図 5: JSONata プレイグラウンド

考慮事項

変数のサイズ

1 つの変数の最大のサイズは 256KiB です。 この制限は、ステート出力を別々の変数に格納することで、Step Functions のペイロードサイズ制限を回避するのに役立ちます。 個々の変数のサイズは最大 256KiB ですが、単一の Assign フィールド内のすべての変数の合計サイズは 256KiB を超えることはできません。 この制限を回避するには Pass ステートを使用できますが、格納されたすべての変数の合計サイズは、1 回の実行につき 10MiB を超えることはできません。

変数の可視性

変数は、ステート間でデータを共有するのを簡単にする強力な仕組みです。使いやすく柔軟であるため、ResultPathOutputPath、JSONata の Output フィールドよりも変数を優先してください。ただし、Output を使う可能性のある場面が 2 つあります。1 つ目は、外側のスコープから内側のスコープの変数にアクセスできない場合です。この場合、Output フィールドを使うと、ワークフローの異なるレベル間でデータを共有できます。2 つ目は、ワークフローの最終ステートからレスポンスを送信する際に、Output フィールドを使う必要がある場合です。以下の JSONPath から JSONata への移行図に、その詳細を示しています。
図 6: JSONPath から JSONata への移行

さらに、特定のステートに割り当てられた変数は、同じステートからはアクセスできません。

"Assign Variables": {
  "Type": "Pass",
  "Next": "Reassign Variables",
  "Assign": {
    "x": 1,
    "y": 2
  }
},
"Reassign Variables": {
  "Type": "Pass",
  "Assign": {
    "x": 5,
    "y": 10,
      ## The assignment will fail unless you define x and y in a prior state.
      ## otherwise, the value of z will be 3 instead of 15.
    "z": "{% $x+$y %}"
  },
  "Next": "Pass"
}

ベストプラクティス

Step Functions の検証 API は、ワークフローのセマンティックチェックを提供し、早期の問題発見を可能にします。 ワークフローの安全な更新を確実するために、検証 API とバージョニングとエイリアスを組み合わせて、段階的にデプロイすることをお勧めします。

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

相互排他

QueryLanguage タイプの使用は相互に排他的です。変数の割り当て時に JSONPath/組み込み関数と JSONata を混在させないでください。たとえば、以下のタスクは失敗します。変数 b は JSONata を使用していますが、c は組み込み関数を使用しているためです。

"Store Inputs": {
  "Type": "Pass",
  "QueryLanguage": "JSONata"
  "Assign": {
    "inputs": {
      "a": 123,
      "b": "{% $states.input.randomInput %}",
      "c.$": "States.MathRandom($.start, $.end)"
    }
  },
  "Next": "Average"
}

JSONPath で変数を使用するにはQueryLanguageJSONPath に設定するか、タスク定義からこの属性を削除してください。

結論

変数と JSONata により、AWS Step Functions は開発者が Amazon States Language (ASL) で通常のプログラミングパラダイムに合わせた簡潔なコードでエレガントなワークフローを記述できるようになり、開発者体験が向上しました。 開発者は、余分なデータ変換のステップを省略することで、より高速に、クリーンなコードを記述できるようになりました。 これらの機能は、新規および既存のワークフローの両方で使用できるため、JSONPath から JSONata と変数への移行を柔軟に行うことができます。

変数と JSONata は、AWS Step Functions が利用可能なすべての AWS リージョンで追加料金なしでお客様にご利用いただけます。 JSONata変数 のユーザーガイド、および jsonata-variables ブランチのサンプルアプリケーションを参照してください。

サーバーレスに関する知識を深めるには、Serverless Land をご覧ください。