AWS Step FunctionsのParallelステートにおける変数スコープとデータ共有方法を解説
はじめに
AWS Step Functionsの変数は、ワークフロー内でデータを効率的に管理するための重要な機能です。
Parallelステートで変数を利用する際には、各ブランチ間やメインフローとの間でどのようにデータが共有されるのかを理解することが重要です。
実際にParallelステートで変数を初めて利用した際、変数のスコープについて考慮する必要があったため、このブログではParallelステート内外での変数のスコープとデータ共有方法について解説します。
ここで言う「データ共有」とは、Step Functionsのワークフロー内で変数を使ったデータの受け渡しを行うことを指します。
変数のスコープの基本
Step Functionsにおける変数のスコープは、AWS公式ブログに基づくと以下のように定義されます。
-
外部スコープ: ステートマシンの
States
フィールド内のすべてのステートを含む領域(ParallelまたはMapステート内のステートは除く)
このスコープで定義された変数はメインワークフロー全体から参照可能 -
内部スコープ: Parallelステート内の各ブランチやMapステート内の各イテレーションが持つ領域
このスコープで定義された変数は、そのブランチまたはイテレーション内でのみ参照可能
Parallelステートにおける変数の参照に関する重要なポイントは以下の通りです。
- Parallelステート内の各ブランチは外部スコープの変数(メインワークフロー内で定義された変数)を参照できます。
- Mapステートは、外部スコープの変数を参照できません。
- あるブランチで定義された変数は、他のブランチからは参照できません。例えば、「ブランチA」で定義された変数は「ブランチB」からは見えず、逆も同様です。
- ブランチ内で定義された変数は、そのブランチの実行が終了すると、後続のステートからは参照できなくなります。
Step Functions における変数は、プログラミング言語と同様のスコープを持ちます。内部スコープと外部スコープがあり、それぞれのレベルで変数を定義します。内部スコープの変数は map、parallel、ネストされたワークフロー内で定義され、その特定のスコープ内でのみアクセス可能です。一方、外部スコープの変数はトップレベルで設定されます。一度変数が割り当てられると、実行順序に関係なく、後続のどのステートからでもこれらの変数にアクセスできます。しかし、このブログのリリース時点では、Distributed Map は外部スコープの変数を参照できません。
https://aws.amazon.com/jp/blogs/news/simplifying-developer-experience-with-variables-and-jsonata-in-aws-step-functions/
Parallelステートが完了した後の後続ステートでブランチの結果を利用するには、以下の手順が必要です。
- 各ブランチの最終ステートで、必要なデータを出力(
Output
)として設定する - Parallelステートの変数(
Assign
)を使用して、その出力を外部スコープの変数として定義する
この方法により、ブランチ内で生成されたデータをワークフロー全体で活用できるようになります。
サンプルステートマシンの解説
以下は、Parallelステートにおける変数のスコープとデータ共有を検証するためのサンプルステートマシンです。
{
"Comment": "ステートマシン: Parallelステートにおける変数のスコープとデータ共有",
"StartAt": "InitializeVariables",
"QueryLanguage": "JSONata",
"States": {
"InitializeVariables": {
"Type": "Pass",
"Assign": {
"globalCounter": 1,
"globalMessage": "This is a global variable in main workflow"
},
"Next": "BeforeParallel"
},
"BeforeParallel": {
"Type": "Pass",
"Assign": {
"sharedData": "Data set before parallel execution"
},
"Next": "ParallelProcessing"
},
"ParallelProcessing": {
"Type": "Parallel",
"Branches": [
{
"StartAt": "BranchA_Step1",
"States": {
"BranchA_Step1": {
"Type": "Pass",
"Assign": {
"branchAData": "Data from Branch A",
"branchACounter": 100,
"branchAAccessedGlobalData": "{% $globalMessage %}"
},
"Next": "BranchA_Step2"
},
"BranchA_Step2": {
"Type": "Pass",
"Output": {
"branchId": "A",
"counter": "{% $branchACounter %}",
"message": "{% $branchAData & ' - processed in Step 2' %}",
"step1Data": "{% $branchAData %}",
"step2Data": "Final data from Branch A Step 2",
"accessedGlobalData": "{% $globalMessage %}"
},
"End": true
}
}
},
{
"StartAt": "BranchB_Step1",
"States": {
"BranchB_Step1": {
"Type": "Pass",
"Assign": {
"branchBData": "Data from Branch B",
"branchBCounter": 200,
"branchBAccessedSharedData": "{% $sharedData %}"
},
"Next": "BranchB_Step2"
},
"BranchB_Step2": {
"Type": "Pass",
"Output": {
"branchId": "B",
"counter": "{% $branchBCounter %}",
"message": "{% $branchBData & ' - processed in Step 2' %}",
"step1Data": "{% $branchBData %}",
"step2Data": "Final data from Branch B Step 2",
"accessedSharedData": "{% $sharedData %}"
},
"End": true
}
}
}
],
"Assign": {
"parallelData": "Data defined at the Parallel state level",
"updatedGlobalCounter": "{% $globalCounter + 10 %}",
"branchAResult": "{% $states.result[0] %}",
"branchBResult": "{% $states.result[1] %}",
"totalCounters": "{% $states.result[0].counter + $states.result[1].counter %}",
"branchMessages": {
"fromBranchA": "{% $states.result[0].message %}",
"fromBranchB": "{% $states.result[1].message %}"
}
},
"Next": "AfterParallel"
},
"AfterParallel": {
"Type": "Pass",
"Assign": {
"postParallelData": "Data set after parallel execution",
"calculatedTotal": "{% $totalCounters %}",
"globalSummary": {
"originalCounter": "{% $globalCounter %}",
"updatedCounter": "{% $updatedGlobalCounter %}",
"message": "{% $globalMessage %}"
},
"parallelSummary": {
"parallelVariable": "{% $parallelData %}"
},
"branchResults": {
"fromBranchA": "{% $branchAResult %}",
"fromBranchB": "{% $branchBResult %}"
},
"messagesSummary": "{% $branchMessages %}"
},
"End": true
}
}
}
このステートマシンは、次の4つのステップで構成されています。
-
InitializeVariables: 外部スコープの変数(
globalCounter
とglobalMessage
)を初期化します。これらの変数はワークフロー全体から参照可能です。 -
BeforeParallel: Parallel実行前に共有データ(
sharedData
)を設定します。この変数も外部スコープに属し、後続のすべてのステートから参照できます。 -
ParallelProcessing: 2つのブランチ(AとB)を並行実行します。各ブランチは独自の内部スコープを持ち、それぞれ独自の変数を定義しています。また、外部スコープの変数も参照しています。ブランチの実行完了後、Parallelステートの
Assign
で各ブランチの結果を集約し、新たな外部スコープの変数として定義します。 -
AfterParallel: Parallel実行後に、すべての結果を整理して最終的な変数セットを作成します。ここでは、外部スコープの変数とParallelステートで定義された変数を組み合わせて使用しています。
次のセクションでは、このステートマシンを使って変数のやりとりパターンを詳しく見ていきます。
変数のやりとりのパターン
1. 各ブランチから外部スコープの変数を参照
Parallelステート内の各ブランチは、外部スコープで定義された変数(globalCounter
、globalMessage
)やParallel実行前に定義された変数(sharedData
)を直接参照できます。これは特別な構文や追加の設定なしに行えます。
例えば、ブランチAでは、外部スコープのglobalMessage
変数を以下のように参照できます。
"BranchA_Step1": {
"Type": "Pass",
"Assign": {
"branchAAccessedGlobalData": "{% $globalMessage %}"
},
// ...
}
同様に、ブランチBでは、Parallel実行前に定義されたsharedData
変数を参照しています。
"BranchB_Step1": {
"Type": "Pass",
"Assign": {
"branchBAccessedSharedData": "{% $sharedData %}"
},
// ...
}
この機能により、各ブランチは共通のデータを参照しながら、それぞれ独立した処理を実行できます。
2. Parallelステートでの変数定義とタイミング
Parallelステートで変数を定義できますが、これらの変数はすべてのブランチの実行が完了した後に定義されます。
そのため、ブランチの実行中にはこれらの変数を参照することはできません。
以下は、Parallelステートで変数を定義する例です。
"ParallelProcessing": {
"Type": "Parallel",
"Assign": {
"parallelData": "Data defined at the Parallel state level",
"updatedGlobalCounter": "{% $globalCounter + 10 %}"
},
// ...
}
Parallelステートで定義された変数は外部スコープに属し、Parallelステート完了後の後続ステートから参照可能になります。
これにより、並行処理の結果を集約して後続の処理で利用することができます。
3. ブランチ内での変数の利用と制限
各ブランチ内で定義された変数は、そのブランチ内の後続のステートでのみ使用できます。これには以下の制限があります。
- ブランチ外の後続のステート(Parallelステート完了後のステート)からは参照できません
- 他のブランチで定義された変数は参照できません(ブランチ間でのデータ共有はできない)
以下は、ブランチA内で変数を定義する例です。
"BranchA_Step1": {
"Type": "Pass",
"Assign": {
"branchAData": "Data from Branch A"
},
"Next": "BranchA_Step2"
}
この例では、branchAData
変数はブランチA内の後続ステート(BranchA_Step2
)からは参照できますが、ブランチBや、Parallelステート完了後のステートからは直接参照できません。
4. ブランチの結果を後続ステートで利用する方法
各ブランチ内で定義された変数を、ブランチ外の後続のステートで使用するには、以下の手順が必要です。
-
ブランチからのデータ出力: 各ブランチの最終ステート(BranchA_Step2とBranchB_Step2)の出力を使用して、データをParallelステートに返します。
- 各ブランチの最終ステートで、ブランチの結果を変数として定義しても、後続のステートに直接渡すことはできません。
-
Parallelステートでのデータ集約: Parallelステートの変数を使用して、ブランチの結果を外部スコープの変数として定義し、後続のステートで利用できるようにします。
4-1 ブランチからのデータ出力
各ブランチで生成されたデータをParallelステート外で利用するには、各ブランチの最終ステートで出力を使用して明示的にデータを出力する必要があります。
以下は、ブランチAの最終ステートでデータを出力する例です。
"BranchA_Step2": {
"Type": "Pass",
"Output": {
"branchId": "A",
"counter": "{% $branchACounter %}",
"message": "{% $branchAData & ' - processed in Step 2' %}",
"step2Data": "Final data from Branch A Step 2"
},
"End": true
}
ブランチが終了すると、そのブランチ内で定義された変数(branchACounter
やbranchAData
など)はスコープ外になります。そのため、必要なデータは必ず出力に含めておく必要があります。
4-2 Parallelステートでのデータ集約と変数定義
Parallelステートでは、$states.result
配列を通じて各ブランチの出力結果にアクセスできます。
変数を使用して、これらの結果を外部スコープの変数として定義します。
以下は、Parallelステートで各ブランチの結果を変数として定義する例です。
"ParallelProcessing": {
"Assign": {
"branchAResult": "{% $states.result[0] %}",
"branchBResult": "{% $states.result[1] %}",
"totalCounters": "{% $states.result[0].counter + $states.result[1].counter %}",
"branchMessages": {
"fromBranchA": "{% $states.result[0].message %}",
"fromBranchB": "{% $states.result[1].message %}"
}
},
"Next": "AfterParallel"
}
定義された変数は、後続のステートで以下のように利用できます。
"AfterParallel": {
"Type": "Pass",
"Assign": {
"postParallelData": "Data set after parallel execution",
"calculatedTotal": "{% $totalCounters %}",
"branchResults": {
"fromBranchA": "{% $branchAResult %}",
"fromBranchB": "{% $branchBResult %}"
}
},
"End": true
}
このプロセスを理解することで、複数のブランチで並行処理された結果を効果的に集約し、後続の処理で活用することができます。
変数と出力の効果的な使い分け
Step Functionsのワークフローを設計する際、変数と出力を適切に使い分けることが重要です。
Parallelステートを含むワークフローでは、以下のような使い分けが効果的です。
-
ブランチ内の中間ステートとメインワークフロー
- 変数(Assign)を使用: 変数はスコープ内の後続ステートから容易に参照できるため、ブランチ内やメインフロー内でのデータ共有に適しています。
-
ブランチの最終ステート
- 出力(Output)を使用: ブランチが終了すると内部変数はスコープ外になるため、Parallelステートにデータを渡すには出力として明示的に設定する必要があります。
この使い分けにより、ブランチ内のデータフローとブランチからメインワークフローへのデータ受け渡しが明確に区別され、ステートマシンの動作が予測しやすくなります。
Choiceをブランチの最終ステートに設定できない制約
データ共有方法とはすこしズレますが、Parallelステートのブランチの最終ステートには、Choiceステートタイプを設定することができません。
Choiceステートを使用する場合は、その後に成功のステートタイプを追加し、そのステートを最終ステートとして設定しましょう。
この最終ステートの出力に、後続のステートで変数として利用したいデータを記載します。
これにより、ブランチの処理結果を適切にParallelステートに返すことができます。
まとめ
Parallelステートにおける変数のスコープとデータ共有について、重要なポイントは以下の通りです。
-
変数のスコープの特性
- 外部スコープの変数:ワークフロー全体から参照可能
- 内部スコープの変数:そのブランチ内でのみ参照可能
-
データ共有のパターン
- メインワークフロー→ブランチ:外部スコープの変数を内部から直接参照
- ブランチ→メインワークフロー:ブランチの最終ステートの
Output
を使用し、ParallelステートのAssign
で外部変数として定義
-
効果的な使い分け
- メインワークフローやブランチ内の中間ステート:変数(
Assign
)を使用 - ブランチの最終ステート:出力(
Output
)を使用
- メインワークフローやブランチ内の中間ステート:変数(
これらの概念を理解し適切に活用することで、複雑なワークフローでも効率的にデータを管理できます。
参考