ECSのBlue/Greenデプロイ環境をCloudFormationで作成してみた

ECSのBlue/Greenデプロイ環境をCloudFormationで作成してみた

Clock Icon2025.05.28

ECSのBlue/Greenデプロイを試すときにCloudFormationで何回も作り直せるようにしたかったので作成してみました。

作成する構成

今回作成する構成は以下になります。
ECS Blue_Green 1

構成図にある通りコードはGitHubリポジトリを使用します。
GitHubリポジトリのmainブランチへプッシュされた際にCodePipelineが動作してデプロイを開始します。
コンテナイメージはCodeBuildで作成してECRへプッシュします。
その後CodeDeployでECSへBlue/Greenデプロイを実行します。
Blue/Greenデプロイの動作は以下のブログをご確認ください。
https://dev.classmethod.jp/articles/ecs-deploytype/#toc-1

リソース作成

事前準備

リソース作成の前提としてGitHubリポジトリの作成とGitHubコネクションを作成してCodePipelineのソースステージとして使用できることを前提としています。
GitHubリポジトリとGitHubコネクションの作成は以下のドキュメントを参考に設定してください。
https://docs.github.com/ja/repositories/creating-and-managing-repositories/creating-a-new-repository
https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/connections-github.html

GitHubリポジトリを作成したらmainブランチに以下のファイルをプッシュしてください。

  • buildspec.yaml
  • appspec.yaml
  • index.html
  • taskdef.json

buildspec.yamlはコンテナイメージをビルドするコマンドを記載しています。
https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/ecs-cd-pipeline.html

AWS_ACCOUNT_ID、AWS_DEFAULT_REGION、IMAGE_REPO_NAMEはCodeBuildの環境変数から読み込まれます。
出力アーティファクトとしてimageDetail.json、appspec.yaml、taskdef.jsonを指定しています。
imageDetail.jsonは以下のドキュメントに記載されている通り、ECSとCodeDeployにECRのイメージURLを連携するためのファイルになります。
https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/file-reference.html#file-reference-ecs-bluegreen

buildspec.yaml
version: 0.2
phases:
    pre_build:
        commands:
            - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
            - REPOSITORY_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME
            - IMAGE_TAG=build-$(echo $CODEBUILD_BUILD_ID | awk -F":" '{print $2}')
    build:
        commands:
            - docker build -t $REPOSITORY_URI:latest .
            - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
    post_build:
        commands:
            - docker push $REPOSITORY_URI:latest
            - docker push $REPOSITORY_URI:$IMAGE_TAG
            - printf '{"ImageURI":"%s"}' $REPOSITORY_URI:$IMAGE_TAG > imageDetail.json
            - sed -i "s/AWS-Account-ID/${AWS_ACCOUNT_ID}/g" taskdef.json
artifacts:
    files:
        - imageDetail.json
        - appspec.yaml
        - taskdef.json

appspec.yamlはタスク定義のARNとコンテナ名、ポート番号を指定しています。
ECSにデプロイするときに使用できるパラメータなどは以下のドキュメントに記載されています。
https://docs.aws.amazon.com/ja_jp/codedeploy/latest/userguide/reference-appspec-file-structure-resources.html#reference-appspec-file-structure-resources-ecs
TaskDefinitionで<TASK_DEFINITION>と指定していますが、この部分はデプロイの実行時に自動でARNに変換されるのでそのままファイルに保存してください。
https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/action-reference-ECSbluegreen.html#action-reference-ECSbluegreen-input

appspec.yaml
version: 0.0
Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: <TASK_DEFINITION>
        LoadBalancerInfo:
          ContainerName: "task-dev"
          ContainerPort: 80

今回はApacheのコンテナイメージを使用してサンプルのHTMLファイルを公開するためECRのパブリックギャラリーからイメージを取得するDockerfileを作成します。

Dockerfile
FROM public.ecr.aws/docker/library/httpd:2.4
COPY ./html/ /usr/local/apache2/htdocs/

index.htmlはとくに説明することは無いですが一般的なテストページのファイルになります。

