AWS Step Functionsを使用したAWSリソースのデプロイ

本記事は 春のスキルアップ応援フェア2026 4/26付の記事です
こんにちは、SCSKでAWSの内製化支援『テクニカルエスコートサービス』を担当している貝塚です。
以前、以下の記事を書きました。
Network Firewall ProxyをNetwork Firewall環境に導入する
Network Firewall ProxyをTransit Gateway + Network FirewallによるInspection環境に導入するときのアーキテクチャや留意点を検討しました。
この記事で構築した検証環境は、Network Firewall Proxyがプレビュー中でCloudFormationリソースが提供されていなかったため、以下のような手順でデプロイしていました。
 
  1. 2つのCloudFormationでVPC/Transit Gateway/Network Firewall/AWS Private CA等の基盤をデプロイ
  2. シェルスクリプトでAWS CLIを使用してNetwork Firewall Proxyリソースを作成
  3. 別のCloudFormationスタックでNetwork Firewall Proxy Endpointをデプロイ
上記記事で使用したCloudFormationテンプレート等を掲載しようと考えていたのですが、自分用に作成したデプロイ手順書を見ると意外と手順が多いのです。読者の方が実際に試すならもっと楽にデプロイできるようにしたいと考え、Step Functionsを使って全体をまとめようと思い立ちました。
 
ただ、実は私、何年もAWSに関わる仕事をしていて、まだ一度もStep Functionsに触れたことがありません。そこで、いきなり前述の記事のデプロイをStep Functionsに乗せるのではなく、まずは「CloudFormation → シェルスクリプト → CloudFormation」という順番でデプロイし、それぞれの間でパラメータ連携する必要のある、できる限り簡単な構成をStep Functionsで作ってみることにしました。
 

Step Functionsとは — どういうときにはまるのか

Step Functionsは、AWSの各種サービスを順番に呼び出すワークフローを定義・実行するサービスです。各ステップの成功/失敗に応じた分岐やリトライ、エラーハンドリングを、コードではなくJSON(Amazon States Language: ASL)で宣言的に記述できます。
 
もちろん、複数のAWSサービスを順番に呼び出すだけならシェルスクリプトでも実現できます。では、シェルスクリプトではなくStep Functionsを選ぶ理由は何でしょうか。AWS公式のFAQでは、Step Functionsのユースケースとして「DevOps and IT automation」が挙げられており、インフラデプロイの自動化は想定された用途です(*1)。その中でも、以下の条件が重なるケースでシェルスクリプトに対する優位性が出てきます。
 
  • CloudFormationだけでは完結しないデプロイ。例えばCloudFormation未対応のリソースをAPI/CLIで作成するステップを含むもの
  • ステップ間でデータの受け渡しが必要なもの(前のCloudFormationスタックのOutputsを次のステップに渡す等)
  • 長時間かかるステップを含むもの(CloudFormationスタック作成やリソースの作成待ちなど、数十分を要する場合)

上記それぞれを個別に考えると、CloudFormationのスタックをデプロイするAWS CLIコマンドと個別のAWSリソースをデプロイするAWS CLIコマンドを羅列すればよくない?とか、シェル変数に入れて受け渡すだけでしょう?とか、待っている数十分の間にシェルスクリプトの実行環境が障害起こす可能性がどれだけあるというの?となりますが、こうした細かい考慮事項を自分で管理しようと考えると意外に面倒なものです。Step Functionsを使えば各ステップの実行状態がコンソールで視覚的に確認できるため、どこで何が起きたかが一目瞭然ですし、問題が起きた場合のエラーハンドリングも簡単に実装できます。

今回のNetwork Firewall Proxyのデプロイは、まさにこれらの条件に該当するケースでした。CloudFormationだけでは完結せず、途中でAWS CLIによるAPI呼び出しが必要で、しかもその前後のCloudFormationスタック間でパラメータの受け渡しがある。加えて、読者の方に実際に試してもらうことを考えると、シェルスクリプトを手元で実行してもらうよりも、Step Functionsをデプロイして実行ボタンを押すだけで全工程が自動実行される方が親切です。
(*1) Step FunctionsからCloudFormationスタックを作成するパターンはAWS公式ブログでも紹介されており、CloudFormation StackSetの複数アカウントデプロイをStep Functionsでオーケストレーションする事例が、コミュニティでは、Step FunctionsのワークフローからCloudFormation CreateStackを直接呼び出してスタックを作成する手順が紹介されています。「CloudFormationだけでは完結しない処理をStep Functionsでつなぐ」というのは、確立されたパターンと言えそうです。

