[2パターン] CFn で Fargate の Blue/Green Deployment の CodePipeline を構築する

2021.03.10

はじめに

おはようございます、もきゅりんです。

皆さん、Blue/Green デプロイメントしてますか?

Blue/Green デプロイメントとは、はすでに理解されていることを前提として話を進めます。

本稿では、Fargate の Blue/Green デプロイする CodePipelineを構築します。

CodePipeline を使った Fargate のBlue/Greenには下図の通り、プレースホルダを利用したものと、CodeDeploy::BlueGreen フックを利用したものと、2パターンがあるという認識です。(他にもいろいろあるかもしれませんが)

プレースホルダ

placeholder_pipeline

CodeDeploy::BlueGreen フック

hook_pipeline

2つのデプロイパターンの違い

プレースホルダとCodeDeploy::BlueGreenフックを使ったパターンにおける差異を簡単にまとめておきます。

プレースホルダ版でやってること

あなたの組織に最適なECSデプロイ手法の考察 | DevelopersIO で丁寧に解説がされていますが、要点は下記です。

CodeDeployの機能によって、バージョン管理ツール (CodeCommitやGitHub) に格納された taskdef.json のイメージURI および appspec.ymlのタスク定義のプレースホルダのみを更新してECS Blue/Greenデプロイを実行します。

したがって、タスク定義のURI以外の部分の更新は、Blue/Greenデプロイの管理外となります。

CodeDeploy::BlueGreen フック版(以下フック版)でやってること

詳細は、Create an Amazon ECS blue/green deployment through AWS CloudFormation - AWS CodeDeploy をご確認いただきたいのですが、Transform というAWSから提供されるマクロの機能を使って ECS の Blue/Green の環境を作成します。

マクロを使用すると、検索して置換操作のような単純なアクションからテンプレート全体の広範な変換 (Transform) まで、テンプレートに対してカスタム処理を実行できるようになります。

マクロの詳細は、AWS CloudFormation マクロを使用したテンプレートのカスタム処理の実行 - AWS CloudFormation をご参照下さい。

Transform は、AWS CloudFormation によってホストされているマクロで使用するための特別なアクセス許可は必要ありません。

よく認知されているのは、Serverless Application Model (AWS SAM) で使われている Transform かと思います。

Transform の詳細は、変換のリファレンス - AWS CloudFormationをご参照下さい。

こちらの要点としては、下記になります。

  • AWSCodeDeploy アプリケーションおよびデプロイメントグループを作成しない
  • アプリケーション仕様ファイル (AppSpecファイル)は指定せず、通常 AppSpec ファイルで管理される情報は、AWS::CodeDeploy::BlueGreen フックによって管理される
  • CFn スタックのタスク定義およびサービス定義が変更されたタイミングで Blue/Green デプロイメントを実行する

プレースホルダ版とは異なり、イメージURI以外のタスク定義の更新やサービスの設定更新も、Blue/Greenデプロイの管理範囲となります。

以下は通常のCodeDeployと異なり、注意が必要です。

  • トラフィックの再ルーティングの設定をできないため、トラフィックを再ルーティングするタイミング指定できない
  • 待機中に正式リリースを実行することはできないため、設定した終了待機まで旧環境は残り続ける
  • ロールバックはAWSCloudFormationでスタックの更新をキャンセルする
  • タスク定義およびサービス定義リソースに対する更新と、同じスタック更新内の他のリソースに対する更新を含めることはできないため、別々のスタック更新操作を実行するか、テンプレートから Transform および Hook セクションを削除(コメントアウト)して、スタックの更新を実行する

CloudFormation のスタックでリソース一式を管理することになります。

仕様の詳細は、AWSCloudFormationを使用してCodeDeployを介してECSブルー/グリーンデプロイを実行します-AWSCloudFormation をご確認下さい。

以下でプレースホルダ版とフック版をそれぞれ紹介します。

プレースホルダ版 Blue/Green デプロイ

前提

CloudFormation 一撃で Fargate のBlue/Green Deployment 環境を構築する を参考に下記リソースを構築します。

  • ECR
  • CodeDeployのアプリケーション/デプロイメントグループの作成
  • ECSの構築