index.html
<html>
<head>
<meta charset="UTF-8">
<title>テストページ</title>
</head>
<body bgcolor="#10100E" text="#cccccc">
小林 陸 ECS<br>
</body>
</html>

taskdef.jsonにタスクの設定を記載します。
<IMAGE1_NAME>はデプロイ時に自動で変換されるのでそのままファイルに保存してください。
タスク実行ロールで指定しているIAMロールのAWS-Account-IDはbuildspec.yamlで変換を行っています。

taskdef.json
{
    "containerDefinitions": [
        {
            "name": "task-dev",
            "image": <IMAGE1_NAME>,
            "cpu": 0,
            "portMappings": [
                {
                    "containerPort": 80,
                    "hostPort": 80,
                    "protocol": "tcp"
                }
            ],
            "essential": true,
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "/ecs/logs/ecs-dev-log",
                    "awslogs-region": "ap-northeast-1",
                    "awslogs-stream-prefix": "ecs-dev-log"
                }
            }
        }
    ],
    "family": "task-dev",
    "executionRoleArn": "arn:aws:iam::AWS-Account-ID:role/iam-dev-ecs-tast-execution-role",
    "networkMode": "awsvpc",
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "cpu": "256",
    "memory": "512"
}

ディレクトリ構成は以下のようにしてください。

tree
.
├── Dockerfile
├── appspec.yaml
├── buildspec.yaml
├── html
│   └── index.html
└── taskdef.json

CI/CD環境作成

事前準備が完了したらECRと初期起動時用のコンテナイメージを作成します。
ECRは以下のCloudFormationテンプレートを使用して作成します。

AWSTemplateFormatVersion: "2010-09-09"
Description: ECR

Metadata:
# ------------------------------------------------------------#
# Metadata
# ------------------------------------------------------------# 
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label: 
          default: Parameters for env Name
        Parameters:
          - env

Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------# 
  env:
    Type: String
    Default: dev
    AllowedValues:
      - dev

Resources:
# ------------------------------------------------------------#
# ECR
# ------------------------------------------------------------# 
  ECR:
    Type: AWS::ECR::Repository
    Properties:
      EmptyOnDelete: true
      EncryptionConfiguration:
        EncryptionType: AES256
      RepositoryName: !Sub ecr-${env}

ECRの作成が完了したら以下のコマンドでコンテナイメージを作成してpushします。

aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com
docker build -t ecr-dev .
docker tag ecr-dev:latest AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/ecr-dev:latest
docker push AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/ecr-dev:latest

コンテナイメージのpushが完了したら以下のCloudFormationテンプレートでECSやCodePipelineなどを作成します。

AWSTemplateFormatVersion: "2010-09-09"
Description: ECS

Metadata:
# ------------------------------------------------------------#
# Metadata
# ------------------------------------------------------------# 
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Label: 
          default: Parameters for env Name
        Parameters:
          - env
      - Label:
          default: Parameters for Network
        Parameters:
          - VPCCIDR
          - PublicSubnet01CIDR
          - PublicSubnet02CIDR
          - PrivateSubnet01CIDR
          - PrivateSubnet02CIDR
      - Label:
          default: Parameters for CodePipeline
        Parameters:
          - ConnectionArn
          - FullRepositoryId

Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------# 
  env:
    Type: String
    Default: dev
    AllowedValues:
      - dev

  VPCCIDR:
    Default: 192.168.0.0/16
    Type: String

  PublicSubnet01CIDR:
    Default: 192.168.0.0/24
    Type: String

  PublicSubnet02CIDR:
    Default: 192.168.1.0/24
    Type: String

  PrivateSubnet01CIDR:
    Default: 192.168.2.0/24
    Type: String

  PrivateSubnet02CIDR:
    Default: 192.168.3.0/24
    Type: String

  ConnectionArn:
    Type: String

  FullRepositoryId:
    Type: String

