ECS Fargateのデプロイ環境をCFnでサクッと構築してみた(Blue/Greenデプロイメント編)

Fargateのデプロイに慣れよう第二弾、Blue/Greenデプロイメント編です!

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。AWS事業本部トクヤマシュンです。

ECS Fargateのデプロイ方法には、RollingアップデートとBlue/Greenデプロイメントの2種類があります。
それぞれの違いはドキュメントに記載がありますが、実際に触ってみないと中々理解しづらいものです。

そこで今回はCloudFormationを使って両方のデプロイ環境をサクっと構築するためのテンプレートをご紹介します。
こちらは第二弾、Blue Greenデプロイメントです。 興味のある方は、是非一度ご自身の環境で試してみてください。

第一弾のRollingアップデートは次のエントリからどうぞ。

構築する環境

今回の構成は次の図の通りです。

Blue/Greenデプロイメントはフックを使う方法とプレースホルダーを使う方法がありますが、
今回はプレースホルダーを使って構築します。

CFnテンプレートなどのソースコード

構築用のCFnテンプレートやパイプラインソースとして利用するDockerファイルを下記GitHubに格納していますので、構築の際はご参考ください。

構築の際はCLIを使うことをオススメします。
parameters/配下に変数ファイルを準備していますので、各自の環境に合わせて適宜設定してください。
リソース名はプレフィックスをSystemName-Environmentで統一しています。
デフォルトでは、SystemName=system, Environment=devとしています。

次章からはCFnテンプレートを確認します。

CFnテンプレートは折りたたんで記載してありますので、ご注意ください

VPC構築

VPCスタック作成

次のCFnテンプレートを使ってスタックを作成し、VPCを構築します。
(VPCはRollingアップデート、Blue/Greenデプロイメント共通です)

vpc.yaml(クリックで展開)

vpc.yaml

AWSTemplateFormatVersion: 2010-09-09
Description:
  VPC Create

Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "System Configuration"
        Parameters:
          - Environment
          - SystemtName
      - Label:
          default: "Netowork Configuration"
        Parameters:
          - VPCCIDR
          - AvailabilityZone1
          - AvailabilityZone2
          - PublicSubnet1CIDR
          - PublicSubnet2CIDR
          - PrivateSubnet1CIDR
          - PrivateSubnet2CIDR

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------# 
Parameters:
  Environment:
    Default: dev
    Type: String
  SystemName:
    Default: system
    Type: String
  VPCCIDR:
    Type: String
    Default: "10.10.0.0/16"
  AvailabilityZone1:
    Type: String
    Default: "ap-northeast-1a"
  AvailabilityZone2:
    Type: String
    Default: "ap-northeast-1c"
  PublicSubnet1CIDR:
    Type: String
    Default: "10.10.0.0/24"
  PublicSubnet2CIDR:
    Type: String
    Default: "10.10.1.0/24"
  PrivateSubnet1CIDR:
    Type: String
    Default: "10.10.2.0/24"
  PrivateSubnet2CIDR:
    Type: String
    Default: "10.10.3.0/24"


Resources:
  # ------------------------------------------------------------#
  #  VPC
  # ------------------------------------------------------------#
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VPCCIDR
      EnableDnsSupport: 'true'
      EnableDnsHostnames: 'true'
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-vpc"
        - Key: Systemname
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"


  # ------------------------------------------------------------#
  #  Internet Gateway
  # ------------------------------------------------------------#
  IGW:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-igw"
        - Key: Systemname
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"


  AttachGateway:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref VPC
      InternetGatewayId: !Ref IGW

  # ------------------------------------------------------------#
  #  NAT Gateway
  # ------------------------------------------------------------#
  NGW1: 
    Type: "AWS::EC2::NatGateway"
    Properties: 
      AllocationId: !GetAtt NGW1EIP.AllocationId 
      SubnetId: !Ref PublicSubnet1
      Tags: 
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-ngw-1"
        - Key: Systemname
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"

  NGW1EIP: 
    Type: "AWS::EC2::EIP"
    Properties: 
      Domain: vpc
      Tags: 
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-nat-eip-1"
        - Key: Systemname
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"
    

  NGW2: 
    Type: "AWS::EC2::NatGateway"
    Properties: 
      AllocationId: !GetAtt NGW2EIP.AllocationId 
      SubnetId: !Ref PublicSubnet2
      Tags: 
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-ngw-2"
        - Key: Systemname
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"

  NGW2EIP: 
    Type: "AWS::EC2::EIP"
    Properties: 
      Domain: vpc
      Tags: 
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-nat-eip-2"
        - Key: Systemname
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"


  # ------------------------------------------------------------#
  #  PublicSubnet
  # ------------------------------------------------------------#
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref PublicSubnet1CIDR
      AvailabilityZone: !Ref AvailabilityZone1
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-public-subnet-1"
        - Key: Systemname
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"

  PublicSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref PublicSubnet2CIDR
      AvailabilityZone: !Ref AvailabilityZone2
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-public-subnet-2"
        - Key: Systemname
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"


  # ------------------------------------------------------------#
  #  Public RouteTable
  # ------------------------------------------------------------#
  PublicRouteTable1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: !Sub "${SystemName}-${Environment}-public-rt-1"
      - Key: Systemname
        Value: !Sub "${SystemName}"
      - Key: Environment
        Value: !Sub "${Environment}"


  PublicRouteTable2:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: !Sub "${SystemName}-${Environment}-public-rt-2"
      - Key: Systemname
        Value: !Sub "${SystemName}"
      - Key: Environment
        Value: !Sub "${Environment}"

  # ------------------------------------------------------------#
  #  Public Routing
  # ------------------------------------------------------------#
  PublicRoute1:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable1
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref IGW

  PublicRoute2:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PublicRouteTable2
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref IGW

  # ------------------------------------------------------------#
  #  Public RouteTable Association
  # ------------------------------------------------------------#
  PublicSubnetAttach1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable1
      SubnetId: !Ref PublicSubnet1

  PublicSubnetAttach2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable2
      SubnetId: !Ref PublicSubnet2

  # ------------------------------------------------------------#
  #  PrivateSubnet
  # ------------------------------------------------------------#
  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref PrivateSubnet1CIDR
      AvailabilityZone: !Ref AvailabilityZone1
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-private-subnet-1"
        - Key: Systemname
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"
 
  PrivateSubnet2:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: !Ref PrivateSubnet2CIDR
      AvailabilityZone: !Ref AvailabilityZone2
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-private-subnet-2"
        - Key: Systemname
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"


  # ------------------------------------------------------------#
  #  Private RouteTable
  # ------------------------------------------------------------#
  PrivateRouteTable1:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: !Sub "${SystemName}-${Environment}-private-rt-1"
      - Key: Systemname
        Value: !Sub "${SystemName}"
      - Key: Environment
        Value: !Sub "${Environment}"


  PrivateRouteTable2:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref VPC
      Tags:
      - Key: Name
        Value: !Sub "${SystemName}-${Environment}-private-rt-2"
      - Key: Systemname
        Value: !Sub "${SystemName}"
      - Key: Environment
        Value: !Sub "${Environment}"


  # ------------------------------------------------------------#
  #  Private Routing
  # ------------------------------------------------------------#
  PrivateRoute1:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable1
      DestinationCidrBlock: "0.0.0.0/0"
      NatGatewayId: !Ref NGW1

  PrivateRoute2:
    Type: AWS::EC2::Route
    Properties:
      RouteTableId: !Ref PrivateRouteTable2
      DestinationCidrBlock: "0.0.0.0/0"
      NatGatewayId: !Ref NGW2
  # ------------------------------------------------------------#
  #  Private RouteTable Association
  # ------------------------------------------------------------#
  PrivateSubnetAttach1:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable1
      SubnetId: !Ref PrivateSubnet1

  PrivateSubnetAttach2:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable2
      SubnetId: !Ref PrivateSubnet2