準備にえらい時間がかかりますが、ようやく本題です。

やること

CodePipelineからECSにBlue/Greenデプロイする | DevelopersIO の内容になります。

図を再掲しますが、以下です。

placeholder_pipeline

対応することは、以下になります。

  1. CodeCommitにAppspec, Dockerfile(+その他) , taskdef.jsonをプッシュする
  2. CloudFormation を実行する

本番で利用する際は、“Too Many Requests.” でビルドが失敗する…。AWS CodeBuild で IP ガチャを回避するために Docker Hub ログインしよう!という話 | DevelopersIO を考慮してテンプレートに追記するのを推奨します。

手動承認ステージはコメントアウトしていますので、利用する場合は必要な設定を確認の上、ご利用下さい。

placeholder-codepipeline.yml

AWSTemplateFormatVersion: 2010-09-09
Description: CodePipeline For ECS Fargate Blue/Green Deploy with PlaceHolder
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Parameters:
  NameTagPrefix:
    Type: String
    Default: test
    Description: Prefix of Name tags.
  ServiceName:
    Type: String
    Default: myapp
    Description: Prefix of Service tags.
  CodeCommitRepositoryName:
    Type: String
  CodeDeployAppName:
    Type: String
  CodeDeployDGName:
    Type: String
  ContainerName:
    Type: String
  ECRName:
    Type: String
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Resources:
  # ------------------------------------------------------------#
  # IAM Roles
  # ------------------------------------------------------------#
  # CodeWatchEventを実行できるIAMRole
  CloudwatchEventRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${NameTagPrefix}-${ServiceName}-CloudWatchEventRole
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - events.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: CloudWatchEventsPipelineExecution
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: codepipeline:StartPipelineExecution
                Resource: !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${Pipeline}

  # CodeBuildに適用するIAMRole
  CodeBuildServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${NameTagPrefix}-${ServiceName}-CodeBuildServiceNameRole
      Path: /
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: codebuild.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: SampleCodeBuildAccess
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Resource: '*'
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
              - Effect: Allow
                Resource: !Sub arn:aws:s3:::${ArtifactBucket}/*
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketAcl
                  - s3:GetBucketLocation
              - Effect: Allow
                Action:
                  - codebuild:CreateReportGroup
                  - codebuild:CreateReport
                  - codebuild:UpdateReport
                  - codebuild:BatchPutTestCases
                  - codebuild:BatchPutCodeCoverages
                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: '*'

  # CodePipelineに適用するIAMRole
  CodePipelineServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${NameTagPrefix}-${ServiceName}-CodePipelineServiceRole
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: codepipeline.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: SamplePipeline
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Action:
                  - iam:PassRole
                Resource: '*'
                Effect: Allow
                Condition:
                  StringEqualsIfExists:
                    iam:PassedToService:
                      - ecs-tasks.amazonaws.com
              - Resource:
                  - !Sub arn:aws:s3:::${ArtifactBucket}/*
                Effect: Allow
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketVersioning
              - Action:
                  - codecommit:CancelUploadArchive
                  - codecommit:GetBranch
                  - codecommit:GetCommit
                  - codecommit:GetRepository
                  - codecommit:GetUploadArchiveStatus
                  - codecommit:UploadArchive
                Resource: '*'
                Effect: Allow
              - Action:
                  - codedeploy:CreateDeployment
                  - codedeploy:GetApplication
                  - codedeploy:GetApplicationRevision
                  - codedeploy:GetDeployment
                  - codedeploy:GetDeploymentConfig
                  - codedeploy:RegisterApplicationRevision
                  - codedeploy:*
                Resource: '*'
                Effect: Allow
              - Action:
                  - elasticbeanstalk:*
                  - ec2:*
                  - elasticloadbalancing:*
                  - autoscaling:*
                  - cloudwatch:*
                  - sns:*
                  - cloudformation:*
                  - rds:*
                  - sqs:*
                  - ecs:*
                Resource: '*'
                Effect: Allow
              - Action:
                  - codebuild:BatchGetBuilds
                  - codebuild:StartBuild
                  - codebuild:BatchGetBuildBatches
                  - codebuild:StartBuildBatch
                Resource: '*'
                Effect: Allow

  # S3Bucket
  ArtifactBucket:
    Type: AWS::S3::Bucket
    Properties:
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True

  # CloudWatchEventの実行ルール
  AmazonCloudWatchEventRule:
    Type: AWS::Events::Rule
    Properties:
      EventPattern:
        source:
          - aws.codecommit
        detail-type:
          - CodeCommit Repository State Change
        resources:
          - Fn::Join:
              - ''
              - - 'arn:aws:codecommit:'
                - !Ref 'AWS::Region'
                - ':'
                - !Ref 'AWS::AccountId'
                - ':'
                - !Ref CodeCommitRepositoryName
        detail:
          event:
            - referenceCreated
            - referenceUpdated
          referenceType:
            - branch
          referenceName:
            - master
      Targets:
        - Arn: !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${Pipeline}
          RoleArn: !GetAtt CloudwatchEventRole.Arn
          Id: codepipeline-AppPipeline

  # CodeBuild
  CodeBuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      ServiceRole: !Ref CodeBuildServiceRole
      Artifacts:
        Type: CODEPIPELINE
      Source:
        Type: CODEPIPELINE
        BuildSpec: |
          version: 0.2
          phases:
            pre_build:
              commands:
                - echo Logging in to Amazon ECR...
                - $(aws ecr get-login --no-include-email)
                - IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
            build:
              commands:
                - echo Build started on `date`
                - echo Building the Docker image...
                - docker build -t $REPOSITORY_URI:$IMAGE_TAG .
                - docker tag $REPOSITORY_URI:$IMAGE_TAG $REPOSITORY_URI:$IMAGE_TAG
            post_build:
              commands:
                - echo Build completed on `date`
                - echo Pushing the Docker images...
                - docker push $REPOSITORY_URI:$IMAGE_TAG
                - echo Writing imageDetail json...
                - echo "{\"name\":\"${ContainerName}\",\"ImageURI\":\"${REPOSITORY_URI}:${IMAGE_TAG}\"}" > imageDetail.json
          artifacts:
            files: imageDetail.json
      Environment:
        PrivilegedMode: true
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/standard:4.0
        Type: LINUX_CONTAINER
        EnvironmentVariables:
          - Name: AWS_DEFAULT_REGION
            Value: !Ref AWS::Region
          - Name: REPOSITORY_URI
            Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRName}
          - Name: ContainerName
            Value: !Ref ContainerName
          - Name: DOCKER_BUILDKIT
            Value: '1'
      Name: !Ref AWS::StackName
  # ------------------------------------------------------------#
  # CodePipeline
  # ------------------------------------------------------------#
  Pipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      RoleArn: !GetAtt CodePipelineServiceRole.Arn
      Name: !Sub ${NameTagPrefix}-${ServiceName}-pipeline
      ArtifactStore:
        Type: S3
        Location: !Ref ArtifactBucket
      Stages:
        - Name: Source
          Actions:
            - Name: SourceAction
              ActionTypeId:
                Category: Source
                Owner: AWS
                Version: '1'
                Provider: CodeCommit
              Configuration:
                RepositoryName: !Ref CodeCommitRepositoryName
                PollForSourceChanges: false
                BranchName: master
              RunOrder: 1
              OutputArtifacts:
                - Name: App
        - Name: Build
          Actions:
            - Name: Build
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: '1'
                Provider: CodeBuild
              Configuration:
                ProjectName: !Ref CodeBuildProject
              RunOrder: 1
              InputArtifacts:
                - Name: App
              OutputArtifacts:
                - Name: BuildOutput
        # - Name: Approval
        #   Actions:
        #     - Name: Manual_Approval
        #       ActionTypeId:
        #         Category: Approval
        #         Owner: AWS
        #         Version: '1'
        #         Provider: Manual
        #       Configuration:
        #         CustomData: !Sub '${ServiceName} will be updated. Do you want to deploy it?'
        #         NotificationArn: arn:aws:sns:ap-northeast-1:xxxxxxxx:hogehoge
        - Name: Deploy
          Actions:
            - Name: Deploy
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Version: '1'
                Provider: CodeDeployToECS
              Configuration:
                AppSpecTemplateArtifact: App
                AppSpecTemplatePath: appspec.yaml
                TaskDefinitionTemplateArtifact: App
                TaskDefinitionTemplatePath: taskdef.json
                ApplicationName: !Ref CodeDeployAppName
                DeploymentGroupName: !Ref CodeDeployDGName
                Image1ArtifactName: BuildOutput
                Image1ContainerName: IMAGE1_NAME
              RunOrder: 1
              InputArtifacts:
                - Name: App
                - Name: BuildOutput
              Region: !Ref AWS::Region
# ------------------------------------------------------------#
# Outputs
# ------------------------------------------------------------#
Outputs:
  PipelinelogicalID:
    Description: logical ID.
    Value: !Ref Pipeline

フック版 Blue/Green デプロイメント

前提

  • VPC、ALB Security Group、ECS Task Security Groupが構築済み
  • Fargateを配置するサブネットは、NATゲートウェイ等経由でインターネット通信可能であること
  • ECRが構築済み

やること

図を再掲しますが、以下です。

CodeBuildが新しくビルドされたイメージURI をCloudFormationのテンプレートに反映して、CodePipeline のデプロイステージで CloudFormation を実行します。

hook_pipeline

対応することは、以下になります。

  1. CodeCommitにDockerfile (+その他)、CloudFormationテンプレート をプッシュする
  2. CloudFormation を実行する

繰り返しますが、本番で利用する際は、“Too Many Requests.” でビルドが失敗する…。AWS CodeBuild で IP ガチャを回避するために Docker Hub ログインしよう!という話 | DevelopersIO を考慮してテンプレートに追記するのを推奨します。

デプロイメントグループの設定についてはテンプレートを確認の上、適宜変更下さい。

現状の設定は下記です。

  • デプロイ設定は、5分後ごとに20%ずつグリーンにルーティングします。
  • 元のリビジョンの終了は30分後です。

手動承認ステージはコメントアウトしていますので、利用する場合は必要な設定を確認の上、ご利用下さい。

AWSCloudFormationを使用してCodeDeployを介してECSブルー/グリーンデプロイを実行します-AWSCloudFormation に注記がありますが、使用してみてテンプレートの注意として下記があります。

注記

  • ALBおよびALB配下はすべてテンプレートで管理する必要があります
  • ServiceRole は必須と記載されているのですが、なくても動きます(なお、サンプルテンプレートには入っていません)
  • VPCやサブネットグループ、セキュリティグループ、クラスターなどはパラメータ指定できますが、イメージURIはパラメータ指定するとCodeDeployはうまく動きません
  • 必ず変更セットを確認してリソースの何がどう変わるかを確認してから実行しましょう

このパイプラインでは、コミットハッシュを元にイメージURIが更新されるため、CloudFormation のテンプレートの変更だけでもタスク定義が更新され、 フック対象のリソースだけのスタック更新ができません。

Transform および Hook セクションを削除(コメントアウト)して、ALBの設定変更をしたところ、問題なくスタック更新は成功しました。

その後に、再度コメントアウトを外して、コンテンツを更新して CodeCommit にプッシュしたところ、無事 Blue/Green デプロイは実行できます。

なお、仮にロールバックした場合、テンプレートの設定を現環境と合わせるために再度 Blue/Greenデプロイをし直す必要があるかと思います。

cfn-ecs-bg-deploy.yml

AWSTemplateFormatVersion: 2010-09-09
Description: Fargate Blue/Green template with CodeDeployBlueGreenHook
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Parameters:
  NameTagPrefix:
    Type: String
    Default: demo
    Description: Prefix of Name tags.
  Env:
    Type: String
    Default: dev
    Description: Prefix of Env tags.
  App:
    Type: String
    Default: apps
    Description: Prefix of App tags.
  Vpc:
    Type: AWS::EC2::VPC::Id
    Default: 'vpc-xxxxxx'
  Subnet1a:
    Type: AWS::EC2::Subnet::Id
    Default: 'subnet-xxxxxx'
  Subnet1c:
    Type: AWS::EC2::Subnet::Id
    Default: 'subnet-xxxxxx'
  LBSecurityGroup:
    Type: AWS::EC2::SecurityGroup::Id
    Default: 'sg-xxxxxxxxx'
  S3LogBucketName:
    Type: String
    Default: demo-alb-logs
  ECSCluster:
    Type: String
    Default: 'arn:aws:ecs:ap-northeast-1:xxxxxxxxxx:cluster/demo-cluster'
  FamilyName:
    Type: String
    Default: 'demo-container'
  TaskContainerName:
    Type: String
    Default: 'demo-container'
  TaskExecutionRoleArn:
    Type: String
    Default: 'arn:aws:iam::xxxxxxxxxxxxx:role/ecsTaskExecutionRole'
# ------------------------------------------------------------#
# Transform
# ------------------------------------------------------------#
Transform:
  - 'AWS::CodeDeployBlueGreen'
Hooks:
    CodeDeployBlueGreenHook:
    Properties:
      TrafficRoutingConfig:
        Type: TimeBasedLinear
        TimeBasedLinear:
          StepPercentage: 20
          BakeTimeMins: 5
        AdditionalOptions:
          TerminationWaitTimeInMinutes: 30
      Applications:
        - Target:
            Type: 'AWS::ECS::Service'
            LogicalID: ECSService
          ECSAttributes:
            TaskDefinitions:
              - BlueTaskDefinition
              - GreenTaskDefinition
            TaskSets:
              - BlueTaskSet
              - GreenTaskSet
            TrafficRouting:
              ProdTrafficRoute:
                Type: 'AWS::ElasticLoadBalancingV2::Listener'
                LogicalID: listenerProdTraffic
              TargetGroups:
                - lbTargetGroupBlue
                - lbTargetGroupGreen
    Type: 'AWS::CodeDeploy::BlueGreen'
# ------------------------------------------------------------#
# Resources
# ------------------------------------------------------------#
Resources:
  lbTargetGroupBlue:
    Type: 'AWS::ElasticLoadBalancingV2::TargetGroup'
    Properties:
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: /
      HealthCheckPort: '80'
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 5
      Matcher:
        HttpCode: '200'
      Port: 80
      Protocol: HTTP
      TargetType: ip
      UnhealthyThresholdCount: 5
      VpcId: !Ref Vpc
  lbTargetGroupGreen:
    Type: 'AWS::ElasticLoadBalancingV2::TargetGroup'
    Properties:
      HealthCheckIntervalSeconds: 30
      HealthCheckPath: /
      HealthCheckPort: '80'
      HealthCheckProtocol: HTTP
      HealthCheckTimeoutSeconds: 5
      HealthyThresholdCount: 5
      Matcher:
        HttpCode: '200'
      Port: 80
      Protocol: HTTP
      TargetType: ip
      UnhealthyThresholdCount: 5
      VpcId: !Ref Vpc
  loadBalancer:
    Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer'
    Properties:
      Name: !Sub ${NameTagPrefix}-${Env}-${App}-alb
      LoadBalancerAttributes:
        - Key: deletion_protection.enabled
          Value: 'false'
        - Key: access_logs.s3.enabled
          Value: 'true'
        - Key: routing.http2.enabled
          Value: 'true'
        - Key: routing.http.drop_invalid_header_fields.enabled
          Value: 'false'
        - Key: idle_timeout.timeout_seconds
          Value: '60'
        - Key: access_logs.s3.bucket
          Value: !Ref S3LogBucketName
        - Key: access_logs.s3.prefix
          Value: 'prod'
      Scheme: internet-facing
      SecurityGroups:
        - !Ref LBSecurityGroup
      Subnets:
        - !Ref Subnet1a
        - !Ref Subnet1c
      Type: application
  listenerProdTraffic:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref lbTargetGroupBlue
      LoadBalancerArn: !Ref loadBalancer
      Port: 80
      Protocol: HTTP
  listenerTestTraffic:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref lbTargetGroupGreen
      LoadBalancerArn: !Ref loadBalancer
      Port: 8080
      Protocol: HTTP
  BlueTaskDefinition:
    Type: 'AWS::ECS::TaskDefinition'
    Properties:
      ExecutionRoleArn: !Ref TaskExecutionRoleArn
      ContainerDefinitions:
        - Name: !Ref TaskContainerName
          Image: xxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/hogehoge:1.0.0
          Essential: true
          PortMappings:
            - HostPort: 80
              Protocol: tcp
              ContainerPort: 80
      RequiresCompatibilities:
        - FARGATE
      NetworkMode: awsvpc
      Cpu: '256'
      Memory: '512'
      Family: !Ref FamilyName
  ECSService:
    Type: 'AWS::ECS::Service'
    Properties:
      Cluster: !Ref ECSCluster
      DesiredCount: 1
      DeploymentController:
        Type: EXTERNAL
  BlueTaskSet:
    Type: 'AWS::ECS::TaskSet'
    Properties:
      Cluster: !Ref ECSCluster
      LaunchType: FARGATE
      NetworkConfiguration:
        AwsVpcConfiguration:
          AssignPublicIp: ENABLED
          SecurityGroups:
            - !Ref LBSecurityGroup
          Subnets:
            - !Ref Subnet1a
            - !Ref Subnet1c
      PlatformVersion: LATEST
      Scale:
        Unit: PERCENT
        Value: 1
      Service: !Ref ECSService
      TaskDefinition: !Ref BlueTaskDefinition
      LoadBalancers:
        - ContainerName: !Ref TaskContainerName
          ContainerPort: 80
          TargetGroupArn: !Ref lbTargetGroupBlue
  PrimaryTaskSet:
    Type: 'AWS::ECS::PrimaryTaskSet'
    Properties:
      Cluster: !Ref ECSCluster
      Service: !Ref ECSService
      TaskSetId: !GetAtt
        - BlueTaskSet
        - Id

hook-codepipeline.yml

AWSTemplateFormatVersion: 2010-09-09
Description: CodePipeline For ECS Fargate Blue/Green Deploy with CodeDeploy Hook
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Parameters:
  NameTagPrefix:
    Type: String
    Default: demo
    Description: Prefix of Name tags.
  Env:
    Type: String
    Default: stg
    Description: Prefix of Env tags.
  ServiceName:
    Type: String
    Default: myapp
    Description: Prefix of Service tags.
  CodeCommitRepoName:
    Type: String
  ECRName:
    Type: String
  TemplateName:
    Type: String
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Resources:
  # ------------------------------------------------------------#
  # IAM Roles
  # ------------------------------------------------------------#
  # CodeWatchEventを実行できるIAMRole
  CloudwatchEventRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${NameTagPrefix}-${Env}-${ServiceName}-CloudWatchEventRole
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - events.amazonaws.com
            Action: sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: CloudWatchEventsPipelineExecution
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action: codepipeline:StartPipelineExecution
                Resource: !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${Pipeline}

  # CodeBuildに適用するIAMRole
  CodeBuildServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${NameTagPrefix}-${Env}-${ServiceName}-CodeBuildServiceNameRole
      Path: /
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: codebuild.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: SampleCodeBuildAccess
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Resource: '*'
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
              - Effect: Allow
                Resource: !Sub arn:aws:s3:::${ArtifactBucket}/*
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketAcl
                  - s3:GetBucketLocation
              - Effect: Allow
                Action:
                  - codebuild:CreateReportGroup
                  - codebuild:CreateReport
                  - codebuild:UpdateReport
                  - codebuild:BatchPutTestCases
                  - codebuild:BatchPutCodeCoverages
                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: '*'

  # CloudFormationに適用するIAMRole
  CFnServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${NameTagPrefix}-${Env}-${ServiceName}-CloudFormationRole
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: cloudformation.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: SampleCloudFormationPolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Action:
                  - iam:PassRole
                Resource: '*'
                Effect: Allow
                Condition:
                  StringEqualsIfExists:
                    iam:PassedToService:
                      - ecs-tasks.amazonaws.com
              - Action:
                  - codedeploy:CreateDeployment
                  - codedeploy:GetApplication
                  - codedeploy:GetApplicationRevision
                  - codedeploy:GetDeployment
                  - codedeploy:GetDeploymentConfig
                  - codedeploy:RegisterApplicationRevision
                  - codedeploy:*
                Resource: '*'
                Effect: Allow
              - Action:
                  - ec2:*
                  - elasticloadbalancing:*
                  - autoscaling:*
                  - cloudwatch:*
                  - sns:*
                  - cloudformation:*
                  - ecs:*
                Resource: '*'
                Effect: Allow

  # CodePipelineに適用するIAMRole
  CodePipelineServiceRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${NameTagPrefix}-${Env}-${ServiceName}-CodePipelineServiceRole
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: codepipeline.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: SamplePipeline
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Action:
                  - iam:PassRole
                Resource: '*'
                Effect: Allow
                Condition:
                  StringEqualsIfExists:
                    iam:PassedToService:
                      - ecs-tasks.amazonaws.com
              - Resource:
                  - !Sub arn:aws:s3:::${ArtifactBucket}/*
                Effect: Allow
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketVersioning
              - Resource: '*'
                Effect: Allow
                Action:
                  - ecr:DescribeImages
              - Action:
                  - codecommit:CancelUploadArchive
                  - codecommit:GetBranch
                  - codecommit:GetCommit
                  - codecommit:GetRepository
                  - codecommit:GetUploadArchiveStatus
                  - codecommit:UploadArchive
                Resource: '*'
                Effect: Allow
              - Action:
                  - codedeploy:CreateDeployment
                  - codedeploy:GetApplication
                  - codedeploy:GetApplicationRevision
                  - codedeploy:GetDeployment
                  - codedeploy:GetDeploymentConfig
                  - codedeploy:RegisterApplicationRevision
                  - codedeploy:*
                Resource: '*'
                Effect: Allow
              - Action:
                  - elasticbeanstalk:*
                  - ec2:*
                  - elasticloadbalancing:*
                  - autoscaling:*
                  - cloudwatch:*
                  - sns:*
                  - cloudformation:*
                  - rds:*
                  - sqs:*
                  - ecs:*
                Resource: '*'
                Effect: Allow
              - Action:
                  - codebuild:BatchGetBuilds
                  - codebuild:StartBuild
                  - codebuild:BatchGetBuildBatches
                  - codebuild:StartBuildBatch
                Resource: '*'
                Effect: Allow

  # S3Bucket
  ArtifactBucket:
    Type: AWS::S3::Bucket
    Properties:
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True

  # CloudWatchEventの実行ルール
  AmazonCloudWatchEventRule:
    Type: AWS::Events::Rule
    Properties:
      EventPattern:
        source:
          - aws.codecommit
        detail-type:
          - CodeCommit Repository State Change
        resources:
          - Fn::Join:
              - ''
              - - 'arn:aws:codecommit:'
                - !Ref 'AWS::Region'
                - ':'
                - !Ref 'AWS::AccountId'
                - ':'
                - !Ref CodeCommitRepoName
        detail:
          event:
            - referenceCreated
            - referenceUpdated
          referenceType:
            - branch
          referenceName:
            - master
      Targets:
        - Arn: !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${Pipeline}
          RoleArn: !GetAtt CloudwatchEventRole.Arn
          Id: codepipeline-AppPipeline

  # CodeBuild
  CodeBuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      ServiceRole: !Ref CodeBuildServiceRole
      Artifacts:
        Type: CODEPIPELINE
      Source:
        Type: CODEPIPELINE
        BuildSpec: |
          version: 0.2
          phases:
            pre_build:
              commands:
                - echo Logging in to Amazon ECR...
                - $(aws ecr get-login --no-include-email)
                - IMAGE_TAG=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
            build:
              commands:
                - echo Build started on `date`
                - echo Building the Docker image...
                - docker build -t $REPOSITORY_URI:$IMAGE_TAG .
                - docker tag $REPOSITORY_URI:$IMAGE_TAG $REPOSITORY_URI:$IMAGE_TAG
            post_build:
              commands:
              - echo Build completed on `date`
              - echo Pushing the Docker images...
              - docker push $REPOSITORY_URI:$IMAGE_TAG
              - echo Updating CloudFormation Template...
              - sed -i -e "s|$REPOSITORY_URI:.*$|$REPOSITORY_URI:$IMAGE_TAG|" $TEMPLATE_NAME.yml
          artifacts:
            files: $TEMPLATE_NAME.yml
      Environment:
        PrivilegedMode: true
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/standard:4.0
        Type: LINUX_CONTAINER
        EnvironmentVariables:
          - Name: AWS_DEFAULT_REGION
            Value: !Ref AWS::Region
          - Name: REPOSITORY_URI
            Value: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRName}
          - Name: TEMPLATE_NAME
            Value: !Ref TemplateName
          - Name: DOCKER_BUILDKIT
            Value: '1'
      Name: !Ref AWS::StackName
  # ------------------------------------------------------------#
  # CodePipeline
  # ------------------------------------------------------------#
  Pipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      RoleArn: !GetAtt CodePipelineServiceRole.Arn
      Name: !Sub ${NameTagPrefix}-${Env}-${ServiceName}-pipeline
      ArtifactStore:
        Type: S3
        Location: !Ref ArtifactBucket
      Stages:
        - Name: Source
          Actions:
            - Name: SourceAction
              ActionTypeId:
                Category: Source
                Owner: AWS
                Version: '1'
                Provider: CodeCommit
              Configuration:
                RepositoryName: !Ref CodeCommitRepoName
                PollForSourceChanges: false
                BranchName: master
              RunOrder: 1
              OutputArtifacts:
                - Name: App
        - Name: Build
          Actions:
            - Name: Build
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: '1'
                Provider: CodeBuild
              Configuration:
                ProjectName: !Ref CodeBuildProject
              RunOrder: 1
              InputArtifacts:
                - Name: App
              OutputArtifacts:
                - Name: BuildOutput
        # - Name: Approval
        #   Actions:
        #     - Name: Manual_Approval
        #       ActionTypeId:
        #         Category: Approval
        #         Owner: AWS
        #         Version: '1'
        #         Provider: Manual
        #       Configuration:
        #         CustomData: !Sub '${ServiceName} will be updated. Do you want to deploy it?'
        #         NotificationArn: arn:aws:sns:ap-northeast-1:xxxxxxx:hogehoge
        - Name: Deploy
          Actions:
            - Name: Deploy
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Version: '1'
                Provider: CloudFormation
              InputArtifacts:
                - Name: BuildOutput
              Configuration:
                ActionMode: CREATE_UPDATE
                Capabilities: CAPABILITY_AUTO_EXPAND
                RoleArn: !GetAtt CFnServiceRole.Arn
                StackName: !Sub ${NameTagPrefix}-${Env}-${TemplateName}
                TemplatePath: !Sub 'BuildOutput::${TemplateName}.yml'
              RunOrder: 1
              Region: !Ref AWS::Region
# ------------------------------------------------------------#
# Outputs
# ------------------------------------------------------------#
Outputs:
  PipelinelogicalID:
    Description: logical ID.
    Value: !Ref Pipeline

最後に

本稿では、プレースホルダおよびCodeDeploy::BlueGreenフックを活用する CodePipeline の構築をする CFn を紹介しました。

これまで Fargate のBlue/Green デプロイで使われていたのは、プレースホルダ版で情報も多そうなので無難だと思いますが、若干クセはありつつもフック版もまずは開発環境から利用してみても良いのではないでしょうか。

そもそも Blue/Green デプロイをするかどうか、CodePipelineを使うかどうか、そして、どのパターンを選択するかなどの選択肢が広がったとしてご検討の上、ご活用下さい。

以上です。

どこかのどなたかのお役に立てば幸いです。

参考