CloudFormationでECSのBlue/Greenデプロイを構築（その３－CodeDeploy::BlueGreenフックをPipelineによる実現）

#概要
AWS ECS（Fargate）のサービスをBlue/Greenにデプロイして、Code Pipelineからコンテナイメージやアプリを変更させ、CodeBuildでdocker buildし、CodeDeployにてBlueからGreenに切り替えさせる手順を紹介する。
AWS ECS（Fargate）のサービスのFargate Blue/Greenのデプロイは、プレースホルダを利用する方法とCodeDeploy::BlueGreen フックを利用する方法2パータンがありますが、今回はCodeDeploy::BlueGreen フックをCodePipelineにより実現する方法を検証する。
CodeDeploy::BlueGreen フックを利用して実現するやり方は、下記の記事をご参照ください。

https://qiita.com/zhangsd/items/70283c4c980f2ea5a7c4

プレースホルダを利用してECS（Fargate）サービスのBlue/Green CICDを実現する方は、下記の記事をご参照ください

https://qiita.com/zhangsd/items/0573373726fc8a3d1fd7

やり方として、Transform というAWSのマクロ機能を利用し、CodeDeploy::BlueGreen フックにECSタスク、ECSサービス、ECSタスクセットを定義し、これらのリソースの更新は、CodePipelineに渡してBlue/Greenデプロイを実現する。
この考えで、二つのYmlファイルを作った。一つはCodeDeploy::BlueGreen フックとECSタスク、ECSサービス、ECSタスクセットを定義するhook-deploy.ymlファイル、もひとつはPipelineを定義するhook-pipeline.ymlである。

#環境用意
以下のリソースは事前に用意して置く。
S3パケット
ECRリポジトリ
VPC
Subnet1a
Subnet1c
SecurityGroup
Dockerfile

#実施プロシージャ
１．	CodeDeploy::BlueGreen フックとECSリソースを作成

CodeDeploy::BlueGreen フックを作成にはAWSのTransformを使う。Transform含むテンプレートはaws cloudformation create-stackコマンドでは実行できないのでご注意ください。その代わり、aws cloudformation packageコマンドでパッケージを作る。また、作れたパッケージはS3のバケットにインクルードするファイルをアップロードする必要があるので、S3パケットの事前用意も必要である。

aws cloudformation packageコマンドで作成されたCloudFormationテンプレートファイルを基に、CloudFormationのスタックを作成するには、aws cloudformation create-stackコマンドもdeployコマンドもできます。

!Refなどの短縮形の組み込み関数の利用やテンプレートファイルにTransformを入れる位置によるLocationファイル名を変換される場合のテンプレートを実行にはdeployで行う必要があるようで、本文ではaws cloudformation create-stackコマンドによる無事実行できた。

```hook-deploy.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Fargate Blue/Green deploy with CodeDeployBlueGreenHook
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Parameters:
  AwsTokyoRegion:
    Type: String
    Default: xxxxxxxx
  Service:
    Type: String
    Default: xxxx
  Env:
    Type: String
    Default: dev
  App:
    Type: String
    Default: hello

  ECRName:
    Type: String
    Default: ubuntu18
  ContainerName:
    Type: String
    Default: ununtu18-container1
  CodeCommitRepositoryName:
    Type: String
    Default: codecommit-ubuntu18

  NameTagPrefix:
    Type: String
    Default: hook
    Description: Prefix of Name tags.
  ServiceName:
    Type: String
    Default: xxxx
    Description: Prefix of Service tags.

  Vpc:
    Type: AWS::EC2::VPC::Id
    Default: 'vpc-xxxxxxxxxxxxxx'
  Subnet1a:
    Type: AWS::EC2::Subnet::Id
    Default: 'subnet-xxxxxxxxxxxxxx'
  Subnet1c:
    Type: AWS::EC2::Subnet::Id
    Default: 'subnet-xxxxxxxxxxxxxx'
  LBSecurityGroup:
    Type: AWS::EC2::SecurityGroup::Id
    Default: 'sg-xxxxxxxxxxxxxx'
  S3LogBucketName:
    Type: String
    Default: xxx-xxxx-dev-hook-s3-xxxxx

  TaskExecutionRoleArn:
    Type: String
    Default: 'arn:aws:iam::xxxxxxxxxxxx:role/ecsTaskExecutionRole'
  FamilyName:
    Type: String
    Default: fargate-cluster-task1
  ECSCluster:
    Type: String
    Default: 'arn:aws:ecs:xxxxxxxx:xxxxxxxxxxxx:cluster/fargate-cluster-1'

# ------------------------------------------------------------#
# Transform
# ------------------------------------------------------------#
Transform:
  - 'AWS::CodeDeployBlueGreen'
Hooks:
  CodeDeployBlueGreenHook:
    Properties:
      TrafficRoutingConfig:
        Type: TimeBasedLinear
        TimeBasedLinear:
          StepPercentage: 30
          BakeTimeMins: 1
        AdditionalOptions:
          TerminationWaitTimeInMinutes: 5
      Applications:
        - Target:
            Type: 'AWS::ECS::Service'
            LogicalID: ECSService
          ECSAttributes:
            TaskDefinitions:
              - BlueTaskDefinition
              - GreenTaskDefinition
            TaskSets:
              - BlueTaskSet
              - GreenTaskSet
            TrafficRouting:
              ProdTrafficRoute:
                Type: 'AWS::ElasticLoadBalancingV2::Listener'
                LogicalID: listenerProdTraffic
#              TestTrafficRoute:
#                Type: 'AWS::ElasticLoadBalancingV2::Listener'
#                LogicalID: listenerTestTraffic
              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 ContainerName
          Image: xxxxxxxxxxxx.dkr.ecr.xxxxxxxx.amazonaws.com/ubuntu18:latest
#          Image: xxxxxxxxxxxx.dkr.ecr.xxxxxxxx.amazonaws.com/docker-nginx:latest
          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
#        Type: CODE_DEPLOY

  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 ContainerName
          ContainerPort: 80
          TargetGroupArn: !Ref lbTargetGroupBlue

  PrimaryTaskSet:
    Type: 'AWS::ECS::PrimaryTaskSet'
    Properties:
      Cluster: !Ref ECSCluster
      Service: !Ref ECSService
      TaskSetId: !GetAtt
        - BlueTaskSet
        - Id
```