Resources:
# ------------------------------------------------------------#
# S3
# ------------------------------------------------------------# 
  S3:
    Type: AWS::S3::Bucket
    Properties: 
      BucketEncryption: 
        ServerSideEncryptionConfiguration: 
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      BucketName: !Sub ${AWS::StackName}-${AWS::AccountId}-artifact
      OwnershipControls:
        Rules: 
          - ObjectOwnership: BucketOwnerEnforced
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True
      Tags:
        - Key: Name
          Value: !Sub ${AWS::StackName}-${AWS::AccountId}-artifact

# ------------------------------------------------------------#
# CloudWatch Logs
# ------------------------------------------------------------# 
  ECSLogGroup:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Sub "/ecs/logs/ecs-${env}-log"

# ------------------------------------------------------------#
# IAM
# ------------------------------------------------------------# 
  TaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ecs-tasks.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
      RoleName: !Sub iam-${env}-ecs-tast-execution-role

  CodeBuildRolePolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties: 
      PolicyDocument:
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            Action:
              - 's3:PutObject'
              - 's3:GetObject'
            Resource: "*"
          - Effect: Allow
            Action: 
              - 'codebuild:CreateReportGroup'
              - 'codebuild:CreateReport'
              - 'codebuild:UpdateReport'
              - 'codebuild:BatchPutTestCases'
              - 'codebuild:BatchPutCodeCoverages'
            Resource: "*"
          - Effect: Allow
            Action: 
              - 'logs:CreateLogGroup'
              - 'logs:CreateLogStream'
              - 'logs:PutLogEvents'
            Resource: "*"
          - Effect: Allow
            Action: 
              - ecr:GetAuthorizationToken
              - ecr:BatchCheckLayerAvailability
              - ecr:GetDownloadUrlForLayer
              - ecr:GetRepositoryPolicy
              - ecr:DescribeRepositories
              - ecr:ListImages
              - ecr:DescribeImages
              - ecr:BatchGetImage
              - ecr:InitiateLayerUpload
              - ecr:UploadLayerPart
              - ecr:CompleteLayerUpload
              - ecr:PutImage
            Resource: "*"
      ManagedPolicyName: !Sub iam-${env}-codebuild-role-policy

  CodeBuildRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument: 
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            Principal: 
              Service: 
                - codebuild.amazonaws.com
            Action: 
              - 'sts:AssumeRole'
      ManagedPolicyArns: 
        - !Ref CodeBuildRolePolicy
      RoleName: !Sub iam-${env}-codebuild-role
      Tags:
        - Key: Name
          Value: !Sub iam-${env}-codebuild-role

  CodeDeployRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - codedeploy.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS
      RoleName: !Sub iam-${env}-codedeploy-ecs-role

  CodeDeployIAMRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument: 
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            Principal: 
              Service: 
                - codedeploy.amazonaws.com
            Action: 
              - 'sts:AssumeRole'
      ManagedPolicyArns: 
        - arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole
      RoleName: !Sub iam-${env}-codedeploy-role
      Tags:
        - Key: Name
          Value: !Sub iam-${env}-codedeploy-role

  CodePipelineRolePolicy:
    Type: AWS::IAM::ManagedPolicy
    Properties: 
      PolicyDocument:
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            Action:
              - "codestar-connections:UseConnection"
            Resource: 
              - "*"
          - Effect: Allow
            Action:
              - "codebuild:BatchGetBuilds"
              - "codebuild:StartBuild"
              - "codebuild:BatchGetBuildBatches"
              - "codebuild:StartBuildBatch"
            Resource: 
              - "*"
          - Effect: Allow
            Action:
              - "codedeploy:CreateDeployment"
              - "codedeploy:GetApplication"
              - "codedeploy:GetApplicationRevision"
              - "codedeploy:GetDeployment"
              - "codedeploy:GetDeploymentConfig"
              - "codedeploy:RegisterApplicationRevision"
            Resource: 
              - "*"
          - Effect: Allow
            Action:
              - "s3:GetObject"
              - "s3:PutObject"
              - "s3:ListBucket"
            Resource: "*"
          - Effect: Allow
            Action:
              - "ecs:RegisterTaskDefinition"
              - "iam:PassRole"
            Resource: "*"
      ManagedPolicyName: !Sub iam-${env}-codepipeline-role-policy

  CodePipelineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument: 
        Version: "2012-10-17"
        Statement: 
          - Effect: Allow
            Principal: 
              Service: 
                - codepipeline.amazonaws.com
            Action: 
              - 'sts:AssumeRole'
      ManagedPolicyArns: 
        - !Ref CodePipelineRolePolicy
      RoleName: !Sub iam-${env}-codepipeline-role
      Tags:
        - Key: Name
          Value: !Sub iam-${env}-codepipeline-role