シェルスクリプトの実行方法

Step Functionsからシェルスクリプトを実行する方法はいくつかあります。ECS Fargateのコンテナ内で実行する方法や、Lambda関数内でsubprocessを使う方法も考えられますが、今回はCodeBuildを採用しました。理由は、CodeBuildのマネージドイメージにはAWS CLIがプリインストールされており、既存のシェルスクリプトの中身をbuildspec.ymlにほぼそのまま記述できること、そしてStep Functionsとの最適化統合(.syncパターン)によりビルド完了まで自動待機してくれることです。ECS Fargateも.sync統合に対応していますが、Dockerイメージの作成やECSクラスタの事前準備が必要になります。Lambdaは15分のタイムアウト制限があり、AWS CLIもランタイムに含まれていないため、今回の用途には不向きでした。ただし、buildspec.ymlはYAML形式なので、シェルスクリプトをそのまま実行ファイルとして使えるわけではない点は留意が必要です。以下のようにYAMLの配列要素としてシェルスクリプトの各行を書く形になります。
phases:
  build:
    commands:
      - echo "=== Getting VPC Stack Outputs ==="
      - echo "VPC Stack Name ${VPC_STACK_NAME}"
      - echo "S3 Bucket ${S3_BUCKET}"
      - SUBNET_ID=$(aws cloudformation describe-stacks --stack-name ${VPC_STACK_NAME} --query "Stacks[0].Outputs[?OutputKey=='SubnetId'].OutputValue" --output text --no-cli-pager)
      - echo "Subnet ID ${SUBNET_ID}"
      - SECURITY_GROUP_ID=$(aws cloudformation describe-stacks --stack-name ${VPC_STACK_NAME} --query "Stacks[0].Outputs[?OutputKey=='SecurityGroupId'].OutputValue" --output text --no-cli-pager)
      - echo "Security Group ID ${SECURITY_GROUP_ID}"
      - echo "=== Creating EC2 Instance ==="
      - INSTANCE_ID=$(aws ec2 run-instances --image-id resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 --instance-type t3.micro --subnet-id ${SUBNET_ID} --security-group-ids ${SECURITY_GROUP_ID} --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=sfn-poc-test},{Key=Cost,Value=XXX}]' --query 'Instances[0].InstanceId' --output text --no-cli-pager)
      - echo "Instance ID ${INSTANCE_ID}"
 

今回作ったもの

本番のデプロイ構造(CloudFormation → シェルスクリプト → CloudFormation)を簡易に再現するため、以下の3ステップの依存チェーンを組みました。
Step 1でVPC基盤を作り、そのスタック名をStep 2のCodeBuildに環境変数として渡します。CodeBuildはスタック名をもとにCloudFormation OutputsからSubnetIdやSecurityGroupIdを取得してEC2インスタンスを作成し、作成されたInstanceIdをJSONファイルとしてS3に書き出します。CodeBuildの実行結果を直接Step Functionsに返す手段がないため、S3を中継しています。Step 3でStep FunctionsがそのJSONファイルをS3から読み取ってInstanceIdを取得し、Step 4のCloudFormationスタックにパラメータとして渡してCloudWatch Alarmを作る、という流れです。
 
1. CloudFormation SDK統合 + ポーリングループが正しく動作するか
2. CodeBuild .sync 統合でビルド完了まで自動待機するか
3. フェーズ間のデータ受け渡し(スタック名 → CodeBuild環境変数 → S3 result.json → CloudFormationパラメータ)が機能するか
ステートマシン全体図は以下の通りとなります。

Step Functionsの実装パターン解説

CloudFormation SDK統合 + ポーリングループ

Step FunctionsにはCloudFormation用の最適化統合(.sync パターン、つまり完了まで自動待機してくれる統合)が存在しません。そのため、CloudFormation APIはAWS SDK統合で呼び出し、スタック完了待ちは自前のポーリングループで実装する必要があります。