２. フックとECSリソース定義ファイルからCloudFormationテンプレートファイルを作成
CodeDeploy::BlueGreen フックとECSリソース定義ファイルからCloudFormationテンプレートファイルを作成すうには--output-template-fileで指定したファイルに変換後の内容が出力される（今回は出力後のテンプレートファイルをcf-deploy-template.ymlとする。）

```hook-deploy.sh
#!/bin/bash
aws cloudformation package --template-file ./hook-deploy.yml --s3-bucket xxx-xxxx-dev-hook-s3-xxxx --output-template-file cf-deploy-template.yml
```

```cf-deploy-template.yml
AWSTemplateFormatVersion: 2010-09-09
Description: Fargate Blue/Green deploy with CodeDeployBlueGreenHook
Parameters:
  AwsTokyoRegion:
    Type: String
    Default: xxxxxxxx
  Service:
    Type: String
    Default: xxxx
  Env:
    Type: String
    Default: dev
  App:
    Type: String
    Default: hello
  ECRName:
    Type: String
    Default: ubuntu18
  ContainerName:
    Type: String
    Default: ununtu18-container1
  CodeCommitRepositoryName:
    Type: String
    Default: codecommit-ubuntu18
  CodeDeployAppName:
    Type: String
    Default: AppECS-fargate-cluster-1-fargate-cluster-service1
  CodeDeployGrpName:
    Type: String
    Default: DgpECS-fargate-cluster-1-fargate-cluster-service1
  NameTagPrefix:
    Type: String
    Default: hook
    Description: Prefix of Name tags.
  ServiceName:
    Type: String
    Default: xxxx
    Description: Prefix of Service tags.
  Vpc:
    Type: AWS::EC2::VPC::Id
    Default: vpc-xxxxxxxxxxxx
  Subnet1a:
    Type: AWS::EC2::Subnet::Id
    Default: subnet-xxxxxxxxxxxx
  Subnet1c:
    Type: AWS::EC2::Subnet::Id
    Default: subnet-xxxxxxxxxxxx
  LBSecurityGroup:
    Type: AWS::EC2::SecurityGroup::Id
    Default: sg-xxxxxxxxxxxx
  S3LogBucketName:
    Type: String
    Default: xxx-xxxx-dev-hook-s3-xxxxxx
  TaskExecutionRoleArn:
    Type: String
    Default: arn:aws:iam::xxxxxxxxxxxx:role/ecsTaskExecutionRole
  FamilyName:
    Type: String
    Default: fargate-cluster-task1
  ECSCluster:
    Type: String
    Default: arn:aws:ecs:xxxxxxxx:xxxxxxxxxxxx:cluster/fargate-cluster-1
Transform:
- AWS::CodeDeployBlueGreen
Hooks:
  CodeDeployBlueGreenHook:
    Properties:
      TrafficRoutingConfig:
        Type: TimeBasedLinear
        TimeBasedLinear:
          StepPercentage: 30
          BakeTimeMins: 1
        AdditionalOptions:
          TerminationWaitTimeInMinutes: 5
      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:
  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:
        Fn::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: ContainerName
        Image: xxxxxxxxxxxx.dkr.ecr.xxxxxxxx.amazonaws.com/ubuntu18:latest
        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: ContainerName
        ContainerPort: 80
        TargetGroupArn:
          Ref: lbTargetGroupBlue
  PrimaryTaskSet:
    Type: AWS::ECS::PrimaryTaskSet
    Properties:
      Cluster:
        Ref: ECSCluster
      Service:
        Ref: ECSService
      TaskSetId:
        Fn::GetAtt:
        - BlueTaskSet
        - Id
```