# ------------------------------------------------------------#
# VPC
# ------------------------------------------------------------# 
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags: 
        - Key: Name
          Value: !Sub vpc-${env}

# ------------------------------------------------------------#
# InternetGateway
# ------------------------------------------------------------# 
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags: 
        - Key: Name
          Value: !Sub igw-${env}

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

# ------------------------------------------------------------#
# Subnet
# ------------------------------------------------------------# 
  PublicSubnet01:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      CidrBlock: !Ref PublicSubnet01CIDR
      MapPublicIpOnLaunch: true
      Tags: 
        - Key: Name
          Value: !Sub subnet-${env}-pub1
      VpcId: !Ref VPC

  PublicSubnet02:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1c
      CidrBlock: !Ref PublicSubnet02CIDR
      MapPublicIpOnLaunch: true
      Tags: 
        - Key: Name
          Value: !Sub subnet-${env}-pub2
      VpcId: !Ref VPC

  PrivateSubnet01:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1a
      CidrBlock: !Ref PrivateSubnet01CIDR
      MapPublicIpOnLaunch: true
      Tags: 
        - Key: Name
          Value: !Sub subnet-${env}-prv1
      VpcId: !Ref VPC

  PrivateSubnet02:
    Type: AWS::EC2::Subnet
    Properties:
      AvailabilityZone: ap-northeast-1c
      CidrBlock: !Ref PrivateSubnet02CIDR
      MapPublicIpOnLaunch: true
      Tags: 
        - Key: Name
          Value: !Sub subnet-${env}-prv2
      VpcId: !Ref VPC

# ------------------------------------------------------------#
# RouteTable
# ------------------------------------------------------------# 
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: rtb-${env}-pub

  PublicRouteTableRoute:
    Type: AWS::EC2::Route
    Properties:
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
      RouteTableId: !Ref PublicRouteTable

  PublicRtAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet01

  PublicRtAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet02

  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: rtb-${env}-prv

  PrivateRtAssociation1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnet01

  PrivateRtAssociation2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnet02

# ------------------------------------------------------------#
# SecurityGroup
# ------------------------------------------------------------# 
  ALBSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: for alb
      GroupName: !Sub securitygroup-${env}-alb
      SecurityGroupEgress: 
        - CidrIp: 0.0.0.0/0
          FromPort: -1
          IpProtocol: -1
          ToPort: -1
      SecurityGroupIngress:
        - FromPort: 80
          IpProtocol: tcp
          CidrIp: 0.0.0.0/0
          ToPort: 80
        - FromPort: 8080
          IpProtocol: tcp
          CidrIp: 0.0.0.0/0
          ToPort: 8080
      Tags: 
        - Key: Name
          Value: !Sub securitygroup-${env}-alb
      VpcId: !Ref VPC

  ECSSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: for ecs
      GroupName: !Sub securitygroup-${env}-ecs
      SecurityGroupEgress: 
        - CidrIp: 0.0.0.0/0
          FromPort: -1
          IpProtocol: -1
          ToPort: -1
      SecurityGroupIngress:
        - FromPort: 80
          IpProtocol: tcp
          SourceSecurityGroupId: !Ref ALBSG
          ToPort: 80
      Tags: 
        - Key: Name
          Value: !Sub securitygroup-${env}-ecs
      VpcId: !Ref VPC

  VPCEndpointSG:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: for vpc endpoint
      GroupName: !Sub securitygroup-${env}-vpc-endpoint
      SecurityGroupEgress: 
        - CidrIp: 0.0.0.0/0
          FromPort: -1
          IpProtocol: -1
          ToPort: -1
      SecurityGroupIngress:
        - FromPort: 443
          IpProtocol: tcp
          SourceSecurityGroupId: !Ref ECSSG
          ToPort: 443
      Tags: 
        - Key: Name
          Value: !Sub securitygroup-${env}-vpc-endpoint
      VpcId: !Ref VPC