具体的には、CreateStack → Wait → DescribeStacks → Choice(完了判定)のループを組みます。ASLの該当部分を抜粋します(一部、読みやすさを優先して修正・省略しています)。

"CreateVpcStack": {
  "Type": "Task",
  "Resource": "arn:aws:states:::aws-sdk:cloudformation:createStack",
  "Parameters": {
    "StackName.$": "$.vpcStackName",
    "TemplateURL.$": "$.vpcTemplateUrl",
    "RoleARN": "arn:aws:iam::...:role/CloudFormation-role",
    "Tags": [{"Key": "Cost", "Value": "XXX"}]
  },
  "ResultPath": "$.createVpcResult",
  "Next": "WaitForVpcStack"
},
"WaitForVpcStack": {
  "Type": "Wait",
  "Seconds": 15,
  "Next": "CheckVpcStack"
},
"CheckVpcStack": {
  "Type": "Task",
  "Resource": "arn:aws:states:::aws-sdk:cloudformation:describeStacks",
  "Parameters": {
    "StackName.$": "$.vpcStackName"
  },
  "ResultPath": "$.describeVpcResult",
  "Next": "IsVpcStackComplete"
},
"IsVpcStackComplete": {
  "Type": "Choice",
  "Choices": [
    {
      "Variable": "$.describeVpcResult.Stacks[0].StackStatus",
      "StringEquals": "CREATE_COMPLETE",
      "Next": "ExtractVpcOutputs"
    },
    {
      "Variable": "$.describeVpcResult.Stacks[0].StackStatus",
      "StringMatches": "*FAILED*",
      "Next": "DeploymentFailed"
    },
    {
      "Variable": "$.describeVpcResult.Stacks[0].StackStatus",
      "StringMatches": "*ROLLBACK*",
      "Next": "DeploymentFailed"
    }
  ],
  "Default": "WaitForVpcStack"
}

ポイントは以下の通りです。

  • CloudFormationテンプレートはS3にアップロードして TemplateURL で参照する必要がある(SDK統合ではローカルファイル参照不可)
  • DescribeStacks の結果を ResultPath で保持し、Choiceステートで StackStatus を判定
  • CREATE_COMPLETE でもなく FAILED/ROLLBACK でもない場合(CREATE_IN_PROGRESS 等)は Default でWaitに戻る
  • ポーリング間隔はスタックの規模に応じて調整(今回は15秒)

CodeBuild最適化統合(.sync)

CodeBuildにはStep Functionsとの最適化統合があり、startBuild.sync と書くだけでビルド完了まで自動待機してくれます。CloudFormationのポーリングループと比べると非常にシンプルです(一部、読みやすさを優先して修正・省略しています)。

"RunCreateEC2Build": {
  "Type": "Task",
  "Resource": "arn:aws:states:::codebuild:startBuild.sync",
  "Parameters": {
    "ProjectName": "sfn-poc-orchestrator-create-ec2",
    "EnvironmentVariablesOverride": [
      {
        "Name": "VPC_STACK_NAME",
        "Value.$": "$.vpcStackName",
        "Type": "PLAINTEXT"
      },
      {
        "Name": "S3_BUCKET",
        "Value.$": "$.s3BucketName",
        "Type": "PLAINTEXT"
      }
    ]
  },
  "ResultPath": "$.buildResult",
  "Next": "ReadEC2Result"
}

EnvironmentVariablesOverride で、前のステップで取得した値をCodeBuildの環境変数として渡しています。CodeBuild内のbuildspec.ymlでは、この環境変数を使ってAWS CLIコマンドを実行します。

CodeBuildの出力値(今回はEC2のInstanceId)を後続ステートに渡すには、CodeBuild内でS3にJSONファイルを書き出し、後続のS3 GetObject SDK統合で読み取る方式を採用しました(以下、一部、読みやすさを優先して省略しています)。

