
はじめに
システム事業部の方です。
今回は AWS Step Functions を使って CloudFormation StackSetを自動デプロイ する仕組みを構築したので、その内容を紹介します。
AWS環境を複数アカウントで管理している場合、
「同じスタックを各アカウントへ一括展開したい」という要件は非常に多いですよね。
StackSet を使えばそれが可能ですが、
-
「複数アカウントへ段階的にデプロイしたい」
-
「失敗時に自動リトライや削除を行いたい」
といった柔軟な制御をしたい場合、Step Functions が非常に便利です。
課題
| 課題 | 詳細 |
|---|---|
| マルチアカウント展開 | 同じテンプレートを複数AWSアカウントに展開するのは手間がかかる |
| ステータス管理の複雑さ | StackSetの状態(CREATE_IN_PROGRESSやUPDATE_IN_PROGRESSなど)を追いながら制御が必要 |
| 自動化の難しさ | LambdaやCLIだけでマルチアカウント処理を制御するのは例外処理が煩雑 |
これらの課題を解決するために、Step Functionsの状態管理 + JSONataによる条件分岐 を活用します。
全体構成🧩
今回の構成では、Step Functionsが中心となり、
自動的に各アカウントへ CloudFormation StackSet を展開します。
-
StartExecution
まず、別の State Machine を同期実行(startExecution.sync)します。
この段階で、対象アカウント情報(Targets)などの入力パラメータを渡します。 -
ListStackSets
CloudFormation の API を呼び出し、既存の StackSet 一覧を取得します。 -
Choice分岐(JSONata判定)
JSONata 式で目的の StackSet 名が既存リストに含まれるかを判定します。-
存在すれば → 既存の StackSet に対してデプロイを進行
-
存在しなければ → 新しく StackSet を作成
-
-
CreateStackSet(必要な場合)
CloudFormation StackSet を新規作成します。
IAM 権限やパラメータはテンプレートに基づいて自動設定されます。 -
Map 状態(並列実行)
各アカウントに対して Stack Instances を並列展開します。
それぞれのアカウントで CloudFormation スタックを作成。 -
Wait / DescribeStackInstance
一定間隔で進行状況を確認します。
「CREATE_COMPLETE」や「UPDATE_COMPLETE」になるまでループ。 -
Choice分岐で成功/失敗判定
成功したインスタンスは完了、失敗した場合は削除タスク(deleteStackInstances)を実行。 -
Succeed / Fail 終了状態
全アカウントの処理が正常に完了すれば Succeed、
途中で失敗が発生した場合は Fail 状態で停止します。
ポイント💡
-
StackSetの存在チェックを
ListStackSetsで実施 -
JSONata式で条件分岐(Lambda不要)
-
Map状態で複数アカウントへ並列展開
実装例
ここでは、Step Functions単体でStackSetの存在確認〜作成〜インスタンス展開まで を完結させています。
Lambdaを使わずに、JSONataとAWS SDK統合タスク(aws-sdk:cloudformation:*)だけで制御できる点がポイントです。
実装の流れ💡
-
ListStackSets で既存のStackSet一覧を取得
-
JSONata式で目的のStackSetが存在するかチェック
-
存在しなければ CreateStackSet を実行
-
成功後、各アカウントに対して CreateStackInstances をMapで展開
-
定期的に DescribeStackInstance / DescribeStacks で進行状況を監視
-
成功 or 再試行 or 削除をChoice分岐で制御