# ------------------------------------------------------------#
# VPC Endpoint
# ------------------------------------------------------------# 
  S3Endpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties: 
      RouteTableIds: 
        - !Ref PrivateRouteTable
      ServiceName: !Sub com.amazonaws.${AWS::Region}.s3
      VpcEndpointType: Gateway
      VpcId: !Ref VPC

  ECRdkrEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ecr.dkr
      VpcId: !Ref VPC
      SubnetIds: 
        - !Ref PrivateSubnet01
        - !Ref PrivateSubnet02
      SecurityGroupIds:
        - !Ref VPCEndpointSG

  ECRapiEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      ServiceName: !Sub com.amazonaws.${AWS::Region}.ecr.api
      VpcId: !Ref VPC
      SubnetIds:
        - !Ref PrivateSubnet01
        - !Ref PrivateSubnet02
      SecurityGroupIds:
        - !Ref VPCEndpointSG

  LogsEndpoint:
    Type: AWS::EC2::VPCEndpoint
    Properties:
      VpcEndpointType: Interface
      PrivateDnsEnabled: true
      ServiceName: !Sub com.amazonaws.${AWS::Region}.logs
      VpcId: !Ref VPC
      SubnetIds: 
        - !Ref PrivateSubnet01
        - !Ref PrivateSubnet02
      SecurityGroupIds:
        - !Ref VPCEndpointSG

# ------------------------------------------------------------#
# ALB
# ------------------------------------------------------------# 
  ALB:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      IpAddressType: ipv4
      LoadBalancerAttributes:
        - Key: deletion_protection.enabled
          Value: false
      Name: !Sub alb-${env}-ecs
      Scheme: internet-facing
      SecurityGroups:
        - !Ref ALBSG
      Subnets: 
        - !Ref PublicSubnet01
        - !Ref PublicSubnet02
      Tags: 
        - Key: Name
          Value: !Sub alb-${env}-ecs
      Type: application

  TargetGroup1:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckEnabled: true
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: /
      HealthCheckPort: traffic-port
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 5
      IpAddressType: ipv4
      Matcher:
        HttpCode: 200
      Name: !Sub tg-${env}-01
      Port: 80
      Protocol: HTTP
      ProtocolVersion: HTTP1
      Tags: 
        - Key: Name
          Value: !Sub tg-${env}-01
      TargetType: ip
      UnhealthyThresholdCount: 2
      VpcId: !Ref VPC

  TargetGroup2:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckEnabled: true
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: /
      HealthCheckPort: traffic-port
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 5
      IpAddressType: ipv4
      Matcher:
        HttpCode: 200
      Name: !Sub tg-${env}-02
      Port: 80
      Protocol: HTTP
      ProtocolVersion: HTTP1
      Tags: 
        - Key: Name
          Value: !Sub tg-${env}-02
      TargetType: ip
      UnhealthyThresholdCount: 2
      VpcId: !Ref VPC

  ALBHTTPListener1:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - TargetGroupArn: !Ref TargetGroup1
          Type: forward
      LoadBalancerArn: !Ref ALB
      Port: 80
      Protocol: HTTP

  ALBHTTPListener2:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - TargetGroupArn: !Ref TargetGroup2
          Type: forward
      LoadBalancerArn: !Ref ALB
      Port: 8080
      Protocol: HTTP