VPC構築のポイント

ECR構築

Blue/Greenデプロイメント用ECRスタック作成

次のCFnテンプレートを使ってスタックを作成し、Blue/Greenデプロイメント用のECRを構築します。  

ecr-blue-green.yaml(クリックで展開)

ecr-blue-green.yaml

AWSTemplateFormatVersion: 2010-09-09
Description:
  ECR Create

Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "System Configuration"
        Parameters:
          - Environment
          - SystemtName

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------# 
Parameters:
  Environment:
    Default: dev
    Type: String
  SystemName:
    Default: system
    Type: String

Resources:
  # ------------------------------------------------------------#
  #  ECR
  # ------------------------------------------------------------#
  BlueGreenECR:
    Type: AWS::ECR::Repository
    Properties: 
      RepositoryName: !Sub "${SystemName}-${Environment}-blgr-repo"
      EncryptionConfiguration: 
        EncryptionType: "AES256"
      ImageScanningConfiguration: 
        ScanOnPush: true
      ImageTagMutability: IMMUTABLE
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-blgr-repo"
        - Key: SystemName
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"

ECR構築のポイント

  • リポジトリはSSE-S3(AES256)によるサーバー側暗号化を実施
  • push時にイメージスキャンを行う
  • タグはIMMUTABLEとする

ECS構築

Blue/Greenアップデート用ECS

次のCFnテンプレートを使ってスタックを作成し、Blue/Greenアップデート用のECSを構築します。  

ecs-blue-green.yaml(クリックで展開)

ecs-blue-green.yaml

AWSTemplateFormatVersion: 2010-09-09
Description:
  ECS and ALB Create

Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "System Configuration"
        Parameters:
          - Environment
          - SystemtName
      - Label:
          default: "Netowork Configuration"
        Parameters:
          - VpcId
          - ALBSubnetId1
          - ALBSubnetId2
          - ECSSubnetId1
          - ECSSubnetId2
          - ALBAllowInboundIP
      - Label:
          default: "Fargate Configuration"
        Parameters:
          - ECSImage
          - ECSTaskCPUUnit
          - ECSTaskMemory
          - ECSTaskDesiredCount
      - Label:
          default: "Scaling Configuration"
        Parameters:
          - ServiceScaleCpuTarget
          - ServiceScaleInCooldown
          - ServiceScaleOutCooldown
          - TaskMinContainerCount
          - TaskMaxContainerCount

# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------# 
Parameters:
  Environment:
    Default: dev
    Type: String
  SystemName:
    Default: system
    Type: String

#VPCID
  VpcId:
    Description : "VPC ID"
    Type: AWS::EC2::VPC::Id
    Default: "vpc-"


#ALBSubnet1
  ALBSubnetId1:
    Description : "ALB Subnet 1st"
    Type : AWS::EC2::Subnet::Id
    Default: "subnet-public1Id"

#ALBSubnet2
  ALBSubnetId2:
    Description : "ALB Subnet 2nd"
    Type : AWS::EC2::Subnet::Id
    Default: "subnet-public2Id"

#ALBAllowInboundIpAddress
  ALBAllowInboundIP:
    Description : "ALB Subnet 2nd"
    Type: String
    Default: "xxx.xxx.xxx.xxx/32"

#ECSSubnet1
  ECSSubnetId1:
    Description : "ECS Subnet 1st"
    Type : AWS::EC2::Subnet::Id
    Default: "subnet-private1Id"

#ECSSubnet2
  ECSSubnetId2:
    Description : "ECS Subnet 2nd"
    Type : AWS::EC2::Subnet::Id
    Default: "subnet-private2Id"

#ECSTaskCPUUnit
  ECSTaskCPUUnit:
    Type: String
    Default: "256"

#ECSTaskMemory
  ECSTaskMemory:
    Type: String
    Default: "512"

#ECSImage
  ECSImage:
    Type: String
    Default: "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/system-dev-blgr-repo:latest"

#ECSTaskDesiredCount
  ECSTaskDesiredCount:
    Type: Number
    Default: 1