"ReadEC2Result": {
  "Type": "Task",
  "Resource": "arn:aws:states:::aws-sdk:s3:getObject",
  "Parameters": {
    "Bucket.$": "$.s3BucketName",
    "Key": "results/create-ec2/result.json"
  },
  "ResultSelector": {
    "body.$": "States.StringToJson($.Body)"
  },
  "ResultPath": "$.ec2Result",
  "Next"

States.StringToJson 組み込み関数で、S3から読んだ文字列をJSONオブジェクトに変換しています。これにより、後続のCloudFormationスタック作成時に $.ec2Result.body.instanceId のようにドット記法でInstanceIdを参照できます。

フェーズ間のデータ受け渡し

Step Functionsのデータフローを理解する上で重要なのが ResultPath と ResultSelector です。

  • ResultPath: タスクの出力をステート入力JSONのどこに格納するかを指定する。 “ResultPath”: “$.buildResult” とすると、元の入力JSONに buildResult キーが追加された形で出力されます。元の入力データが失われないのがポイントです。
  • ResultSelector: タスクの出力から必要な部分だけを抽出する。S3 GetObjectの巨大なレスポンスから Body だけを取り出す、といった用途に使います。

今回の構成では、データが以下のように流れます。

CloudFormation Stack A 作成完了
↓ ExtractVpcOutputs (Passステート) でスタック名等を整形
↓
CodeBuild 環境変数 (VPC_STACK_NAME, S3_BUCKET) を受け取り
↓ buildspec.yml内でVPCスタックのOutputsを問い合わせてSubnetId, SecurityGroupIdを取得
↓ EC2インスタンスを作成
↓ result.json を S3 に書き出し
↓
S3 GetObject + States.StringToJson で InstanceId を取得
↓
CloudFormation Stack B Parameters (InstanceId)

この一連の流れが、Step Functionsの宣言的な定義だけで見通しよく書けるのはなかなか利便性高いです。

まとめ

試した結果、3つの検証ポイントはすべて問題なく動作しました。検証ポイントに対する結果と所感は以下の通りです。

  • CloudFormation SDK統合 + ポーリングループ: 自前で組む必要はあるが、パターンさえ覚えれば難しくない
  • CodeBuild .sync 統合: .sync サフィックスをつけるだけで完了待ちしてくれるので非常に楽
  • フェーズ間のデータ受け渡し: ResultPath/ResultSelector/States.StringToJson の組み合わせで柔軟に対応できる

今回で基本構造が確認できたので、次の記事ではNetwork Firewall Proxyのデプロイ自動化に進みます。

デプロイ手順とソースコード

本記事で実施した内容を実際に試してみたい方のために、デプロイ手順とソースコードを掲載します。
しかしあれですね、デプロイを楽にするためのStep Functions等のスタック(Orchestratorスタック)作成を個別に実施する必要がありそこは手順に基づく手作業になるので、結局デプロイの手順の数はそんなに削減されないという・・・。

1. Orchestratorスタックのデプロイ

まず、Step Functionsステートマシン、CodeBuildプロジェクト、S3バケット等を含むOrchestratorスタックをデプロイします。

aws cloudformation create-stack \
  --stack-name sfn-poc-orchestrator \
  --template-body file://cfn-sfn-poc-orchestrator.yaml \
  --capabilities CAPABILITY_IAM \
  --region ap-northeast-1

2. CloudFormationテンプレートとbuildspecのS3アップロード

Orchestratorスタックが作成したS3バケットに、VPC/Alarm用のCloudFormationテンプレートとbuildspecをアップロードします。

# S3バケット名を取得
BUCKET=$(aws cloudformation describe-stacks \
  --stack-name sfn-poc-orchestrator \
  --query "Stacks[0].Outputs[?OutputKey=='ArtifactBucketName'].OutputValue" \
  --output text \
  --region ap-northeast-1)

# CloudFormationテンプレートをアップロード
aws s3 cp cfn-sfn-poc-vpc.yaml s3://${BUCKET}/cfn-templates/cfn-sfn-poc-vpc.yaml
aws s3 cp cfn-sfn-poc-alarm.yaml s3://${BUCKET}/cfn-templates/cfn-sfn-poc-alarm.yaml

# buildspecをアップロード
aws s3 cp buildspec.yml s3://${BUCKET}/buildspec/sfn-poc-create-ec2/buildspec.yml

### 3. ステートマシンの実行

# ステートマシンARNを取得
STATE_MACHINE_ARN=$(aws cloudformation describe-stacks \
  --stack-name sfn-poc-orchestrator \
  --query "Stacks[0].Outputs[?OutputKey=='StateMachineArn'].OutputValue" \
  --output text \
  --region ap-northeast-1)

# ステートマシンを実行
aws stepfunctions start-execution \
  --state-machine-arn ${STATE_MACHINE_ARN} \
  --input "{
    \"vpcStackName\": \"sfn-poc-vpc\",
    \"alarmStackName\": \"sfn-poc-alarm\",
    \"vpcTemplateUrl\": \"https://s3.ap-northeast-1.amazonaws.com/${BUCKET}/cfn-templates/cfn-sfn-poc-vpc.yaml\",
    \"alarmTemplateUrl\": \"https://s3.ap-northeast-1.amazonaws.com/${BUCKET}/cfn-templates/cfn-sfn-poc-alarm.yaml\",
    \"s3BucketName\": \"${BUCKET}\",
    \"region\": \"ap-northeast-1\"
  }" \
  --region ap-northeast-1