３. ECSタスク定義などのDeployスタックを作成

作成されたcf-tするにフックとECSリソース定義ファイルからCloudFormationテンプレートファイルを作成する。
このテンプレートファイルには IAM リソースが含まれるので CAPABILITY_IAM を指定する必要がある。さらにマクロを使って変更セットを作成するのでCAPABILITY_AUTO_EXPANDの指定も必要である。

```dly.sh
#!/bin/bash
CHANGESET_OPTION="--no-execute-changeset"
#if [ $# = 1 ] && [ $1 = "deploy" ]; then
if [ $1 = "deploy" ]; then
echo "deploy mode - create-stack..."
CHANGESET_OPTION=""
fi
#CFN_TEMPLATE="./nlb.yml"
CFN_TEMPLATE=$2
CFN_STACK_NAME=hook-deploy
# テンプレートの実⾏
aws cloudformation create-stack --stack-name ${CFN_STACK_NAME} --template-body file://./${CFN_TEMPLATE} ${CHANGESET_OPTION} \
    --capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND
```

./dly.sh deploy cf-deploy-template.yml


４. CodePipelineスタックを作成

以下のYmlファイルとaws cliによるCloudFormationのCodePipelineを作成する。

```hook-pipeline.yml
AWSTemplateFormatVersion: 2010-09-09
Description: CodePipeline For ECS Fargate Blue/Green Deploy with CodeDeploy Hook
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Parameters:
  AwsTokyoRegion:
    Type: String
    Default: xxxxxxxx
  Service:
    Type: String
    Default: xxxx
  Env:
    Type: String
    Default: dev
  App:
    Type: String
    Default: hello

  ECRName:
    Type: String
    Default: ubuntu18
  TemplateName:
    Type: String
    Default: cf-template
  ContainerName:
    Type: String
    Default: ununtu18-container1
  CodeCommitRepositoryName:
    Type: String
    Default: codecommit-ubuntu18

  ServiceName:
    Type: String
    Default: xxxx
    Description: Prefix of Service tags.

# ------------------------------------------------------------#
# Resources
# ------------------------------------------------------------#

Resources:

  # CodeWatchEventを実行できるIAMRole
  CloudwatchEventRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub  xxx-cwe-${Service}-${Env}-hook-role
      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 xxx-codebuild-${Service}-${Env}-hook-role
      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 xxx-cfn-${Service}-${Env}-hook-role
      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 xxx-codepipeline-${Service}-${Env}-hook-role
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: codepipeline.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: xxx-CodePipelineHookPolicy
          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

  # CodeDeployに適用するIAMRole
  CodeDeployRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: 'Allow'
            Principal:
              Service:
                - 'codedeploy.amazonaws.com'
            Action:
              - 'sts:AssumeRole'
      Path: '/'
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS
        - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser
      RoleName: !Sub xxx-codedeploy-${Service}-${Env}-hook-role

  # 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 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 xxx-${ServiceName}-hook-pipeline1
      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-xxxxxxxx:xxxxxxxx:hogehoge
        - Name: Deploy
          Actions:
            - Name: Deploy
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Version: '1'
                Provider: CloudFormation
              InputArtifacts:
#                - Name: App
                - Name: BuildOutput
              Configuration:
                ActionMode: CREATE_UPDATE
                Capabilities: CAPABILITY_AUTO_EXPAND
                RoleArn: !GetAtt CFnServiceRole.Arn
#                StackName: hook-pipeline
                StackName: hook-deploy
                TemplatePath: !Sub 'BuildOutput::${TemplateName}.yml'
              RunOrder: 1
              Region: !Ref AWS::Region

# ------------------------------------------------------------#
# Outputs
# ------------------------------------------------------------#
Outputs:
  PipelineDev:
    Description: Dev xxxx Pipeline
    Value: !Ref Pipeline
    Export:
      Name: hook-DevxxxxAppPipeline
```