# Scaling params
  ServiceScaleCpuTarget:
    Description: Target Tracking Scaling CPU Target
    Type: Number
    Default: 70

  ServiceScaleInCooldown:
    Type: Number
    Description: Target Tracking Scale In Cooldown seconds
    Default: 180

  ServiceScaleOutCooldown:
    Type: Number
    Description: Target Tracking Scale Out Cooldown seconds
    Default: 60

  TaskMinContainerCount:
    Type: Number
    Description: Minimum number of containers to run for the service
    Default: 1
    ConstraintDescription: Value must be at least one

  TaskMaxContainerCount:
    Type: Number
    Description: Maximum number of containers to run for the service when auto scaling out
    Default: 2
    ConstraintDescription: Value must be at least one

Resources:

  # ------------------------------------------------------------#
  #  Security Group
  # ------------------------------------------------------------#
  ALBSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: ALB Security Group
      GroupName: !Sub "${SystemName}-${Environment}-blgr-alb-sg"
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Ref ALBAllowInboundIP
        - IpProtocol: tcp
          FromPort: 8080
          ToPort: 8080
          CidrIp: !Ref ALBAllowInboundIP
      Tags:
      - Key: Name
        Value: !Sub "${SystemName}-${Environment}-blgr-alb-sg"
      - Key: Systemname
        Value: !Sub "${SystemName}"
      - Key: Environment
        Value: !Sub "${Environment}"


  TaskSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Task Security Group
      GroupName: !Sub "${SystemName}-${Environment}-blgr-task-sg"
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          SourceSecurityGroupId: !Ref ALBSecurityGroup
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
      Tags:
      - Key: Name
        Value: !Sub "${SystemName}-${Environment}-blgr-task-sg"
      - Key: Systemname
        Value: !Sub "${SystemName}"
      - Key: Environment
        Value: !Sub "${Environment}"

# ------------------------------------------------------------#
#  ALB
# ------------------------------------------------------------#
  ALB:
    Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
    Properties:
      Name: !Sub "${SystemName}-${Environment}-blgr-alb"
      LoadBalancerAttributes:
        - Key: "deletion_protection.enabled"
          Value: false
        - Key: "idle_timeout.timeout_seconds"
          Value: 60
        - Key: "access_logs.s3.enabled"
          Value: true
        - Key: "access_logs.s3.bucket"
          Value: !Sub "${SystemName}-${Environment}-blgr-alb-log-bucket-${AWS::AccountId}"
      Scheme: "internet-facing"
      SecurityGroups:
        - !Ref ALBSecurityGroup
      Subnets:
        - !Ref ALBSubnetId1
        - !Ref ALBSubnetId2
      Type: "application"
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-blgr-alb"
        - Key: SystemName
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"

  ALBListenerBlue:
    Type: "AWS::ElasticLoadBalancingV2::Listener"
    Properties:
      DefaultActions:
        - TargetGroupArn: !Ref TargetGroupBlue
          Type: forward
      LoadBalancerArn: !Ref ALB
      Port: 80
      Protocol: HTTP

  ALBListenerGreen:
    Type: "AWS::ElasticLoadBalancingV2::Listener"
    Properties:
      DefaultActions:
        - TargetGroupArn: !Ref TargetGroupGreen
          Type: forward
      LoadBalancerArn: !Ref ALB
      Port: 8080
      Protocol: HTTP

  TargetGroupBlue:
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Properties:
      HealthCheckPath: /
      VpcId: !Ref VpcId
      Name: !Sub "${SystemName}-${Environment}-blgr-tg-blue"
      Protocol: HTTP
      Port: 80
      TargetType: ip

  TargetGroupGreen:
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Properties:
      HealthCheckPath: /
      VpcId: !Ref VpcId
      Name: !Sub "${SystemName}-${Environment}-blgr-tg-green"
      Protocol: HTTP
      Port: 80
      TargetType: ip

# ------------------------------------------------------------#
#  ALB Log S3 Bucket
# ------------------------------------------------------------#
  ALBLogBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub "${SystemName}-${Environment}-blgr-alb-log-bucket-${AWS::AccountId}"
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: "AES256"
            BucketKeyEnabled: true
      PublicAccessBlockConfiguration:
        BlockPublicAcls: TRUE
        BlockPublicPolicy: TRUE
        IgnorePublicAcls: TRUE
        RestrictPublicBuckets: TRUE
      LifecycleConfiguration:
        Rules:
          - Id: !Sub "${SystemName}-${Environment}-blgr-alb-log-lifecycle"
            Status: Enabled
            ExpirationInDays: 400
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-blgr-alb-log-bucket-${AWS::AccountId}"
        - Key: SystemName
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"

  logsBucketPolicy:
    Type: AWS::S3::BucketPolicy
    DependsOn: ALBLogBucket
    Properties:
      Bucket: !Sub "${SystemName}-${Environment}-blgr-alb-log-bucket-${AWS::AccountId}"
      PolicyDocument:
        Statement:
          - Action:
              - 's3:PutObject'
            Effect: 'Allow'
            Resource:
              - Fn::Join:
                  - ''
                  - - "arn:aws:s3:::"
                    - !Sub "${SystemName}-${Environment}-blgr-alb-log-bucket-${AWS::AccountId}"
                    - "/*"
            Principal:
              AWS: '582318560864'
          - Action:
              - 's3:GetBucketAcl'
            Effect: 'Allow'
            Resource:
              Fn::Join:
                  - ''
                  - - "arn:aws:s3:::"
                    - !Sub "${SystemName}-${Environment}-blgr-alb-log-bucket-${AWS::AccountId}"
            Principal:
              Service: 'logdelivery.elb.amazonaws.com'

# ------------------------------------------------------------#
# ECS Cluster
# ------------------------------------------------------------#
  ECSCluster:
    Type: "AWS::ECS::Cluster"
    Properties:
      ClusterName: !Sub "${SystemName}-${Environment}-blgr-cluster"
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-blgr-cluster"
        - Key: SystemName
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"