Step Functionsコンソールで実行状況を確認できます。全ステップが成功すると、VPC、EC2インスタンス、CloudWatch Alarmが作成されます。

ソースコード

cfn-sfn-poc-orchestrator.yaml(Orchestrator: Step Functions + CodeBuild + S3)

AWSTemplateFormatVersion: '2010-09-09'
Description: Step Functions PoC - Orchestrator (State Machine + CodeBuild + S3)

Parameters:
  EnvironmentName:
    Type: String
    Default: dev

Resources:
  # ========================================
  # S3 Bucket
  # ========================================
  ArtifactBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub '${AWS::StackName}-artifacts-${AWS::AccountId}-${AWS::Region}'
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      VersioningConfiguration:
        Status: Enabled
      Tags:
        - Key: Cost
          Value: XXX

  # ========================================
  # CodeBuild IAM Role
  # ========================================
  CodeBuildRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: codebuild.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: CodeBuildPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ec2:RunInstances
                  - ec2:DescribeInstances
                  - ec2:DescribeInstanceStatus
                  - ec2:CreateTags
                Resource: '*'
              - Effect: Allow
                Action:
                  - cloudformation:DescribeStacks
                Resource: '*'
              - Effect: Allow
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetBucketLocation
                  - s3:ListBucket
                Resource:
                  - !Sub '${ArtifactBucket.Arn}'
                  - !Sub '${ArtifactBucket.Arn}/*'
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: '*'
              - Effect: Allow
                Action:
                  - ssm:GetParameters
                Resource: !Sub 'arn:aws:ssm:${AWS::Region}::parameter/aws/service/ami-amazon-linux-latest/*'
      Tags:
        - Key: Cost
          Value: XXX

  # ========================================
  # CodeBuild Project
  # ========================================
  CreateEC2Project:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: !Sub '${AWS::StackName}-create-ec2'
      ServiceRole: !GetAtt CodeBuildRole.Arn
      Artifacts:
        Type: NO_ARTIFACTS
      Environment:
        Type: LINUX_CONTAINER
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/amazonlinux2-x86_64-standard:5.0
        EnvironmentVariables:
          - Name: S3_BUCKET
            Value: !Ref ArtifactBucket
          - Name: SUBNET_ID
            Value: placeholder
          - Name: SECURITY_GROUP_ID
            Value: placeholder
      Source:
        Type: S3
        Location: !Sub '${ArtifactBucket}/buildspec/sfn-poc-create-ec2/'
      TimeoutInMinutes: 15
      Tags:
        - Key: Cost
          Value: XXX

  # ========================================
  # CloudFormation Execution Role
  # ========================================
  CloudFormationRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: cloudformation.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AdministratorAccess
      Tags:
        - Key: Cost
          Value: XXX

  # ========================================
  # Step Functions IAM Role
  # ========================================
  StepFunctionsRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: states.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: StepFunctionsPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - cloudformation:CreateStack
                  - cloudformation:DescribeStacks
                Resource: '*'
              - Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:PutObject
                Resource:
                  - !Sub '${ArtifactBucket.Arn}/*'
              - Effect: Allow
                Action:
                  - codebuild:StartBuild
                  - codebuild:StopBuild
                  - codebuild:BatchGetBuilds
                Resource:
                  - !GetAtt CreateEC2Project.Arn
              - Effect: Allow
                Action:
                  - iam:PassRole
                Resource:
                  - !GetAtt CloudFormationRole.Arn
                Condition:
                  StringEquals:
                    iam:PassedToService: cloudformation.amazonaws.com
              - Effect: Allow
                Action:
                  - events:PutTargets
                  - events:PutRule
                  - events:DescribeRule
                Resource: '*'
      Tags:
        - Key: Cost
          Value: XXX

  # ========================================
  # Step Functions State Machine
  # ========================================
  DeploymentStateMachine:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      StateMachineName: !Sub '${AWS::StackName}-deployment'
      RoleArn: !GetAtt StepFunctionsRole.Arn
      DefinitionString: !Sub |
        {
          "Comment": "SFN PoC: CloudFormation(VPC) -> CodeBuild(EC2) -> CloudFormation(Alarm)",
          "StartAt": "CreateVpcStack",
          "States": {
            "CreateVpcStack": {
              "Type": "Task",
              "Resource": "arn:aws:states:::aws-sdk:cloudformation:createStack",
              "Parameters": {
                "StackName.$": "$.vpcStackName",
                "TemplateURL.$": "$.vpcTemplateUrl",
                "RoleARN": "${CloudFormationRole.Arn}",
                "Tags": [{"Key": "Cost", "Value": "XXX"}]
              },
              "ResultPath": "$.createVpcResult",
              "Next": "WaitForVpcStack",
              "Catch": [{
                "ErrorEquals": ["States.ALL"],
                "Next": "DeploymentFailed",
                "ResultPath": "$.error"
              }]
            },
            "WaitForVpcStack": {
              "Type": "Wait",
              "Seconds": 15,
              "Next": "CheckVpcStack"
            },
            "CheckVpcStack": {
              "Type": "Task",
              "Resource": "arn:aws:states:::aws-sdk:cloudformation:describeStacks",
              "Parameters": {
                "StackName.$": "$.vpcStackName"
              },
              "ResultPath": "$.describeVpcResult",
              "Next": "IsVpcStackComplete",
              "Catch": [{
                "ErrorEquals": ["States.ALL"],
                "Next": "DeploymentFailed",
                "ResultPath": "$.error"
              }]
            },
            "IsVpcStackComplete": {
              "Type": "Choice",
              "Choices": [
                {
                  "Variable": "$.describeVpcResult.Stacks[0].StackStatus",
                  "StringEquals": "CREATE_COMPLETE",
                  "Next": "ExtractVpcOutputs"
                },
                {
                  "Variable": "$.describeVpcResult.Stacks[0].StackStatus",
                  "StringMatches": "*FAILED*",
                  "Next": "DeploymentFailed"
                },
                {
                  "Variable": "$.describeVpcResult.Stacks[0].StackStatus",
                  "StringMatches": "*ROLLBACK*",
                  "Next": "DeploymentFailed"
                }
              ],
              "Default": "WaitForVpcStack"
            },
            "ExtractVpcOutputs": {
              "Type": "Pass",
              "Parameters": {
                "vpcStackName.$": "$.vpcStackName",
                "alarmStackName.$": "$.alarmStackName",
                "alarmTemplateUrl.$": "$.alarmTemplateUrl",
                "s3BucketName.$": "$.s3BucketName"
              },
              "Next": "RunCreateEC2Build"
            },
            "RunCreateEC2Build": {
              "Type": "Task",
              "Resource": "arn:aws:states:::codebuild:startBuild.sync",
              "Parameters": {
                "ProjectName": "${CreateEC2Project}",
                "EnvironmentVariablesOverride": [
                  {
                    "Name": "VPC_STACK_NAME",
                    "Value.$": "$.vpcStackName",
                    "Type": "PLAINTEXT"
                  },
                  {
                    "Name": "S3_BUCKET",
                    "Value.$": "$.s3BucketName",
                    "Type": "PLAINTEXT"
                  }
                ]
              },
              "ResultPath": "$.buildResult",
              "Next": "ReadEC2Result",
              "Catch": [{
                "ErrorEquals": ["States.ALL"],
                "Next": "DeploymentFailed",
                "ResultPath": "$.error"
              }]
            },
            "ReadEC2Result": {
              "Type": "Task",
              "Resource": "arn:aws:states:::aws-sdk:s3:getObject",
              "Parameters": {
                "Bucket.$": "$.s3BucketName",
                "Key": "results/create-ec2/result.json"
              },
              "ResultSelector": {
                "body.$": "States.StringToJson($.Body)"
              },
              "ResultPath": "$.ec2Result",
              "Next": "CreateAlarmStack",
              "Catch": [{
                "ErrorEquals": ["States.ALL"],
                "Next": "DeploymentFailed",
                "ResultPath": "$.error"
              }]
            },
            "CreateAlarmStack": {
              "Type": "Task",
              "Resource": "arn:aws:states:::aws-sdk:cloudformation:createStack",
              "Parameters": {
                "StackName.$": "$.alarmStackName",
                "TemplateURL.$": "$.alarmTemplateUrl",
                "RoleARN": "${CloudFormationRole.Arn}",
                "Parameters": [
                  {
                    "ParameterKey": "InstanceId",
                    "ParameterValue.$": "$.ec2Result.body.instanceId"
                  }
                ],
                "Tags": [{"Key": "Cost", "Value": "XXX"}]
              },
              "ResultPath": "$.createAlarmResult",
              "Next": "WaitForAlarmStack",
              "Catch": [{
                "ErrorEquals": ["States.ALL"],
                "Next": "DeploymentFailed",
                "ResultPath": "$.error"
              }]
            },
            "WaitForAlarmStack": {
              "Type": "Wait",
              "Seconds": 15,
              "Next": "CheckAlarmStack"
            },
            "CheckAlarmStack": {
              "Type": "Task",
              "Resource": "arn:aws:states:::aws-sdk:cloudformation:describeStacks",
              "Parameters": {
                "StackName.$": "$.alarmStackName"
              },
              "ResultPath": "$.describeAlarmResult",
              "Next": "IsAlarmStackComplete",
              "Catch": [{
                "ErrorEquals": ["States.ALL"],
                "Next": "DeploymentFailed",
                "ResultPath": "$.error"
              }]
            },
            "IsAlarmStackComplete": {
              "Type": "Choice",
              "Choices": [
                {
                  "Variable": "$.describeAlarmResult.Stacks[0].StackStatus",
                  "StringEquals": "CREATE_COMPLETE",
                  "Next": "DeploymentSucceeded"
                },
                {
                  "Variable": "$.describeAlarmResult.Stacks[0].StackStatus",
                  "StringMatches": "*FAILED*",
                  "Next": "DeploymentFailed"
                },
                {
                  "Variable": "$.describeAlarmResult.Stacks[0].StackStatus",
                  "StringMatches": "*ROLLBACK*",
                  "Next": "DeploymentFailed"
                }
              ],
              "Default": "WaitForAlarmStack"
            },
            "DeploymentSucceeded": {
              "Type": "Succeed"
            },
            "DeploymentFailed": {
              "Type": "Fail",
              "Error": "DeploymentError",
              "Cause": "One or more deployment steps failed"
            }
          }
        }
      Tags:
        - Key: Cost
          Value: XXX