# ------------------------------------------------------------#
# ECS
# ------------------------------------------------------------# 
  ECSCluster:
    Type: AWS::ECS::Cluster
    Properties:
      CapacityProviders:
        - FARGATE
      ClusterName: !Sub ecs-${env}-cluster
      DefaultCapacityProviderStrategy:
        - CapacityProvider: FARGATE
          Weight: 1

  ECSTaskDef:
    Type: AWS::ECS::TaskDefinition
    Properties:
      ContainerDefinitions:
        - Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/ecr-${env}:latest
          LogConfiguration:
            LogDriver: awslogs
            Options: 
              awslogs-group: !Ref ECSLogGroup
              awslogs-region: !Ref "AWS::Region"
              awslogs-stream-prefix: !Sub ecs-${env}-log
          Name: !Sub task-${env}
          PortMappings:
            - ContainerPort: 80
              HostPort: 80
      Cpu: 256
      ExecutionRoleArn: !Ref TaskExecutionRole
      Family: !Sub task-${env}
      Memory: 512
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE

  ECSService:
    Type: AWS::ECS::Service
    DependsOn: 
      - ALBHTTPListener1
      - ALBHTTPListener2
    Properties:
      Cluster: !Ref ECSCluster
      DesiredCount: 1
      LoadBalancers:
        - ContainerName: !Sub task-${env}
          ContainerPort: 80
          TargetGroupArn: !Ref TargetGroup1
      NetworkConfiguration:
        AwsvpcConfiguration:
          SecurityGroups:
            - !Ref ECSSG
          Subnets:
            - !Ref PrivateSubnet01
            - !Ref PrivateSubnet02
      ServiceName: !Sub service-${env}
      TaskDefinition: !Ref ECSTaskDef
      PlatformVersion: LATEST
      DeploymentController: 
        Type: CODE_DEPLOY

# ------------------------------------------------------------#
# CodeBuild
# ------------------------------------------------------------# 
  CodeBuild:
    Type: AWS::CodeBuild::Project
    Properties: 
      Artifacts:
        Type: CODEPIPELINE
      Description: ECS Blue/Green Deploy test
      Environment:
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/amazonlinux-x86_64-standard:5.0
        Type: LINUX_CONTAINER
        EnvironmentVariables:
          - Name: AWS_DEFAULT_REGION
            Type: PLAINTEXT
            Value: !Sub ${AWS::Region}
          - Name: IMAGE_REPO_NAME
            Type: PLAINTEXT
            Value: !Sub ecr-${env}
          - Name: AWS_ACCOUNT_ID
            Type: PLAINTEXT
            Value: !Sub ${AWS::AccountId}
      Name: !Sub codebuild-${env}
      ServiceRole: !GetAtt CodeBuildRole.Arn
      Source: 
        BuildSpec: buildspec.yaml
        Type: CODEPIPELINE
      Tags:
        - Key: Name
          Value: !Sub codebuild-${env}

# ------------------------------------------------------------#
# CodeDeploy
# ------------------------------------------------------------# 
  CodeDeployApp:
    Type: AWS::CodeDeploy::Application
    DependsOn: ECSService
    Properties:
      ApplicationName: !Sub codedeploy-${env}-app
      ComputePlatform: ECS

  CodeDeployGroup:
    Type: AWS::CodeDeploy::DeploymentGroup
    Properties:
      ApplicationName: !Ref CodeDeployApp
      DeploymentGroupName: !Sub codedeploy-${env}-group
      DeploymentConfigName: CodeDeployDefault.ECSAllAtOnce
      AutoRollbackConfiguration:
        Enabled: true
        Events:
          - DEPLOYMENT_FAILURE
          - DEPLOYMENT_STOP_ON_REQUEST
      BlueGreenDeploymentConfiguration:
        DeploymentReadyOption:
          ActionOnTimeout: CONTINUE_DEPLOYMENT
          WaitTimeInMinutes: 0
        TerminateBlueInstancesOnDeploymentSuccess:
          Action: TERMINATE
          TerminationWaitTimeInMinutes: 5
      DeploymentStyle:
        DeploymentOption: WITH_TRAFFIC_CONTROL
        DeploymentType: BLUE_GREEN
      LoadBalancerInfo:
        TargetGroupPairInfoList:
          - ProdTrafficRoute:
              ListenerArns:
                - !Ref ALBHTTPListener1
            TestTrafficRoute:
              ListenerArns:
                - !Ref ALBHTTPListener2
            TargetGroups:
              - Name: !GetAtt TargetGroup1.TargetGroupName
              - Name: !GetAtt TargetGroup2.TargetGroupName
      ServiceRoleArn: !GetAtt CodeDeployRole.Arn
      ECSServices:
        - ClusterName:
            !Ref ECSCluster
          ServiceName:
            !GetAtt ECSService.Name

