CloudFormationでECSのBlue/Greenデプロイを構築（その１ーCodeDeploy::BlueGreenフック方法）

#概要

AWS の公式ユーザーガイドにより、CloudFormation を使用して、CodeDeploy による ECS ブルー/グリーンデプロイを実行することができます。

https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/blue-green.html

しかし、条件としては「CloudFormation を通じて CodeDeploy を使用して ECS ブルー/グリーンデプロイを実行するには、テンプレートに Amazon ECS サービスとロードバランサーなど、デプロイをモデル化するリソースを含める必要があります。」

また、現用系（ブルー、Prod）から開発環境（グリーン、Test）への切り替えトリガーは、「次の ECS リソースの置き換えが必要なプロパティを更新するスタック更新を実行すると、CloudFormation はグリーンデプロイを開始します。」

AWS::ECS::TaskDefinition
AWS::ECS::TaskSet

ここで、Transform というAWSから提供されるマクロの機能とCodeDeploy::BlueGreenフックを使って ECS の Blue/Green の環境を作成して検証します。

以下は注意するべきこと。

１．ECSサービスやALB、タスク、コンテナに等は、事前に作成しておいたり、他のスタックで作成したものを使うことはできません。これらのすべてリソースは、Transformと同じテンプレート内で定義する必要がある。
２．CodeDeploy と連携させて実現していた ECS のServiceのDeploymentControllerタイプは、Externalなデプロイメントコントローラーとして使用する。
３．CodeDeployのところに、アプリケーションやデプロイグループが作成されません

#環境用意
ECRリポジトリ
VPC
Subnet1a
Subnet1c
SecurityGroup
Dockerfile

#検証方法
##１．現用系（ブルー）アプリを作成
Ubuntu18コンテナにイメージファイル。コンテナにが実行されたらWebBrowserに“Hello World！”を表示される。

```
FROM ubuntu:18.04
-Install dependencies
RUN apt-get update && \
 apt-get -y install apache2
- Install apache and write hello world message
RUN echo 'Hello World!' > /var/www/html/index.html
- Configure apache
RUN echo '. /etc/apache2/envvars' > /root/run_apache.sh && \
 echo 'mkdir -p /var/run/apache2' >> /root/run_apache.sh && \
 echo 'mkdir -p /var/lock/apache2' >> /root/run_apache.sh && \ 
 echo '/usr/sbin/apache2 -D FOREGROUND' >> /root/run_apache.sh && \ 
 chmod 755 /root/run_apache.sh
EXPOSE 80
CMD /root/run_apache.sh
```

## 2.グリーン系：
Nginxコンテナにイメージファイル。コンテナにが実行されたらWebBrowserに“Welcome to nginx!...”を表示される。

デモテンプレート(demo-template.yml)
Fargate で Ununtu18 コンテナを動かして、ALB経由でHTTP 80番ポートでアクセスするの“HelloWorld！”メッセージを作成する。

```yaml
AWSTemplateFormatVersion: 2010-09-09
Description: Fargate Blue/Green deploy with CodeDeployBlueGreenHook
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------#
Parameters:
  AwsTokyoRegion:
    Type: String
    Default: xxxxxxxx
  Service:
    Type: String
    Default: system
  Env:
    Type: String
    Default: dev
  App:
    Type: String
    Default: hello
  ECRName:
    Type: String
    Default: xxxxxx
  ContainerName:
    Type: String
    Default: xxxxxxxxxxx1
  CodeCommitRepositoryName:
    Type: String
    Default: xxxxxxxxxxx
  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-xxxxxxxxxxx'
  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: xxxxxxxxxxxx

  TaskExecutionRoleArn:
    Type: String
    Default: 'arn:aws:iam::xxxxxxxxxxxx:role/ecsTaskExecutionRole'
  FamilyName:
    Type: String
    Default: xxxxxxxxxxx1
  ECSCluster:
    Type: String
    Default: 'arn:aws:ecs:xxxxxxxx:xxxxxxxxxxxx:cluster/xxxxxxxxxxx-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/xxx-ubuntu18:latest
#          Image: xxxxxxxxxxxx.dkr.ecr.xxxxxxxx.amazonaws.com/xxx-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

```

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

CloudFormationスタックを作成するコマンドは下記になる。

```
#!/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} ${CHANGES
ET_OPTION} \
        --capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND

```
./dly.sh deploy xxxx-hook-deploy.yml

スタックが作成されたら、ECSのサービス及びタスクを調べて、作成されたコンテナまたはALBのDNS名にがアクセスしてみると、以下のメッセージが表示される。

http://xxxxxxxxxxx-alb-xxxxxxxx.xxxxxxxx.elb.amazonaws.com/

![hook-deployment-screen1.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1657390/2e480554-5256-5f87-29d7-17ced4d35a66.jpeg)


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

TaskDefinitionを変更する。

```yaml
 BlueTaskDefinition:
    Type: 'AWS::ECS::TaskDefinition'
    Properties:
      ExecutionRoleArn: !Ref TaskExecutionRoleArn
      ContainerDefinitions:
        - Name: !Ref ContainerName
#          Image: xxxxxxxxxxx.dkr.ecr.xxxxxxxx.amazonaws.com/xxx-ubuntu18:latest
          Image: xxxxxxxxxxx.dkr.ecr.xxxxxxxx.amazonaws.com/xxx-nginx:latest
          Essential: true
          PortMappings:
            - HostPort: 80
              Protocol: tcp
              ContainerPort: 80
      RequiresCompatibilities:
        - FARGATE
      NetworkMode: awsvpc
      Cpu: '256'
      Memory: '512'
      Family: !Ref FamilyName

```

変更スタックを作成するコマンドは下記になる。

```
#!/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 update-stack --stack-name ${CFN_STACK_NAME} --template-body file://./${CFN_TEMPLATE} ${CHANGES
ET_OPTION} \
        --capabilities CAPABILITY_NAMED_IAM CAPABILITY_AUTO_EXPAND
```

./dly-update.sh deploy xxxx-hook-deploy.yml

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

![hook-deployment.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1657390/3ea1b325-fc2c-ab20-1f8f-d75d0bc79f2e.jpeg)

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

http://xxxxxxxxxxx-alb-xxxxxxxxxxx.xxxxxxxx.elb.amazonaws.com/

![hook-deployment-screen13.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1657390/5c9b2a51-5dad-cb55-4689-10a8ea026073.jpeg)


##5．ブルー系の停止

５分間のCodeDeploy　TerminationWaitTimeInMinutes待ってから、ブルー系コンテナが停止され、コンテナにアクセスすると返事がありません。


# まとめ
今回、CodeDeploy::BlueGreenフックを使って、簡単なWebアプリ今回を経由してCloudFormationでECSのBlue/Greenデプロイを構築検証した。AWS公式ユーザーガイドとおり、ECSのBule/Green　CodeDeployが動くことを確認できました。この検証では、最初に説明したようにECSサービスやALB、タスク、コンテナに等は、事前に作成しておいたり、他のスタックで作成したものを使うことはできない、これらのすべてリソースは、Transformと同じテンプレート内で定義する必要があるため、実際複雑な商用システムでは、柔軟性は十分ではないかと思います。