Outputs:
  StateMachineArn:
    Value: !Ref DeploymentStateMachine
  ArtifactBucketName:
    Value: !Ref ArtifactBucket
  CodeBuildProjectName:
    Value: !Ref CreateEC2Project

cfn-sfn-poc-vpc.yaml(Stack A: VPC基盤)

AWSTemplateFormatVersion: '2010-09-09'
Description: Step Functions PoC - VPC Base Infrastructure (Stack A)

Resources:
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.99.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-vpc'
        - Key: Cost
          Value: XXX

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-igw'
        - Key: Cost
          Value: XXX

  VPCGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref InternetGateway

  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.99.1.0/24
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-public-subnet'
        - Key: Cost
          Value: XXX

  RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-rt'
        - Key: Cost
          Value: XXX

  DefaultRoute:
    Type: AWS::EC2::Route
    DependsOn: VPCGatewayAttachment
    Properties:
      RouteTableId: !Ref RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  SubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref RouteTable

  SecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: SFN PoC Security Group - outbound only
      VpcId: !Ref VPC
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
      Tags:
        - Key: Name
          Value: !Sub '${AWS::StackName}-sg'
        - Key: Cost
          Value: XXX

Outputs:
  VpcId:
    Value: !Ref VPC
  SubnetId:
    Value: !Ref PublicSubnet
  SecurityGroupId:
    Value: !Ref SecurityGroup