```pip.sh
#!/bin/bash
CHANGESET_OPTION="--no-execute-changeset"
#if [ $# = 1 ] && [ $1 = "deploy" ]; then
if [ $1 = "deploy" ]; then
echo "deploy mode"
CHANGESET_OPTION=""
fi
#CFN_TEMPLATE="./nlb.yml"
CFN_TEMPLATE=$2
CFN_STACK_NAME=hook-pipeline![hook-pipeline-hello-blue.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1657390/2916287f-ae7b-b582-9626-57ff64a3264b.jpeg)

# テンプレートの実⾏
 aws cloudformation deploy --stack-name ${CFN_STACK_NAME} --template-file ${CFN_TEMPLATE} ${CHANGESET_OPTION} \
    --capabilities CAPABILITY_NAMED_IAM --parameter-overrides `cat parameters.hook`
```

CloudFormationスタックを作成するコマンドは下記になる。
./pip.sh deploy hook-pipeline.yml

このテンプレートには IAM リソースが含まれるので CAPABILITY_IAM を指定する必要がある。また、テンプレートにマクロを使って変更セットを作成するのでCAPABILITY_AUTO_EXPANDの指定も必要です

#結果確認

1.	現用系(ブルー)アプリへのアクセス
スタックが作成されたら、ECSのサービス及びタスクを調べて、作成されたコンテナまたはALBのDNS名にがアクセスしてみると、以下のメッセージが表示される。

http://hook-dev-hello-alb-xxxxxxxx.xxxxxxxx-2.elb.amazonaws.com/

   ![hook-pipeline-hello-blue.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1657390/c202d05e-cdba-c71a-73ee-713f2cf945b5.jpeg)

２．開発系（グリーン）アプリを作成（タスク変更）
TaskDefinition に変更が加えられたらそれがフックされ、デプロイが走るので、開発系のコンテナに切り替えます。

CodeCommitレポジトリのDockerfile中身を変更する。

RUN echo 'Hello World - BLUE!' > /var/www/html/index.htmlから
RUN echo 'Hello World - GREEN!' > /var/www/html/index.htmlへ変更。

   ![hook-pipeline-codecommit-ubuntu18.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1657390/22bd0ac5-5536-8e1c-8f51-8776f5ec7b2c.jpeg)

3．CodeDeploymentの実施と開発系（グリーン）に切り替え

実行すると、CloudFormation のマクロが変更セットを作成して、CodeDeploy や グリーン系への切り替えなどを始めます。

   ![hook-pipeline-deploy.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1657390/223fe4bf-4c81-c77e-1ffb-9d3ecb45e7a5.jpeg)

４．開発系（グリーン）へのアクセス
開発系のコンテナに切り替え後、再び。ALBのDNS名にがアクセスしてみると、以下のメッセージが表示される。

http://hook-dev-hello-alb-xxxxxxxx.xxxxxxxx-2.elb.amazonaws.com/

   ![hook-pipeline-hello-green.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1657390/71b86573-a0bd-84ec-0e58-463ee05f7785.jpeg)

５．（オプション）update-stackによる開発系（グリーン）への切り替え
“２．開発系（グリーン）アプリを作成（タスク変更）”の他、deployスタックのアップデートによる開発系アプリの作成及びグリーン系への切り替えもできる。

```dly-update.sh
#!/bin/bash
CHANGESET_OPTION="--no-execute-changeset"
#if [ $# = 1 ] && [ $1 = "deploy" ]; then
if [ $1 = "deploy" ]; then
echo "deploy mode - update-stack..."
CHANGESET_OPTION=""
fi
#CFN_TEMPLATE="./nlb.yml"
CFN_TEMPLATE=$2
CFN_STACK_NAME=hook-deploy
# テンプレートの実⾏
aws cloudformation update-stack --stack-name ${CFN_STACK_NAME} --template-body file://./${CFN_TEMPLATE} ${CHANGESET_OPTION} \
    --capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND
```

#所感
今回、CloudFormationの CodePipelineとCodeDeploy::BlueGreenフックをインテグレートにより、ECS FargateのBlue/Greenサービスデプロイを構築検証した。テスク定義の変更によるECSのBule/Green　CodeDeployが動くことを確認できた。ただ、CodeDeploy::BlueGreenフックリソース定義、CloudFormationテンプレートファイルの作成及びCodePipelineの作成はちょっと手間をかかり、実際複雑な商用システムでは、CodeDeploy::BlueGreenフックとCodePipelineの連携する方法は柔軟性に十分ではないかと思います。