Pro Tip💡
JSONataを使えば if / else のような構文を書かずに
「StackSetがリストに含まれるか?」の判定が可能です。
{
"Comment": "CFn to deploy",
"StartAt": "Step Functions StartExecution",
"States": {
"Step Functions StartExecution": {
"Type": "Task",
"Resource": "arn:aws:states:::states:startExecution.sync:2",
"Arguments": {
"StateMachineArn": "arn:aws:states:ap-northeast-1:123456789012:stateMachine:hou-update-s3-bucket-policy",
"Input": {
"Targets": "{% $states.input.Targets %}",
"glue": false,
"AWS_STEP_FUNCTIONS_STARTED_BY_EXECUTION_ID": "{% $states.context.Execution.Id %}"
}
},
"Next": "Pass",
"Output": {
"Targets": "{% $states.input.Targets %}"
},
"Assign": {
"Targets": "{% $states.input.Targets %}"
}
},
"Pass": {
"Type": "Pass",
"Next": "ListStackSets",
"Assign": {
"Targets": "{% $states.input.Targets %}",
"StackSetName": "hou-api-stackset"
}
},
"ListStackSets": {
"Type": "Task",
"Arguments": {},
"Resource": "arn:aws:states:::aws-sdk:cloudformation:listStackSets",
"Next": "Pass (1)"
},
"Pass (1)": {
"Type": "Pass",
"Next": "Choice",
"Output": {
"StackSetExists": "{% $string($StackSetName) in $map($states.input.Summaries, function($v){$v.StackSetName}) %}",
"StackSetName": "{% $StackSetName %}"
}
},
"Choice": {
"Type": "Choice",
"Choices": [
{
"Next": "Map",
"Condition": "{% ($states.input.StackSetExists) = (true) %}"
}
],
"Default": "CreateStackSet"
},
"CreateStackSet": {
"Type": "Task",
"Arguments": {
"StackSetName": "{% $StackSetName %}",
"TemplateURL": "https://test-hou.s3.ap-northeast-1.amazonaws.com/tmp/moc_api.yaml",
"Capabilities": [
"CAPABILITY_NAMED_IAM"
],
"Parameters": [
{
"ParameterKey": "AgencyName",
"ParameterValue": "default-agency"
},
{
"ParameterKey": "DashboardURLLambdaCodeS3Key",
"ParameterValue": "tmp/generate-url-for-quicksight-dashboard.zip"
},
{
"ParameterKey": "LambdaCodeS3Bucket",
"ParameterValue": "test-hou"
}
]
},
"Resource": "arn:aws:states:::aws-sdk:cloudformation:createStackSet",
"Next": "DescribeStackSet"
},
"DescribeStackSet": {
"Type": "Task",
"Arguments": {
"StackSetName": "{% $StackSetName %}"
},
"Resource": "arn:aws:states:::aws-sdk:cloudformation:describeStackSet",
"Next": "Choice (1)"
},
"Choice (1)": {
"Type": "Choice",
"Choices": [
{
"Next": "Map",
"Condition": "{% ($states.input.StackSet[0].Status) = (\"ACTIVE\") %}"
}
],
"Default": "Fail (1)"
},
"Fail (1)": {
"Type": "Fail"
},
"Map": {
"Type": "Map",
"ItemProcessor": {
"ProcessorConfig": {
"Mode": "INLINE"
},
"StartAt": "CreateStackInstances",
"States": {
"CreateStackInstances": {
"Type": "Task",
"Arguments": {
"Regions": [
"ap-northeast-1"
],
"StackSetName": "{% $StackSetName %}",
"Accounts": [
"{% $Targets.AccountId %}"
],
"ParameterOverrides": [
{
"ParameterKey": "AgencyName",
"ParameterValue": "{% $Targets.AgencyName %}"
},
{
"ParameterKey": "DashboardURLLambdaCodeS3Key",
"ParameterValue": "tmp/generate-url-for-quicksight-dashboard.zip"
},
{
"ParameterKey": "LambdaCodeS3Bucket",
"ParameterValue": "test-hou"
}
]
},
"Resource": "arn:aws:states:::aws-sdk:cloudformation:createStackInstances",
"Next": "Wait"
},
"Wait": {
"Type": "Wait",
"Seconds": 30,
"Next": "DescribeStackInstance"
},
"DescribeStackInstance": {
"Type": "Task",
"Arguments": {
"StackInstanceAccount": "{% $Targets.AccountId %}",
"StackInstanceRegion": "ap-northeast-1",
"StackSetName": "{% $StackSetName %}"
},
"Resource": "arn:aws:states:::aws-sdk:cloudformation:describeStackInstance",
"Next": "Choice (2)"
},
"Choice (2)": {
"Type": "Choice",
"Choices": [
{
"Next": "DescribeStacks",
"Condition": "{% (($states.input.StackInstance.StackInstanceStatus.DetailedStatus) = (\"SUCCEEDED\") and ($states.input.StackInstance.Status) = (\"CURRENT\")) %}"
},
{
"Next": "Wait (2)",
"Condition": "{% (($states.input.StackInstance.StackInstanceStatus.DetailedStatus) = (\"RUNNING\") and ($states.input.StackInstance.Status) = (\"OUTDATED\")) %}"
}
],
"Default": "DeleteStackInstances"
},
"DescribeStacks": {
"Type": "Task",
"Arguments": {
"StackName": "{% $states.input.StackInstance.StackId %}"
},
"Resource": "arn:aws:states:::aws-sdk:cloudformation:describeStacks",
"Next": "Choice (3)"
},
"Choice (3)": {
"Type": "Choice",
"Choices": [
{
"Next": "成功 (1)",
"Condition": "{% (($states.input.Stacks.StackStatus) = (\"CREATE_COMPLETE\") or ($states.input.Stacks.StackStatus) = (\"UPDATE_COMPLETE\")) %}"
},
{
"Next": "Wait (1)",
"Condition": "{% ($states.input.Stacks[0].StackStatus) = (\"CREATE_IN_PROGRESS\") %}"
}
],
"Default": "Fail"
},
"Wait (1)": {
"Type": "Wait",
"Seconds": 10,
"Next": "DescribeStacks"
},
"DeleteStackInstances": {
"Type": "Task",
"Arguments": {
"Regions": [
"ap-northeast-1"
],
"RetainStacks": "false",
"StackSetName": "{% $StackSetName %}",
"Accounts": [
"{% $Targets.AccountId %}"
]
},
"Resource": "arn:aws:states:::aws-sdk:cloudformation:deleteStackInstances",
"Next": "Fail"
},
"Fail": {
"Type": "Fail"
},
"成功 (1)": {
"Type": "Succeed"
},
"Wait (2)": {
"Type": "Wait",
"Seconds": 10,
"Next": "DescribeStackInstance"
}
}
},
"Items": "{% $Targets %}",
"End": true
}
},
"QueryLanguage": "JSONata"
}
実行パラメータ
{
"StackSetName": "My-api-stackset",
"Targets": [
{
"AccountId": "123456789012",
"AgencyName": "ExampleAgency"
}
]
}

注意点⚠️
-
StackSetが存在しない場合
→ 先にCreateStackSetタスクを実行する必要があります。 -
Wait / Describeの間隔
→ Stack作成には時間がかかる場合があるため、適切なSeconds値を設定しましょう。 -
Lambda最小化設計
→ JSONataで多くのロジックは実装可能ですが、外部API連携など複雑な処理はLambdaで補助が現実的。 -
マルチアカウント権限
→ 実行ロールに対象アカウントのStackSet操作権限(cloudformation:*Stack*)が必要です。
まとめ
Step Functions を使えば、CloudFormation StackSet のマルチアカウント・マルチリージョン展開を自動化でき、手作業のミスや工数を大幅に削減できます。
実装のポイント✅
- Pass / Choice / Map 状態で存在確認や分岐を簡単に表現
- JSONata で既存 StackSet 判定が可能
- 待機・確認フローも標準タスクで実装
これにより、CI/CD パイプラインに組み込みやすく、AWS環境のインフラ運用をより安全・効率的に進められます。
参照資料🔗
