zenet_logo

-株式会社ゼネット技術ブログ-

Step FunctionsでCloudFormation StackSetを自動デプロイしてみた⚙️

 

はじめに

システム事業部の方です。
今回は AWS Step Functions を使って CloudFormation StackSetを自動デプロイ する仕組みを構築したので、その内容を紹介します。

AWS環境を複数アカウントで管理している場合、
「同じスタックを各アカウントへ一括展開したい」という要件は非常に多いですよね。

StackSet を使えばそれが可能ですが、

  • 「複数アカウントへ段階的にデプロイしたい」

  • 「失敗時に自動リトライや削除を行いたい」
    といった柔軟な制御をしたい場合、Step Functions が非常に便利です。

課題

課題 詳細
マルチアカウント展開 同じテンプレートを複数AWSアカウントに展開するのは手間がかかる
ステータス管理の複雑さ StackSetの状態(CREATE_IN_PROGRESSUPDATE_IN_PROGRESSなど)を追いながら制御が必要
自動化の難しさ LambdaやCLIだけでマルチアカウント処理を制御するのは例外処理が煩雑

これらの課題を解決するために、Step Functionsの状態管理 + JSONataによる条件分岐 を活用します。

全体構成🧩

今回の構成では、Step Functionsが中心となり、
自動的に各アカウントへ CloudFormation StackSet を展開します。

  1. StartExecution
    まず、別の State Machine を同期実行(startExecution.sync)します。
    この段階で、対象アカウント情報(Targets)などの入力パラメータを渡します。

  2. ListStackSets
    CloudFormation の API を呼び出し、既存の StackSet 一覧を取得します。

  3. Choice分岐(JSONata判定)
    JSONata 式で目的の StackSet 名が既存リストに含まれるかを判定します。

    • 存在すれば → 既存の StackSet に対してデプロイを進行

    • 存在しなければ → 新しく StackSet を作成

  4. CreateStackSet(必要な場合)
    CloudFormation StackSet を新規作成します。
    IAM 権限やパラメータはテンプレートに基づいて自動設定されます。

  5. Map 状態(並列実行)
    各アカウントに対して Stack Instances を並列展開します。
    それぞれのアカウントで CloudFormation スタックを作成。

  6. Wait / DescribeStackInstance
    一定間隔で進行状況を確認します。
    「CREATE_COMPLETE」や「UPDATE_COMPLETE」になるまでループ。

  7. Choice分岐で成功/失敗判定
    成功したインスタンスは完了、失敗した場合は削除タスク(deleteStackInstances)を実行。

  8. Succeed / Fail 終了状態
    全アカウントの処理が正常に完了すれば Succeed、
    途中で失敗が発生した場合は Fail 状態で停止します。

ポイント💡 

  • StackSetの存在チェックをListStackSetsで実施

  • JSONata式で条件分岐(Lambda不要

  • Map状態で複数アカウントへ並列展開

実装例

ここでは、Step Functions単体でStackSetの存在確認〜作成〜インスタンス展開まで を完結させています。
Lambdaを使わずに、JSONataとAWS SDK統合タスク(aws-sdk:cloudformation:*)だけで制御できる点がポイントです。

実装の流れ💡

  1. ListStackSets で既存のStackSet一覧を取得

  2. JSONata式で目的のStackSetが存在するかチェック

  3. 存在しなければ CreateStackSet を実行

  4. 成功後、各アカウントに対して CreateStackInstances をMapで展開

  5. 定期的に DescribeStackInstance / DescribeStacks で進行状況を監視

  6. 成功 or 再試行 or 削除をChoice分岐で制御

Step Functions デザイン

Pro Tip💡 
JSONataを使えば if / else のような構文を書かずに
「StackSetがリストに含まれるか?」の判定が可能です。

 
"StackSetExists": "{% $string($StackSetName) in $map($states.input.Summaries, function($v){$v.StackSetName}) %}" 
{
  "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"
    }
  ]
}

Step Functions 実行結果

注意点⚠️

  • 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環境のインフラ運用をより安全・効率的に進められます。

参照資料🔗

docs.aws.amazon.com

docs.aws.amazon.com