cfn-sfn-poc-alarm.yaml(Stack B: CloudWatch Alarm)

AWSTemplateFormatVersion: '2010-09-09'
Description: Step Functions PoC - CloudWatch Alarm (Stack B)

Parameters:
  InstanceId:
    Type: String
    Description: EC2 Instance ID to monitor

Resources:
  CPUAlarm:
    Type: AWS::CloudWatch::Alarm
    Properties:
      AlarmName: !Sub '${AWS::StackName}-cpu-alarm'
      AlarmDescription: CPU utilization alarm for PoC test
      Namespace: AWS/EC2
      MetricName: CPUUtilization
      Dimensions:
        - Name: InstanceId
          Value: !Ref InstanceId
      Statistic: Average
      Period: 300
      EvaluationPeriods: 1
      Threshold: 80
      ComparisonOperator: GreaterThanThreshold
      TreatMissingData: notBreaching

Outputs:
  AlarmArn:
    Value: !GetAtt CPUAlarm.Arn
  AlarmName:
    Value: !Ref CPUAlarm

buildspec.yml(CodeBuild: EC2インスタンス作成)

version: 0.2
phases:
  build:
    commands:
      - echo "=== Getting VPC Stack Outputs ==="
      - echo "VPC Stack Name ${VPC_STACK_NAME}"
      - echo "S3 Bucket ${S3_BUCKET}"
      - SUBNET_ID=$(aws cloudformation describe-stacks --stack-name ${VPC_STACK_NAME} --query "Stacks[0].Outputs[?OutputKey=='SubnetId'].OutputValue" --output text --no-cli-pager)
      - echo "Subnet ID ${SUBNET_ID}"
      - SECURITY_GROUP_ID=$(aws cloudformation describe-stacks --stack-name ${VPC_STACK_NAME} --query "Stacks[0].Outputs[?OutputKey=='SecurityGroupId'].OutputValue" --output text --no-cli-pager)
      - echo "Security Group ID ${SECURITY_GROUP_ID}"
      - echo "=== Creating EC2 Instance ==="
      - INSTANCE_ID=$(aws ec2 run-instances --image-id resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64 --instance-type t3.micro --subnet-id ${SUBNET_ID} --security-group-ids ${SECURITY_GROUP_ID} --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=sfn-poc-test},{Key=Cost,Value=XXX}]' --query 'Instances[0].InstanceId' --output text --no-cli-pager)
      - echo "Instance ID ${INSTANCE_ID}"
      - echo "=== Waiting for instance to be running ==="
      - aws ec2 wait instance-running --instance-ids ${INSTANCE_ID}
      - echo "Instance is running"
      - echo "=== Writing result to S3 ==="
      - echo "{\"instanceId\":\"${INSTANCE_ID}\"}" > /tmp/result.json
      - cat /tmp/result.json
      - aws s3 cp /tmp/result.json s3://${S3_BUCKET}/results/create-ec2/result.json --no-cli-pager
      - echo "=== Done ==="
著者について

SCSKにて、AWSの技術支援を提供するAWSテクニカルエスコートサービスの業務に従事しています。
自称ネットワークエンジニア。
CloudFormationは触っていても面白みが感じられないけれどCDKは好き
…だったのですが、最近、生成AIで作りやすいのでCloudFormationをよく使うようになりました。生成AI任せなのでスキルは向上していません。

2024-2025 Japan AWS Top Engineer (Networking)
2024-2025 Japan All AWS Certifications Engineer

その他所持資格:
IPA 情報処理安全確保支援士

好きなすみっコはとかげ。

貝塚広行をフォローする

クラウドに強いによるエンジニアブログです。

SCSKクラウドサービス(AWS)は、企業価値の向上につながるAWS 導入を全面支援するオールインワンサービスです。AWS最上位パートナーとして、多種多様な業界のシステム構築実績を持つSCSKが、お客様のDX推進を強力にサポートします。

AWSクラウド
シェアする
タイトルとURLをコピーしました