# ------------------------------------------------------------#
#  ECS LogGroup
# ------------------------------------------------------------#
  ECSLogGroup:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Sub "${SystemName}-${Environment}-blgr-cluster-log"

# ------------------------------------------------------------#
#  ECS Task Execution Role
# ------------------------------------------------------------#
  ECSTaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${SystemName}-${Environment}-blgr-task-execution-role"
      Path: /
      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
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-blgr-task-execution-role"
        - Key: SystemName
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"

# ------------------------------------------------------------#
#  ECS TaskDefinition
# ------------------------------------------------------------#
  ECSTaskDefinition:
    Type: "AWS::ECS::TaskDefinition"
    Properties:
      Cpu: !Ref ECSTaskCPUUnit
      ExecutionRoleArn: !Ref ECSTaskExecutionRole
      Family: !Sub "${SystemName}-${Environment}-blgr-task-definition"
      Memory: !Ref ECSTaskMemory
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      ContainerDefinitions:
        - Name: app-container
          Image: !Ref ECSImage               
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref ECSLogGroup
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: !Ref SystemName
          MemoryReservation: 128
          PortMappings:
            - HostPort: 80
              Protocol: tcp
              ContainerPort: 80
          ReadonlyRootFilesystem: false
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-blgr-task-definition"
        - Key: SystemName
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"


# ------------------------------------------------------------#
#  ECS Service
# ------------------------------------------------------------#
  ECSService:
    Type: AWS::ECS::Service
    DependsOn: ALBListenerBlue
    Properties:
      Cluster: !Ref ECSCluster
      DesiredCount: !Ref ECSTaskDesiredCount
      DeploymentController:
        Type: CODE_DEPLOY
      LaunchType: FARGATE
      LoadBalancers:
        -
          TargetGroupArn: !Ref TargetGroupBlue
          ContainerPort: 80
          ContainerName: !Sub "app-container"
      NetworkConfiguration:
        AwsvpcConfiguration:
            AssignPublicIp: DISABLED
            SecurityGroups:
              - !Ref TaskSecurityGroup
            Subnets:
              - !Ref ECSSubnetId1
              - !Ref ECSSubnetId2
      PlatformVersion: 1.4.0
      ServiceName: !Sub "${SystemName}-${Environment}-blgr-service"
      TaskDefinition: !Ref ECSTaskDefinition
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-blgr-service"
        - Key: SystemName
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"


# ------------------------------------------------------------#
#  Auto Scaling Service
# ------------------------------------------------------------#
  ServiceAutoScalingRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${SystemName}-${Environment}-blgr-autoscaling-role"
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: application-autoscaling.amazonaws.com
            Action: sts:AssumeRole
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-blgr-autoscaling-role"
        - Key: SystemName
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"
  ServiceAutoScalingPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: !Sub "${SystemName}-${Environment}-blgr-autoscaling-policy"
      PolicyDocument:
        Statement:
          - Effect: Allow
            Action:
              - application-autoscaling:*
              - cloudwatch:DescribeAlarms
              - cloudwatch:PutMetricAlarm
              - ecs:DescribeServices
              - ecs:UpdateService
            Resource: '*'
      Roles:
        - !Ref ServiceAutoScalingRole
  ServiceScalingTarget:
    Type: AWS::ApplicationAutoScaling::ScalableTarget
    Properties:
      MinCapacity: !Ref TaskMinContainerCount
      MaxCapacity: !Ref TaskMaxContainerCount
      ResourceId: 
        !Join
        - '/'
        - - 'service'
          - !Ref ECSCluster
          - !GetAtt ECSService.Name
      RoleARN: !GetAtt ServiceAutoScalingRole.Arn
      ScalableDimension: ecs:service:DesiredCount
      ServiceNamespace: ecs
    DependsOn:
      - ECSService
      - ServiceAutoScalingRole

  ServiceScalingPolicyCPU:
    Type: AWS::ApplicationAutoScaling::ScalingPolicy
    Properties:
      PolicyName: !Sub "${SystemName}-${Environment}-blgr-target-tracking-scaling-cpu"
      PolicyType: TargetTrackingScaling
      ScalingTargetId: !Ref ServiceScalingTarget
      TargetTrackingScalingPolicyConfiguration:
        TargetValue: !Ref ServiceScaleCpuTarget
        ScaleInCooldown: !Ref ServiceScaleInCooldown
        ScaleOutCooldown: !Ref ServiceScaleOutCooldown
        PredefinedMetricSpecification:
          PredefinedMetricType: ECSServiceAverageCPUUtilization
    DependsOn: ServiceScalingTarget

ECS構築のポイント

  • ALBは特定のソースIPからのみアクセス可能な想定
    • 変数ALBAllowInboundIPでセキュリティグループのインバウンドルールで許可するIPアドレスを指定
  • ALBのアクセスログはS3に出力
    • SSE-S3(AES256)で暗号化し、400日のライフサイクルを設定
  • Blue/Greenデプロイメントではテストポートに8080を設定
  • ECSサービスのAutoScalingはCPUの追跡ポリシーを設定
    • 閾値は変数で設定可能。変数値のデフォルトには70%を設定
    • Blue/Greenデプロイメントでは状況によってデプロイ失敗となるので注意
    • 参考:CodeDeployによるBlue/Greenデプロイ

CI/CD環境構築

Blue/Greenデプロイメント用CI/CDスタック作成

次のCFnテンプレートを使ってスタックを作成し、Blue/Greenアップデート用のCI/CD環境を構築します。  

cicd-blue-green.yaml(クリックで展開)

cicd-blue-green.yaml