# ------------------------------------------------------------#
# CodePipeline
# ------------------------------------------------------------# 
  CodePipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      ArtifactStore:
        Location: !Ref S3
        Type: S3
      Name: !Sub codepipeline-${env}
      RoleArn: !GetAtt CodePipelineRole.Arn
      Stages: 
        - Actions:
          - ActionTypeId: 
              Category: Source
              Owner: AWS
              Provider: CodeStarSourceConnection
              Version: 1
            Configuration:
              ConnectionArn: !Ref ConnectionArn
              FullRepositoryId: !Ref FullRepositoryId
              BranchName: main
            Name: Source
            Namespace: SourceVariables
            OutputArtifacts:
              - Name: SourceArtifact
            Region: ap-northeast-1
            RunOrder: 1
          Name: Source
        - Actions:
          - ActionTypeId:
              Category: Build
              Owner: AWS
              Provider: CodeBuild
              Version: 1
            Configuration:
              ProjectName: !Ref CodeBuild
            InputArtifacts: 
              - Name: SourceArtifact
            Name: Build
            Namespace: BuildVariables
            OutputArtifacts: 
              - Name: BuildArtifact
            Region: ap-northeast-1
            RunOrder: 1
          Name: Build
        - Actions:
          - ActionTypeId: 
              Category: Deploy
              Owner: AWS
              Provider: CodeDeployToECS
              Version: 1
            Configuration:
              AppSpecTemplateArtifact: BuildArtifact
              AppSpecTemplatePath: appspec.yaml
              ApplicationName: !Ref CodeDeployApp
              DeploymentGroupName: !Ref CodeDeployGroup
              Image1ArtifactName: BuildArtifact
              Image1ContainerName: IMAGE1_NAME
              TaskDefinitionTemplateArtifact: BuildArtifact
              TaskDefinitionTemplatePath: taskdef.json
            Name: Deploy
            Namespace: DeployVariables
            InputArtifacts:
              - Name: BuildArtifact
            Region: ap-northeast-1
            RunOrder: 1
          Name: Deploy
      PipelineType: V2
      Tags:
        - Key: Name
          Value: !Sub codepipeline-${env}

CloudFormationテンプレートファイルを作成したら以下のAWS CLIコマンドでデプロイを行います。

aws cloudformation create-stack --stack-name スタック名 --template-body file://CloudFormationテンプレートファイル名 --capabilities CAPABILITY_NAMED_IAM --parameters ParameterKey=ConnectionArn,ParameterValue=GitHubコネクションのARN ParameterKey=FullRepositoryId,ParameterValue=
オーナ名/リポジトリ名

GitHubコネクションのARNはCodePipelineのコンソールから左のメニューを開いて設定欄の接続から確認ができます。
スクリーンショット 2025-05-28 162724

リソースの作成が完了したらALBのDNS名にHTTPでアクセスするとWebサイトが表示できることが確認できます。
スクリーンショット 2025-05-28 163153

Web画面が表示できたらhtml/index.htmlを編集してGitHubリポジトリにpushしてください。
リポジトリにpushするとCodePipelineが動き出しCodeDeployでデプロイされていることが確認できます。
スクリーンショット 2025-05-28 164632

デプロイ中にALBのDNS名で8080番ポートにアクセスすると更新した内容で表示されます。
スクリーンショット 2025-05-28 164857

さいごに

ECSのBlue/Greenデプロイ環境をCloudFormationで作成してみました。
マネジメントコンソールから設定すると結構手順が多いのですがIaC化しておくことで数分で環境を作れるようになります。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.