AWSTemplateFormatVersion: 2010-09-09
Description: 
  Blue Green Update For ECS Fargate with GitHub

Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "System Configuration"
        Parameters:
          - Environment
          - SystemtName
      - Label:
          default: "VPC Configuration"
        Parameters:
          - VpcId
          - CodeBuildSubnetId1
          - CodeBuildSubnetId2
      - Label:
          default: "ECR・ECS Configuration"
        Parameters:
          - ECSClusterName
          - ECSServiceName
          - ECSTaskContainerName
          - ECRName
      - Label:
          default: "GitHub Configuration"
        Parameters:
          - GitHubOrganizationName
          - GitHubRepositoryName
          - GitHubBranchName
      - Label:
          default: "Blue/Green Configuration"
        Parameters:
          - TargetGroupBlueName
          - TargetGroupGreenName
          - ListenerBlueArn
          - ListenerGreenArn

Parameters:
  Environment:
    Default: dev
    Type: String
  SystemName:
    Default: system
    Type: String

  ECSClusterName:
    Default: system-dev-blgr-cluster
    Description : "CI/CD Deploy ECS Cluster"
    Type: String
  ECSServiceName:
    Default: system-dev-blgr-service
    Description : "CI/CD Deploy ECS Service"
    Type: String
  ECSTaskContainerName:
    Default: app-container
    Description : "CI/CD Deploy ECS Task Container"
    Type: String
  ECRName:
    Default: system-dev-blgr-repo
    Description : "ECR Repository Name"
    Type: String
  GitHubOrganizationName:
    Default: GitHubOrganizationName
    Description : "CI/CD GitHub Organization"
    Type: String
  GitHubRepositoryName:
    Default: GitHubRepositoryName
    Description : "CI/CD GitHub Repository"
    Type: String
  GitHubBranchName:
    Default: GitHubBranchName
    Description : "CI/CD GitHub Branch"
    Type: String
  VpcId:
    Default: vpc-
    Description : "VPC ID"
    Type: AWS::EC2::VPC::Id
  CodeBuildSubnetId1:
    Default: subnet-private1Id
    Description : "Private Subnet 1st"
    Type: AWS::EC2::Subnet::Id
  CodeBuildSubnetId2:
    Default: subnet-private2Id
    Description : "Private Subnet 2nd"
    Type: AWS::EC2::Subnet::Id
  TargetGroupBlueName:
    Default: system-dev-blgr-tg-blue
    Description : "Target Group Blue Name"
    Type: String
  TargetGroupGreenName:
    Default: system-dev-blgr-tg-green
    Description : "Target Group Green Name"
    Type: String
  ListenerBlueArn:
    Default: ListenerBlueArn
    Description : "Listener Blue Arn"
    Type: String
  ListenerGreenArn:
    Default: ListenerGreenArn
    Description : "Listener Green Arn"
    Type: String


Resources:
# ------------------------------------------------------------#
#  Security Group
# ------------------------------------------------------------#

  CodeBuildSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Blue Green Deploy CodeBuild Security Group
      GroupName: !Sub "${SystemName}-${Environment}-blgr-code-build-sg"
      VpcId: !Ref VpcId
      SecurityGroupEgress:
        - IpProtocol: -1
          CidrIp: 0.0.0.0/0
      Tags:
      - Key: Name
        Value: !Sub "${SystemName}-${Environment}-blgr-code-build-sg"
      - Key: Systemname
        Value: !Sub "${SystemName}"
      - Key: Environment
        Value: !Sub "${Environment}"

# ------------------------------------------------------------#
#  CICD Role
# ------------------------------------------------------------#
  CodeBuildServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${SystemName}-${Environment}-blgr-cicd-build-role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service: codebuild.amazonaws.com
            Action: sts:AssumeRole
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-blgr-cicd-build-role"
        - Key: SystemName
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"
  CodeBuildServicePolicy:
    DependsOn: CodePipelineServiceRole
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: !Sub "${SystemName}-${Environment}-blgr-cicd-build-policy"
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Resource: "*"
            Effect: Allow
            Action:
              - logs:CreateLogGroup
              - logs:CreateLogStream
              - logs:PutLogEvents
          - Resource: !Sub arn:aws:s3:::${BLGRArtifactBucket}/*
            Effect: Allow
            Action:
              - s3:GetObject
              - s3:PutObject
              - s3:GetObjectVersion
              - s3:GetBucketAcl
              - s3:GetBucketLocation
          - 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: "*"
            Effect: Allow
            Action:
              - ec2:CreateNetworkInterface
              - ec2:DescribeDhcpOptions
              - ec2:DescribeNetworkInterfaces
              - ec2:DeleteNetworkInterface
              - ec2:DescribeSubnets
              - ec2:DescribeSecurityGroups
              - ec2:DescribeVpcs
          - Resource: "*"
            Effect: Allow
            Action:
              - ec2:CreateNetworkInterface
              - ec2:DescribeDhcpOptions
              - ec2:DescribeNetworkInterfaces
              - ec2:DeleteNetworkInterface
              - ec2:DescribeSubnets
              - ec2:DescribeSecurityGroups
              - ec2:DescribeVpcs
              - ec2:CreateNetworkInterfacePermission
          - Resource: "*"
            Effect: Allow
            Action:
              - codestar-connections:UseConnection
      Roles:
        - !Ref CodeBuildServiceRole

  CodeDeployServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${SystemName}-${Environment}-blgr-cicd-deploy-role"
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: codedeploy.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS

  CodePipelineServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${SystemName}-${Environment}-blgr-cicd-pipeline-role"
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: codepipeline.amazonaws.com
            Action: sts:AssumeRole
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-blgr-cicd-build-role"
        - Key: SystemName
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}"  
  CodePipelineServicePolicy:
    Type: AWS::IAM::Policy
    DependsOn: CodePipelineServiceRole
    Properties:
      PolicyName: !Sub "${SystemName}-${Environment}-blgr-cicd-pipeline-policy"
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Resource:
              - !Sub arn:aws:s3:::${BLGRArtifactBucket}/*
            Effect: Allow
            Action:
              - s3:PutObject
              - s3:GetObject
              - s3:GetObjectVersion
              - s3:GetBucketVersioning
          - Resource: "*"
            Effect: Allow
            Action:
              - codecommit:GetRepository
              - codecommit:ListBranches
              - codecommit:GetUploadArchiveStatus
              - codecommit:UploadArchive
              - codecommit:CancelUploadArchive
              - codedeploy:CreateDeployment
              - codedeploy:GetApplication
              - codedeploy:GetApplicationRevision
              - codedeploy:GetDeployment
              - codedeploy:GetDeploymentConfig
              - codedeploy:RegisterApplicationRevision
              - codebuild:StartBuild
              - codebuild:StopBuild
              - codebuild:BatchGet*
              - codebuild:Get*
              - codebuild:List*
              - codecommit:GetBranch
              - codecommit:GetCommit
              - s3:*
              - ecs:*
              - elasticloadbalancing:*
              - autoscaling:*
              - iam:PassRole
              - codestar-connections:UseConnection
      Roles:
        - !Ref CodePipelineServiceRole

# ------------------------------------------------------------#
# IAMRole For CustomResource Lambda
# ------------------------------------------------------------#
  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${SystemName}-${Environment}-blgr-deploy-groupe-lambda-role"
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
        - arn:aws:iam::aws:policy/AWSCodeDeployFullAccess
      Tags:
        - Key: Name
          Value: !Sub "${SystemName}-${Environment}-blgr-deploy-groupe-lambda-role"
        - Key: SystemName
          Value: !Sub "${SystemName}"
        - Key: Environment
          Value: !Sub "${Environment}" 
  LambdaPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: !Sub "${SystemName}-${Environment}-blgr-cicd-build-policy"
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action:
              - ec2:*
              - logs:*
            Resource: '*'
          - Effect: Allow
            Resource: '*'
            Action:
              - iam:PassRole
            Condition:
              StringEqualsIfExists:
                iam:PassedToService:
                  - codedeploy.amazonaws.com
      Roles:
        - !Ref LambdaRole

# ------------------------------------------------------------#
#  Blue Green Artifact S3 Bucket
# ------------------------------------------------------------#
  BLGRArtifactBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub "${SystemName}-${Environment}-blgr-cicd-artifact-${AWS::AccountId}"
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: aws:kms
            BucketKeyEnabled: true
      PublicAccessBlockConfiguration:
        BlockPublicAcls: TRUE
        BlockPublicPolicy: TRUE
        IgnorePublicAcls: TRUE
        RestrictPublicBuckets: TRUE
      LifecycleConfiguration:
        Rules:
          - Id: !Sub "${SystemName}-${Environment}-blgr-cicd-build-artifact-lifecycle"
            Status: Enabled
            ExpirationInDays: 1827
      Tags:
      - Key: Name
        Value: !Sub "${SystemName}-${Environment}-blgr-cicd-artifact-${AWS::AccountId}"
      - Key: SystemName
        Value: !Sub "${SystemName}"
      - Key: Environment
        Value: !Sub "${Environment}"

# ------------------------------------------------------------#
#  GitHub Connection
# ------------------------------------------------------------#
  GitHubConnection:
    Type: AWS::CodeStarConnections::Connection
    Properties:
      ConnectionName: !Sub "${SystemName}-${Environment}-blgr-cicd-github"
      ProviderType: GitHub
      Tags:
      - Key: Name
        Value: !Sub "${SystemName}-${Environment}-blgr-cicd-github"
      - Key: SystemName
        Value: !Sub "${SystemName}"
      - Key: Environment
        Value: !Sub "${Environment}"

# ------------------------------------------------------------#
#  Code Build LogGroup
# ------------------------------------------------------------#
  CodeBuildLogGroup:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Sub "${SystemName}-${Environment}-blgr-cicd-code-build"

# ------------------------------------------------------------#
#  Code Build
# ------------------------------------------------------------#
  CodeBuildProject:
    Type: AWS::CodeBuild::Project
    DependsOn: CodeBuildServicePolicy
    Properties:
      Name: !Sub "${SystemName}-${Environment}-blgr-cicd-code-build"
      Environment:
        PrivilegedMode: true
        ComputeType: BUILD_GENERAL1_SMALL
        Image: "aws/codebuild/amazonlinux2-x86_64-standard:3.0"
        Type: LINUX_CONTAINER
        EnvironmentVariables:
          - Name: AWS_REGION
            Value: !Ref AWS::Region
          - Name: REPOSITORY_URI
            Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRName}
          - Name: ECSTaskContainerName
            Value: !Ref ECSTaskContainerName
      Artifacts:
        Type: CODEPIPELINE
      Source:
        Type: CODEPIPELINE
        BuildSpec: |
          version: 0.2
          env:
            variables:
              DOCKER_BUILDKIT: "1"
          phases:
            install:
              runtime-versions:
                docker: 19
            pre_build:
              commands:
                - AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
                - echo Logging in to ECR
                - aws ecr --region ${AWS_REGION} get-login-password | docker login --username AWS --password-stdin https://${REPOSITORY_URI}
                - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
                - IMAGE_TAG=${COMMIT_HASH:=latest}
            build:
              commands:
                - echo Build started on `date`
                - docker image build -t ${REPOSITORY_URI}:${COMMIT_HASH} .
            post_build:
              commands:
                - echo Build completed on `date`
                - echo Pushing the Docker images
                - docker image push ${REPOSITORY_URI}:${COMMIT_HASH}
                - printf '{"ImageURI":"%s"}' ${REPOSITORY_URI}:${COMMIT_HASH} > imageDetail.json
          artifacts:
              files: imageDetail.json      
      ServiceRole: !Ref CodeBuildServiceRole
      Cache: 
        Modes: 
          - "LOCAL_DOCKER_LAYER_CACHE"
          - "LOCAL_SOURCE_CACHE"
        Type: "LOCAL"
      LogsConfig: 
        CloudWatchLogs:
          Status: "ENABLED"
          GroupName: !Ref CodeBuildLogGroup
      Visibility: "PRIVATE"
      TimeoutInMinutes: 60
      QueuedTimeoutInMinutes: 480
      VpcConfig:
        SecurityGroupIds: 
          - !Ref CodeBuildSecurityGroup
        Subnets: 
          - !Ref CodeBuildSubnetId1
          - !Ref CodeBuildSubnetId2
        VpcId: !Ref VpcId
      Tags:
      - Key: Name
        Value: !Sub "${SystemName}-${Environment}-blgr-cicd-build"
      - Key: SystemName
        Value: !Sub "${SystemName}"
      - Key: Environment
        Value: !Sub "${Environment}"

# ------------------------------------------------------------#
# Lambda for Create Blue Green Deployment Group
# ------------------------------------------------------------#
  LambdaFunction:
    Type: 'AWS::Lambda::Function'
    DeletionPolicy: 'Delete'
    Properties:
      Code:
        ZipFile: |
          import boto3
          import json
          import logging
          import cfnresponse
          from botocore.exceptions import ClientError

          logger = logging.getLogger()
          logger.setLevel(logging.INFO)
          client = boto3.client('codedeploy')

          def lambda_handler(event, context):

              appName = event['ResourceProperties']['appName']
              deploymentGroup = event['ResourceProperties']['deploymentGroup']
              clusterName = event['ResourceProperties']['ECSClusterName']
              serviceName = event['ResourceProperties']['ECSServiceName']

              print('REQUEST RECEIVED:\n' + json.dumps(event))
              responseData = {}
              try:
                res = client.create_application(
                    applicationName=appName,
                    computePlatform='ECS'
                )
                logger.info(res)
                logger.info("SUCCESS: CodeDeploy Application created.")
                res = client.create_deployment_group(
                    applicationName=appName,
                    deploymentGroupName=deploymentGroup,
                    deploymentConfigName='CodeDeployDefault.ECSAllAtOnce',
                    serviceRoleArn=event['ResourceProperties']['CodeDeployServiceRoleArn'],
                    autoRollbackConfiguration={
                        'enabled': True,
                        'events': [
                            'DEPLOYMENT_FAILURE',
                        ]
                    },
                    deploymentStyle={
                        'deploymentType': 'BLUE_GREEN',
                        'deploymentOption': 'WITH_TRAFFIC_CONTROL'
                    },
                    blueGreenDeploymentConfiguration={
                        'terminateBlueInstancesOnDeploymentSuccess': {
                            'action': 'TERMINATE',
                            'terminationWaitTimeInMinutes': 5
                        },
                        'deploymentReadyOption': {
                            'actionOnTimeout': 'STOP_DEPLOYMENT',
                            'waitTimeInMinutes': 5
                        }
                    },
                    loadBalancerInfo={
                        'targetGroupPairInfoList': [
                            {
                                'targetGroups': [
                                    {
                                        'name': event['ResourceProperties']['TargetGroup1']
                                    },
                                    {
                                        'name': event['ResourceProperties']['TargetGroup2']
                                    },
                                ],
                                'prodTrafficRoute': {
                                    'listenerArns': [
                                        event['ResourceProperties']['ALBListener1'],
                                    ]
                                },
                                'testTrafficRoute': {
                                    'listenerArns': [
                                        event['ResourceProperties']['ALBListener2'],
                                    ]
                                }
                            },
                        ]
                    },
                    ecsServices=[
                        {
                            'serviceName': event['ResourceProperties']['ECSServiceName'],
                            'clusterName': event['ResourceProperties']['ECSClusterName']
                        },
                    ]
                )
              except ClientError as e:
                  logger.error("ERROR: Something error!")
                  logger.error(e)
                  responseData = {'error': str(e)}
                  cfnresponse.send(event, context, cfnresponse.FAILED, responseData)
              else:
                  logger.info(res)
                  logger.info(
                      "SUCCESS: CodeDeploy Application and DeploymentGroup created.")
                  return cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData)
      Handler: index.lambda_handler
      Role: !GetAtt LambdaRole.Arn
      Runtime: python3.8
      Timeout: 10
# ------------------------------------------------------------#
# Custom Resource
# ------------------------------------------------------------#
  CreateCodeDeploy:
    Type: Custom::CreateCodeDeploy
    DependsOn:
      - CodeDeployServiceRole
    Properties:
      ServiceToken: !GetAtt LambdaFunction.Arn
      Region: !Ref AWS::Region
      ECSClusterName: !Ref ECSClusterName
      ECSServiceName: !Ref ECSServiceName
      CodeDeployServiceRoleArn: !GetAtt CodeDeployServiceRole.Arn
      TargetGroup1: !Ref TargetGroupBlueName
      TargetGroup2: !Ref TargetGroupGreenName
      ALBListener1: !Ref ListenerBlueArn
      ALBListener2: !Ref ListenerGreenArn
      appName: !Sub "${SystemName}-${Environment}-blgr-app"
      deploymentGroup: !Sub "${SystemName}-${Environment}-blgr-deployment-group"

# ------------------------------------------------------------#
#  Code Pipeline
# ------------------------------------------------------------#
  Pipeline:
    Type: AWS::CodePipeline::Pipeline
    DependsOn: CodePipelineServicePolicy
    Properties:
      RoleArn: !GetAtt CodePipelineServiceRole.Arn
      Name: !Sub "${SystemName}-${Environment}-blgr-cicd-pipeline"
      ArtifactStore:
        Type: S3
        Location: !Ref BLGRArtifactBucket
      Stages:
        - Name: Source
          Actions:
            - Name: Source
              ActionTypeId:
                Category: Source
                Owner: AWS
                Version: 1
                Provider: CodeStarSourceConnection
              Configuration:
                FullRepositoryId: !Sub
                  - ${GitHubOrganizationName}/${GitHubRepositoryName}
                  - GitHubOrganizationName: !Ref GitHubOrganizationName
                    GitHubRepositoryName: !Ref GitHubRepositoryName
                ConnectionArn: !Ref GitHubConnection
                BranchName: !Ref GitHubBranchName
              RunOrder: 1
              OutputArtifacts:
                - Name: SourceArtifact
        - Name: Build
          Actions:
            - Name: Build
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: 1
                Provider: CodeBuild
              Configuration:
                ProjectName: !Ref CodeBuildProject
              RunOrder: 1
              InputArtifacts:
                - Name: SourceArtifact
              OutputArtifacts:
                - Name: BuildArtifact
        - Name: Deploy
          Actions:
            - Name: Deploy
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Version: 1
                Provider: CodeDeployToECS
              Configuration:
                AppSpecTemplateArtifact: SourceArtifact
                AppSpecTemplatePath: appspec.yaml
                TaskDefinitionTemplateArtifact: SourceArtifact
                TaskDefinitionTemplatePath: !Sub "${SystemName}-${Environment}-taskdef.json"
                ApplicationName: !Sub "${SystemName}-${Environment}-blgr-app"
                DeploymentGroupName: !Sub "${SystemName}-${Environment}-blgr-deployment-group"
                Image1ArtifactName:  BuildArtifact
                Image1ContainerName: IMAGE1_NAME
              RunOrder: 1
              InputArtifacts:
                - Name: SourceArtifact
                - Name:  BuildArtifact
              Region: !Ref AWS::Region
      Tags:
      - Key: Name
        Value: !Sub "${SystemName}-${Environment}-blgr-cicd-pipeline"
      - Key: SystemName
        Value: !Sub "${SystemName}"
      - Key: Environment
        Value: !Sub "${Environment}"

CI/CD環境構築のポイント

  • ソースアクションにはGitHubバージョン2を指定
    • このため、初回のパイプラインは必ず失敗
    • コンソール画面での接続設定後、ソースアクションの実行が可能になる
  • アーティファクトはS3に出力
    • SSE-S3(AES256)で暗号化し、400日のライフサイクルを設定
  • CodeDeploy ECS Blue/Greenデプロイメントグループの作成はLambdaカスタムリソースで実施
  • CodeDeproyのパラメーターは下記を設定
    • デプロイメントコンフィグ:ECSAllAtOnce
    • デプロイ失敗時:ロールバック
    • トラフィックの再ルーティングはルーティング準備ができてから5分以内に実施
    • 再ルーティング後、5分後に以前のタスクは終了

動作確認

それでは構築の動作確認をします。

CFn構築

下記の順番に実行します。

  • vpc.yaml
  • ecr-blue-green.yaml
    • 構築後、latestタグを付与したイメージをpushすること
  • ecs-blue-green.yaml
    • vpcの値に合わせてパラメータファイルを更新すること
  • cicd-blue-green.yaml
    • vpc・ecsの値に合わせてパラメータファイルを更新すること

AWS CLIで下記のコマンドを実行することで、スタックを構築します。

aws cloudformation create-stack \
--stack-name スタック名 \
--template-body file://./テンプレート名 \
--capabilities CAPABILITY_NAMED_IAM \
--parameters file://./parameters/パラメータ名 \
--tags Key=Environment,Value=<Environmentに設定する変数値> \
--tags Key=SystemName,Value=<SystemNameに設定する変数値>

GitHubのリポジトリ構築

BlueGreenデプロイ用のリポジトリを作成し、サンプルソースGitHub/フォルダの内容をアップロードします。

なお、アップロードの際は下記に注意してください。

  • ソースとするGitHubのルートディレクトリに下記ファイルが格納されていること。
    • appspec.yaml
    • SystemName-Encironment-taskdef.json
      • タスクの実行roleを構築したものに変更すること
      • サンプルソースではsystem-dev-taskdef.jsonを用意

AWSとGitHubとの接続

作成したCI/CDパイプラインはGitHubとの接続ができずに初回実行は失敗しています。

コンソール画面からGitHubとの接続の設定を行います。

新しいアプリをインストールする、をクリックします。

GitHubの画面でさきほど作成したリポジトリを選択してSaveします

接続のステータスが利用可能になっていることを確認します。

Blue/Greenデプロイメント実行

index.htmlを次のように更新し、GitHubのmainブランチにpushします。

index.html(クリックで展開)

index.html

<!DOCTYPE html>

<head>
  <meta charset="utf-8" />
  <title>testページ</title>
</head>

<body>
  <h1>Blue/Green Deployment Demo</h1>
</body>

</html>

pushを契機としてパイプラインが起動されます。

しばらく待つと、ビルドも完了します。
ECRを確認すると、さきほどpushしたGitHubのコミットIDをタグとする新たなイメージが作成されています。

CodeDeployの画面では、ステップ1から順にデプロイが行われていきます。

しばらく待つとステップが進み、ステップ3の待機状態となります。

この状態でALBのテストポート(8080)にアクセスすると、デプロイ後の画面確認ができます。

本来のポート(80)では以前のページに引き続きアクセスできます。

この状態でCodeDeployの画面から再ルーティングを行います。
なお、5分間再ルーティングをしないと、ロールバックする設定になっていますので、ご注意ください。

再ルーティングを行った後に本来のポートにアクセスすると、デプロイ後の画面が確認できます。

ALBのターゲットグループを確認すると、Blue側のターゲットグループはunuedとなり、アクセスがなくなっています。

この時点では、古いタスクも起動中のまま残っています。
何か異常があった際には、このタスクに切り戻すことも可能です。

問題なければ、元のタスクセットの終了をクリックします。

デプロイが成功となり、古いタスクが終了されます。

作成リソースの削除

作成時とは逆の順番で、CloudFormationスタックを削除し、リソースを削除してください。
ただしCodeDeployアプリケーションはカスタムリソースで作成しましたので、コンソールから削除してください。

最後に

ECS Fargateのデプロイについて、Blue/Greenデプロイメント環境をCFnで構築する方法を紹介しました。
試したことのない方はぜひ一度やってみてください!
本ブログがどなたかのお役に立てば幸いです。