ECS/FargateのBlue/Greenデプロイ環境を自動構築する

# 背景

今年のゴールデンウィーク頃、コロナ騒ぎで外出できなかったので、自宅でフロントエンド系(Vue)のお勉強をしていました。

その際、Vue(nodejs)で作ったWebアプリをAWS上に自動デプロイ(ECS/FargateのBlue/Greenデプロイ)する仕組みと、その環境をワンコマンドでセットアップ/クリーナップ​する仕組みを構築したので、そのやり方を思い出しながら書いてみます。

# Webアプリについて

本題からは外れますが、軽くWebアプリを紹介しておきます。

作ったWebアプリは、ブラウザ上でオンライン対戦できるカードゲーム(ババ抜き)です。

![ダウンロード (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/c7cf17f1-da72-bc6f-27a6-1151d19855f8.gif)


プログラムの構成はこんな感じ

* フロントエンドは、Node.js(Vue.js/typescript)で作成​
* バックエンドは、Node.js(Express/typescript)で作成​
* フロントエンド-バックエンド間は、Web-Socketで通信 ​

コードはここに置いておきました。

* バックエンド (game-server)
https://github.com/yusuke-ka/game-server

* フロントエンド (game-ui)
https://github.com/yusuke-ka/game-ui

ちなみに、Vueどころか、フロントエンド開発自体ほとんどやったことがなかったので、最終的に出来上がったコードも、上級者から見ると、幼稚なものに仕上がっているかと思います。

なお、トランプの絵などの素材はこちらのフリー素材を利用させていただきました。
https://www.irasutoya.com/
https://chicodeza.com/freeitems/torannpu-illust.html

※個人の学習目的で作成したプログラム内の素材として利用しています。

# AWSの構成

デプロイ後の構成はこんな感じ。

 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/2bcaa50a-3b80-8e63-ee8e-829d7c718348.png)

* ALBは一つを共有して、ポートで分ける。
 * Code-DeployのB/GデプロイもALB共有状態で問題なく実行できた。
* フロントエンドからバックエンドへのリクエストはALBを経由(というかブラウザからリクエスト)​
 * フロントエンドはALBのURLを知っておく必要がある​
* タスク数は1。​
 * デプロイ時のみB/Gなので2個に増える​
* CloudFrontとS3はおまけ​
 * フロントエンド(http-server)の静的ファイルと同じものをS3においてホスティングしてみた。​
 * 前段にCloudFrontを置く構成もやってみた​

# 自動デプロイの構成

game-serverとgame-uiのリポジトリをAWSのCodeCommitに作成し、それぞれコミットしたら自動的にデプロイされるようにした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/dae95194-d7f0-7aac-ca1e-14b4288914d6.png)

CodeCommitのコミットイベントからCodePipelineが起動され、まず、CodeBuildが実行される。

CodeBuildの設定ファイル(buildspec.yml)は、game-serverやgame-uiのリポジトリにプログラムのソースと一緒に入っている。

以下はgame-serverのプロジェクト直下に置いたbuildspec.yml。
({AWSのアカウントID}部分は念のため伏せています。)

```buildspec.yml
version: 0.2

phases:
  install:
    runtime-versions:
      docker: 18
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - aws --version
      - $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)
      - REPOSITORY_URI={AWSのアカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/game-server
      - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
      - IMAGE_TAG=${COMMIT_HASH:=latest}
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...
      - docker build -t $REPOSITORY_URI:latest .
      - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker images...
      - docker push $REPOSITORY_URI:latest
      - docker push $REPOSITORY_URI:$IMAGE_TAG
      - echo Writing image definitions file...
      - printf '{"Version":"1.0","ImageURI":"%s"}' $REPOSITORY_URI:$IMAGE_TAG > imageDetail.json
artifacts:
    files:
      - imageDetail.json
```

以下はUI側（game-ui）用のbuildspec.yml。

```buildspec.yml
version: 0.2

env:
  parameter-store:
    ALB_URL: "/game/ALBDNSName"

phases:
  install:
    runtime-versions:
      docker: 18
  pre_build:
    commands:
      - npm update -g npm
      - npm install
      - echo 'VUE_APP_SERVER='$ALB_URL > .env
      - echo Building the S3 files...
      - npm run build
      - echo Logging in to Amazon ECR...
      - aws --version
      - $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)
      - REPOSITORY_URI={AWSのアカウントID}.dkr.ecr.ap-northeast-1.amazonaws.com/game-ui
      - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
      - IMAGE_TAG=${COMMIT_HASH:=latest}
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...
      - docker build -t $REPOSITORY_URI:latest . --build-arg ALB_URL=$ALB_URL
      - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker images...
      - docker push $REPOSITORY_URI:latest
      - docker push $REPOSITORY_URI:$IMAGE_TAG
      - echo Writing image definitions file...
      - printf '{"Version":"1.0","ImageURI":"%s"}' $REPOSITORY_URI:$IMAGE_TAG > imageDetail.json
      - echo upload s3
      - aws s3 sync --exact-timestamps --delete ./dist/ s3://game-ui-s3-hosting
artifacts:
  files:
    - imageDetail.json
```

yarnでなく、npmを使っているのは、yarn install中のファイルのダウンロードが時間内に終わらなくて、「unexpected end of file」のエラーが発生していたため。
手元なら再度yarn installすれば続きからやってくれるが、CodeBuildにリトライさせるのも微妙なので、とりあえずnpmに変更した。(たぶん他にやりようはあるはず。)

その他、パラメータストアからALBのURLを取得して、docker buildの引数として渡すようなコードが入っている。
あらかじめ用意したALBを使い続けるなら、こんな設定はいらないが(game-uiの.envファイルに最初から書いておけばよい)、
お金節約のため、ALB作成部分をCFn化して、毎日消して、再作成できるようにしたので、こんなことになっている。
パラメータストアに設定するあたりは、CFnでやっているので、後で説明する。

ちなみに、おまけで用意したS3ホスティング用のファイルはここ(CodeBuild)でsyncさせている。

docker buildのコマンドで使われるDockerfileは以下。

```Dockerfile
FROM node:lts-alpine
WORKDIR /app
COPY package*.json ./
RUN yarn install
COPY . .
RUN yarn build
EXPOSE 3000
CMD [ "yarn", "start" ]
```

上記はバックエンドサービス用で、フロントエンド側は以下となる。

```Dockerfile
FROM node:lts-alpine
ARG ALB_URL
RUN npm install -g http-server
WORKDIR /app
COPY package*.json ./
RUN yarn install
COPY . .
RUN echo 'VUE_APP_SERVER='$ALB_URL > .env
RUN yarn build
EXPOSE 80
CMD [ "http-server", "dist", "-p", "80" ]
```

バックエンド側はnodejsで起動するが、フロントエンド側のgame-uiはビルドで静的ファイルが生成されるので、http-serverで配信する。

また、UI側はバックエンドサービスのURL(ALBのURL)を知っておく必要があるので、ビルド前に以下の一文を追加している。
「RUN echo 'VUE_APP_SERVER='$ALB_URL > .env」
buildspec.yml側でdocker buildコマンドを呼び出す際に引数として渡したALB_URLを.envファイルに書き出している。
vueで書かれたgame-uiのプログラムが.envに書かれた環境変数を読み取って、バックエンド側にアクセスする。

buildspec.ymlに戻って、
結果のアーティファクトとなるimageDetail.jsonには、ECRに登録されたDockerイメージのURIが記載されていて、これを次のCodeDeployが読み取り＆ECSのタスク定義に組み込んで、あらかじめ用意しておいたECS/Fargateのサービス上にBlue/Greenデプロイする。

CodeDeploy用の設定ファイルappspec.yamlとECSのタスク定義の設定ファイルtaskdef.jsonは、game-serverなどの実プログラムを入れたリポジトリとは別に用意したリポジトリに入れている。(バックエンド用、フロントエンド用で、それぞれ、infra-serverとinfra-uiというリポジトリ名にした)

CodeDeployが読み取るappspec.yamlは下記。

```appspec.yaml
version: 0.0
Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: <TASK_DEFINITION>
        LoadBalancerInfo:
          ContainerName: "game-server"
          ContainerPort: 3000
```

ECS用の設定になっている。
上記はgame-server用。game-ui側は下記。

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

*＜TASK_DEFINITION＞*部分には、パイプライン実行時に実際のタスク定義がセットされる。

続いてタスク定義用の設定ファイル(taskdef.json)。
まずはバックエンド用。

```taskdef.json
{
    "executionRoleArn": "arn:aws:iam::{AWSのアカウントID}:role/ecsTaskExecutionRole",
    "containerDefinitions": [
        {
            "name": "game-server",
            "image": <IMAGE1_NAME>,
            "essential": true,
            "portMappings": [
                {
                    "hostPort": 3000,
                    "protocol": "tcp",
                    "containerPort": 3000
                }
            ]
        }
    ],
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "networkMode": "awsvpc",
    "cpu": "256",
    "memory": "512",
    "family": "game-server"
}
```

続いてフロントエンド用。

```taskdef.json
{
    "executionRoleArn": "arn:aws:iam::{AWSのアカウントID}:role/ecsTaskExecutionRole",
    "containerDefinitions": [
        {
            "name": "game-ui",
            "image": <IMAGE1_NAME>,
            "essential": true,
            "portMappings": [
                {
                    "hostPort": 80,
                    "protocol": "tcp",
                    "containerPort": 80
                }
            ]
        }
    ],
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "networkMode": "awsvpc",
    "cpu": "256",
    "memory": "512",
    "family": "game-ui"
}
```

*＜IMAGE1_NAME＞*部分には、パイプライン実行時に実際のイメージ名がセットされる。

あとは、appspec.yamlの設定を読み込んだCodeDeployがよしなにやってくれる。

CodePipelineやCodeDeploy自体の構築は、CFnやCLIで書いたので次の章に記載する。

AWSのマネジメントコンソール上でパイプラインを設定する際の方法は下記を参照(AWS公式のチュートリアル)。
https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/tutorials-ecs-ecr-codedeploy.html
(このチュートリアルの手順はCodeBuildによるイメージ作成がない代わりに、あらかじめECRに登録しておいたイメージをソースとして読み込んで利用する感じ。)

上記チュートリアルはマネジメントコンソール上で必要な環境を用意していく感じだが、今回はこれらをCFnとAWS Cliを使ってまとめて作成する感じにしたので、以降でそれについて記載する。

# AWS環境セットアップ/クリーンナップ

これまで記載してきた環境をCFnとAWS Cliを使って、一括作成/一括削除するスクリプトを作成したので、それについて記載する。
(個人用のAWSなので無駄にお金を使いたくなかったというのが、これを作成したモチベーション)

一括作成のスクリプトのイメージは下記の通り。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/3af81e6a-c24b-9e5f-a8bd-a1f58cb33604.png)

なお、一括削除のスクリプトは、上記とは逆順に消していく感じになっている。

最初は、全部CFnで対応しようとしたが、この時点(2020/5/1)のCFnではcode-deployのB/Gデプロイがlambdaしか対応していないので、code-deployだけは、シェルでAWS-CLIを呼び出して作成​

各スタック(とシェル)間のデータのやりとりはパラメータストアで実施​している。

## setup.sh

まずは、一括作成用のスクリプト。

```setup.sh
#!/bin/sh

echo 'setup start'
echo 'create network stack'
aws cloudformation create-stack --stack-name network-stack --region ap-northeast-1 --template-body file://cfn-network-infra-template.yml
aws cloudformation wait stack-create-complete --stack-name network-stack
echo 'network stack created'
echo 'create ecs stack'
aws cloudformation create-stack --stack-name ecs-stack --region ap-northeast-1 --template-body file://cfn-ecs-infra-template.yml
aws cloudformation wait stack-create-complete --stack-name ecs-stack
echo 'ecs stack created'
echo 'create s3 stack'
aws cloudformation create-stack --stack-name s3-stack --region ap-northeast-1 --template-body file://cfn-s3-infra-template.yml
aws cloudformation wait stack-create-complete --stack-name s3-stack
echo 's3 stack created'
echo 'create code-deploy'
sh ./create-codedeploy.sh
echo 'codedeploy created'
echo 'create cicd stack'
aws cloudformation create-stack --stack-name cicd-stack --region ap-northeast-1 --template-body file://cfn-cicd-infra-template.yml
aws cloudformation wait stack-create-complete --stack-name cicd-stack
echo 'cicd stack created'
echo 'setup finish'
echo 'ALB URL :'
echo `aws ssm get-parameter --name /game/ALBDNSName --query "Parameter.Value" --output text`
echo 'Website URL :'
echo `aws ssm get-parameter --name /game/WebsiteURL --query "Parameter.Value" --output text`
```

各スタック作成の完了を待って、次のスタックを実行している。
最後にアクセス先のURLを出力している。

## cfn-network-infra-template.yml

setup.shから最初に呼び出されるのは、ALBなど、ネットワーク環境を構築するテンプレート。
VPCやサブネットなどは、ほぼお金がかからないので、あらかじめ作成しておいたものを利用(削除しない)。
(セキュリティグループは、80,8080,3000,3001ポートを開けておく必要がある)

```cfn-network-infra-template.yml
AWSTemplateFormatVersion: "2010-09-09"
Description:
  Network Infra Create (TargetGroup ALB)

# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------# 
Parameters:
  PJPrefix:
    Type: String
    Default: "game"

#Network
  VpcId:
    Type: String
    Default: "vpc-xxxxxx"

  SecurityGroupId:
    Type: String
    Default: "sg-xxxxxx"

  SubnetIdA:
    Type: String
    Default: "subnet-xxxxxx"

  SubnetIdC:
    Type: String
    Default: "subnet-xxxxxx"

Resources:

# ------------------------------------------------------------#
#  Target Group
# ------------------------------------------------------------#
  TargetGroupForUi1: 
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Properties: 
      VpcId: !Ref VpcId
      Name: !Sub "${PJPrefix}-tg-ui-1"
      Protocol: "HTTP"
      Port: 80
      TargetType: "ip"
      Matcher: 
        HttpCode: 200
    DependsOn: InternetALB

  TargetGroupForUi2: 
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Properties: 
      VpcId: !Ref VpcId
      Name: !Sub "${PJPrefix}-tg-ui-2"
      Protocol: "HTTP"
      Port: 8080
      TargetType: "ip"
      Matcher: 
        HttpCode: 200
    DependsOn: InternetALB

  TargetGroupForServer1: 
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Properties: 
      VpcId: !Ref VpcId
      Name: !Sub "${PJPrefix}-tg-server-1"
      Protocol: "HTTP"
      Port: 3000
      TargetType: "ip"
      Matcher: 
        HttpCode: 200
    DependsOn: InternetALB

  TargetGroupForServer2: 
    Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
    Properties: 
      VpcId: !Ref VpcId
      Name: !Sub "${PJPrefix}-tg-server-2"
      Protocol: "HTTP"
      Port: 3001
      TargetType: "ip"
      Matcher: 
        HttpCode: 200
    DependsOn: InternetALB

# ------------------------------------------------------------#
#  ALB
# ------------------------------------------------------------#
  InternetALB:
    Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
    Properties: 
      IpAddressType: "ipv4"
      Name: !Sub "${PJPrefix}-alb"
      Scheme: "internet-facing"
      SecurityGroups: 
        - !Ref SecurityGroupId
      Subnets: 
        - !Ref SubnetIdA
        - !Ref SubnetIdC
      Type: "application"

  ALBListenerForUi1: 
    Type: "AWS::ElasticLoadBalancingV2::Listener"
    Properties: 
      DefaultActions: 
        - TargetGroupArn: !Ref TargetGroupForUi1
          Type: "forward"
      LoadBalancerArn: !Ref InternetALB
      Port: 80
      Protocol: "HTTP"

  ALBListenerForUi2: 
    Type: "AWS::ElasticLoadBalancingV2::Listener"
    Properties: 
      DefaultActions: 
        - TargetGroupArn: !Ref TargetGroupForUi2
          Type: "forward"
      LoadBalancerArn: !Ref InternetALB
      Port: 8080
      Protocol: "HTTP"

  ALBListenerForServer1: 
    Type: "AWS::ElasticLoadBalancingV2::Listener"
    Properties: 
      DefaultActions: 
        - TargetGroupArn: !Ref TargetGroupForServer1
          Type: "forward"
      LoadBalancerArn: !Ref InternetALB
      Port: 3000
      Protocol: "HTTP"

  ALBListenerForServer2: 
    Type: "AWS::ElasticLoadBalancingV2::Listener"
    Properties: 
      DefaultActions: 
        - TargetGroupArn: !Ref TargetGroupForServer2
          Type: "forward"
      LoadBalancerArn: !Ref InternetALB
      Port: 3001
      Protocol: "HTTP"

# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#      

  ALBDNSName:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /game/ALBDNSName
      Type: String
      Value: !GetAtt InternetALB.DNSName

  ALBName:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /game/ALBName
      Type: String
      Value: !GetAtt InternetALB.LoadBalancerName
  
  TargetGroupUi1Arn:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /game/TargetGroupUi1Arn
      Type: String
      Value: !Ref TargetGroupForUi1
  
  TargetGroupUi2Arn:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /game/TargetGroupUi2Arn
      Type: String
      Value: !Ref TargetGroupForUi2
  
  TargetGroupServer1Arn:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /game/TargetGroupServer1Arn
      Type: String
      Value: !Ref TargetGroupForServer1
  
  TargetGroupServer2Arn:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /game/TargetGroupServer2Arn
      Type: String
      Value: !Ref TargetGroupForServer2 

  TargetGroupUi1Name:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /game/TargetGroupUi1Name
      Type: String
      Value: !GetAtt TargetGroupForUi1.TargetGroupName
  
  TargetGroupUi2Name:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /game/TargetGroupUi2Name
      Type: String
      Value: !GetAtt TargetGroupForUi2.TargetGroupName
  
  TargetGroupServer1Name:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /game/TargetGroupServer1Name
      Type: String
      Value: !GetAtt TargetGroupForServer1.TargetGroupName
  
  TargetGroupServer2Name:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /game/TargetGroupServer2Name
      Type: String
      Value: !GetAtt TargetGroupForServer2.TargetGroupName

  ALBListenerUi1Arn:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /game/ALBListenerUi1Arn
      Type: String
      Value: !Ref ALBListenerForUi1
  
  ALBListenerUi2Arn:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /game/ALBListenerUi2Arn
      Type: String
      Value: !Ref ALBListenerForUi2
  
  ALBListenerServer1Arn:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /game/ALBListenerServer1Arn
      Type: String
      Value: !Ref ALBListenerForServer1
  
  ALBListenerServer2Arn:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /game/ALBListenerServer2Arn
      Type: String
      Value: !Ref ALBListenerForServer2
```

他のCFnスタックが、ここで作成したALBの情報が利用できるように、最後のOutput Parameters部分でパラメータストアに格納しておく。

パラメータストアを利用しているのは、CFn化できなかったCodeDeploy作成のシェルからの呼び出しが容易だったから。

なお、ALBDNSNameとして保存したデータは、CodeBuildでgame-ui側をビルドする際に、バックエンドサービスにアクセスするためのエンドポイントとして設定される。


## cfn-ecs-infra-template.yml

ECS/Fargate用にクラスターとサービスを作成するテンプレート。

```cfn-ecs-infra-template.yml
AWSTemplateFormatVersion: "2010-09-09"
Description:
  ECS Infra Create (Cluster Service)

# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------# 
Parameters:
  PJPrefix:
    Type: String
    Default: "game"

#Network
  SecurityGroupId:
    Type: String
    Default: "sg-xxxxxx"

  SubnetIdA:
    Type: String
    Default: "subnet-xxxxxxx"

  SubnetIdC:
    Type: String
    Default: "subnet-xxxxxx"
    
  TargetGroupUi1Arn:
    Type: AWS::SSM::Parameter::Value<String>
    Default: "/game/TargetGroupUi1Arn"
  
  TargetGroupServer1Arn:
    Type: AWS::SSM::Parameter::Value<String>
    Default: "/game/TargetGroupServer1Arn"

#ECS
  TaskDefUi:
    Type: String
    Default: "game-ui:15"

  TaskDefServer:
    Type: String
    Default: "game-server:5"

Resources:

# ------------------------------------------------------------#
#  ECS
# ------------------------------------------------------------#
  ECSCluster:      
    Type: AWS::ECS::Cluster
    Properties: 
      ClusterName: !Sub "${PJPrefix}-ecs-cluster"

  ECSServiceForUi: 
    Type: AWS::ECS::Service
    Properties: 
      Cluster: !Ref ECSCluster
      DeploymentController: 
        Type: "CODE_DEPLOY"
      DesiredCount: 1
      HealthCheckGracePeriodSeconds: 60
      LaunchType: "FARGATE"
      LoadBalancers: 
        - ContainerName: "game-ui"
          ContainerPort: 80
          TargetGroupArn: !Ref TargetGroupUi1Arn
      NetworkConfiguration: 
        AwsvpcConfiguration:
          AssignPublicIp: "ENABLED"
          SecurityGroups: 
            - !Ref SecurityGroupId
          Subnets: 
            - !Ref SubnetIdA
            - !Ref SubnetIdC
      SchedulingStrategy: "REPLICA"
      ServiceName: !Sub "${PJPrefix}-ecs-service-ui"
      TaskDefinition: !Ref TaskDefUi

  ECSServiceForServer: 
    Type: AWS::ECS::Service
    Properties: 
      Cluster: !Ref ECSCluster
      DeploymentController: 
        Type: "CODE_DEPLOY"
      DesiredCount: 1
      HealthCheckGracePeriodSeconds: 60
      LaunchType: "FARGATE"
      LoadBalancers: 
        - ContainerName: "game-server"
          ContainerPort: 3000
          TargetGroupArn: !Ref TargetGroupServer1Arn
      NetworkConfiguration: 
        AwsvpcConfiguration:
          AssignPublicIp: "ENABLED"
          SecurityGroups: 
            - !Ref SecurityGroupId
          Subnets: 
            - !Ref SubnetIdA
            - !Ref SubnetIdC
      SchedulingStrategy: "REPLICA"
      ServiceName: !Sub "${PJPrefix}-ecs-service-server"
      TaskDefinition: !Ref TaskDefServer

# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#      

  ECSClusterName:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /game/ECSClusterName
      Type: String
      Value: !Ref ECSCluster  

  ECSServiceUiName:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /game/ECSServiceUiName
      Type: String
      Value: !GetAtt ECSServiceForUi.Name

  ECSServiceServerName:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /game/ECSServiceServerName
      Type: String
      Value: !GetAtt ECSServiceForServer.Name
```

パラメータ読み込みのところで、タスク定義のデフォルト値を"game-ui:15"や"game-server:5"としているのは、誤ってバージョン1のタスク定義を消してしまったから。

## cfn-s3-infra-template.yml

S3とCloudFrontを作成するテンプレート。
おまけみたいなものなので、結構適当。
本当はCloudFrontのOAIを使ってS3にアクセスするようにし、S3側は公開しないようにした方がよいが、そのまま両方公開された状態になる。


```cfn-s3-infra-template.yml
AWSTemplateFormatVersion: "2010-09-09"
Description:
  S3 Infra Create (S3 CloudFront)

# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------# 
Parameters:
  PJPrefix:
    Type: String
    Default: "game"

Resources:

# ------------------------------------------------------------#
#  S3
# ------------------------------------------------------------#
  S3Bucket:
    Type: "AWS::S3::Bucket"
    Properties:
      BucketName: !Sub "${PJPrefix}-ui-s3-hosting"
      AccessControl: "PublicRead"
      WebsiteConfiguration:
        IndexDocument: "index.html"
        ErrorDocument: "error.html"
    DeletionPolicy: Retain

  BucketPolicy:
    Type: "AWS::S3::BucketPolicy"
    Properties:
      PolicyDocument:
        Id: 'S3Policy'
        Version: '2012-10-17'
        Statement:
          - Sid: 'PublicReadForGetBucketObjects'
            Effect: 'Allow'
            Principal: '*'
            Action: 's3:GetObject'
            Resource: !Join 
              - ''
              - - 'arn:aws:s3:::'
                - !Ref S3Bucket
                - '/*'
      Bucket: !Ref S3Bucket
      
# ------------------------------------------------------------#
#  CloudFront
# ------------------------------------------------------------#
  CloudFrontDistribution:
    Type: "AWS::CloudFront::Distribution"
    Properties:
      DistributionConfig:
        PriceClass: PriceClass_All
        Origins:
        - DomainName: !Sub "${PJPrefix}-ui-s3-hosting.s3-website-${AWS::Region}.amazonaws.com"
          Id: !Sub "S3-${PJPrefix}-ui-s3-hosting"
          CustomOriginConfig:
            OriginProtocolPolicy: "http-only"
        DefaultCacheBehavior:
          TargetOriginId: !Sub "S3-${PJPrefix}-ui-s3-hosting"
          ViewerProtocolPolicy: "allow-all"
          AllowedMethods:
          - "GET"
          - "HEAD"
          CachedMethods:
          - "GET"
          - "HEAD"
          DefaultTTL: 3600
          MaxTTL: 86400
          MinTTL: 60
          Compress: true
          ForwardedValues:
            Cookies:
              Forward: "none"
            QueryString: false
        Enabled: true

# ------------------------------------------------------------#
# Output Parameters
# ------------------------------------------------------------#      

  S3URL:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /game/S3URL
      Type: String
      Value: !GetAtt S3Bucket.WebsiteURL
  
  WebsiteURL:
    Type: AWS::SSM::Parameter
    Properties:
      Name: /game/WebsiteURL
      Type: String
      Value: !GetAtt CloudFrontDistribution.DomainName
```

一応、最後にS3で静的ホスティングしたURLとCloudFrontのURLをパラメータストアに入れている。

## create-codedeploy.sh

コードデプロイのアプリケーションとデプロイグループをAWS CLIを使って作成するシェル。

最初にこれまでにCFnで作成したリソースの情報をパラメータストアから読み込んでいる。

次に、デプロイグループ設定用jsonテンプレートのプレイスホルダー部分にパラメータストアから取得した値などを埋め込んで、設定用jsonを生成している。

AWS CLIコマンドで生成した設定用jsonを指定してデプロイグループを作成する。

```create-codedeploy.sh
#!/bin/sh

# Parameter
DEPLOYMENT_GROUP_TEMPLATE=./deploy-group-template.json
DEPLOYMENT_GROUP_UI_JSON=./deploy-group-ui.json
DEPLOYMENT_GROUP_SERVER_JSON=./deploy-group-server.json
PJ_PREFIX=game
APPLICATION_UI_NAME=${PJ_PREFIX}-deploy-ui
APPLICATION_SERVER_NAME=${PJ_PREFIX}-deploy-server
DEPLOYMENT_GROUP_UI_NAME=${PJ_PREFIX}-deploy-group-ui
DEPLOYMENT_GROUP_SERVER_NAME=${PJ_PREFIX}-deploy-group-server
SERVICE_ROLE="arn:aws:iam::{AWSのアカウントID}:role/CodeDeployECSRole"
TARGET_GROUP_UI_1=(`aws ssm get-parameter --name /game/TargetGroupUi1Name --query "Parameter.Value" --output text`)
TARGET_GROUP_UI_2=(`aws ssm get-parameter --name /game/TargetGroupUi2Name --query "Parameter.Value" --output text`)
TARGET_GROUP_SERVER_1=(`aws ssm get-parameter --name /game/TargetGroupServer1Name --query "Parameter.Value" --output text`)
TARGET_GROUP_SERVER_2=(`aws ssm get-parameter --name /game/TargetGroupServer2Name --query "Parameter.Value" --output text`)
ALB_LISTENER_UI_1=(`aws ssm get-parameter --name /game/ALBListenerUi1Arn --query "Parameter.Value" --output text`)
ALB_LISTENER_UI_2=(`aws ssm get-parameter --name /game/ALBListenerUi2Arn --query "Parameter.Value" --output text`)
ALB_LISTENER_SERVER_1=(`aws ssm get-parameter --name /game/ALBListenerServer1Arn --query "Parameter.Value" --output text`)
ALB_LISTENER_SERVER_2=(`aws ssm get-parameter --name /game/ALBListenerServer2Arn --query "Parameter.Value" --output text`)
ECS_CLUSTER_NAME=(`aws ssm get-parameter --name /game/ECSClusterName --query "Parameter.Value" --output text`)
ECS_SERVICE_UI_NAME=(`aws ssm get-parameter --name /game/ECSServiceUiName --query "Parameter.Value" --output text`)
ECS_SERVICE_SERVER_NAME=(`aws ssm get-parameter --name /game/ECSServiceServerName --query "Parameter.Value" --output text`)

# Replace
## For UI
cp -f ${DEPLOYMENT_GROUP_TEMPLATE} ${DEPLOYMENT_GROUP_UI_JSON}
sed -i -e 's@#APPLICATION_NAME#@'${APPLICATION_UI_NAME}'@g' ${DEPLOYMENT_GROUP_UI_JSON}
sed -i -e 's@#DEPLOYMENT_GROUP_NAME#@'${DEPLOYMENT_GROUP_UI_NAME}'@g' ${DEPLOYMENT_GROUP_UI_JSON}
sed -i -e 's@#SERVICE_ROLE#@'"${SERVICE_ROLE}"'@g' ${DEPLOYMENT_GROUP_UI_JSON}
sed -i -e 's@#TARGET_GROUP_1#@'${TARGET_GROUP_UI_1}'@g' ${DEPLOYMENT_GROUP_UI_JSON}
sed -i -e 's@#TARGET_GROUP_2#@'${TARGET_GROUP_UI_2}'@g' ${DEPLOYMENT_GROUP_UI_JSON}
sed -i -e 's@#ALB_LISTENER_1#@'${ALB_LISTENER_UI_1}'@g' ${DEPLOYMENT_GROUP_UI_JSON}
sed -i -e 's@#ALB_LISTENER_2#@'${ALB_LISTENER_UI_2}'@g' ${DEPLOYMENT_GROUP_UI_JSON}
sed -i -e 's@#ECS_CLUSTER_NAME#@'${ECS_CLUSTER_NAME}'@g' ${DEPLOYMENT_GROUP_UI_JSON}
sed -i -e 's@#ECS_SERVICE_NAME#@'${ECS_SERVICE_UI_NAME}'@g' ${DEPLOYMENT_GROUP_UI_JSON}
## For Server
cp -f ${DEPLOYMENT_GROUP_TEMPLATE} ${DEPLOYMENT_GROUP_SERVER_JSON}
sed -i -e 's@#APPLICATION_NAME#@'${APPLICATION_SERVER_NAME}'@g' ${DEPLOYMENT_GROUP_SERVER_JSON}
sed -i -e 's@#DEPLOYMENT_GROUP_NAME#@'${DEPLOYMENT_GROUP_SERVER_NAME}'@g' ${DEPLOYMENT_GROUP_SERVER_JSON}
sed -i -e 's@#SERVICE_ROLE#@'${SERVICE_ROLE}'@g' ${DEPLOYMENT_GROUP_SERVER_JSON}
sed -i -e 's@#TARGET_GROUP_1#@'${TARGET_GROUP_SERVER_1}'@g' ${DEPLOYMENT_GROUP_SERVER_JSON}
sed -i -e 's@#TARGET_GROUP_2#@'${TARGET_GROUP_SERVER_2}'@g' ${DEPLOYMENT_GROUP_SERVER_JSON}
sed -i -e 's@#ALB_LISTENER_1#@'${ALB_LISTENER_SERVER_1}'@g' ${DEPLOYMENT_GROUP_SERVER_JSON}
sed -i -e 's@#ALB_LISTENER_2#@'${ALB_LISTENER_SERVER_2}'@g' ${DEPLOYMENT_GROUP_SERVER_JSON}
sed -i -e 's@#ECS_CLUSTER_NAME#@'${ECS_CLUSTER_NAME}'@g' ${DEPLOYMENT_GROUP_SERVER_JSON}
sed -i -e 's@#ECS_SERVICE_NAME#@'${ECS_SERVICE_SERVER_NAME}'@g' ${DEPLOYMENT_GROUP_SERVER_JSON}

# execute AWS CLI
aws deploy create-application --application-name ${APPLICATION_UI_NAME} --compute-platform ECS
aws deploy create-application --application-name ${APPLICATION_SERVER_NAME} --compute-platform ECS
aws deploy create-deployment-group --application-name ${APPLICATION_UI_NAME} --deployment-group-name ${DEPLOYMENT_GROUP_UI_NAME} --cli-input-json file://${DEPLOYMENT_GROUP_UI_JSON}
aws deploy create-deployment-group --application-name ${APPLICATION_SERVER_NAME} --deployment-group-name ${DEPLOYMENT_GROUP_SERVER_NAME} --cli-input-json file://${DEPLOYMENT_GROUP_SERVER_JSON}
```

デプロイグループのテンプレートファイルはこれ。
\#hogehoge\# となっている部分を置換して、別ファイルとして出力。

```deploy-group-template.json
{
  "applicationName": "#APPLICATION_NAME#",
  "deploymentGroupName": "#DEPLOYMENT_GROUP_NAME#",
  "serviceRoleArn": "#SERVICE_ROLE#",
  "deploymentStyle": {
    "deploymentType": "BLUE_GREEN",
    "deploymentOption": "WITH_TRAFFIC_CONTROL"
  },
  "blueGreenDeploymentConfiguration": {
    "terminateBlueInstancesOnDeploymentSuccess": {
      "action": "TERMINATE",
      "terminationWaitTimeInMinutes": 0
    },
    "deploymentReadyOption": {
      "actionOnTimeout": "CONTINUE_DEPLOYMENT",
      "waitTimeInMinutes": 0
    }
  },
  "loadBalancerInfo": {
    "targetGroupPairInfoList": [
      {
        "targetGroups": [
          {
            "name": "#TARGET_GROUP_1#"
          },
          {
            "name": "#TARGET_GROUP_2#"
          }
        ],
        "prodTrafficRoute": {
          "listenerArns": [
            "#ALB_LISTENER_1#"
          ]
        },
        "testTrafficRoute": {
          "listenerArns": [
            "#ALB_LISTENER_2#"
          ]
        }
      }
    ]
  },
  "ecsServices": [
    {
      "clusterName": "#ECS_CLUSTER_NAME#",
      "serviceName": "#ECS_SERVICE_NAME#"
    }
  ]
}
```

## cfn-cicd-infra-template.yml

CodePipelineやCodeBuildを作成するテンプレート。

ロールはあらかじめ作成しておいたものを使っている。
(ここで生成するようにしてもよいが、お金がかからないので消さずにそのまま使っている)

```cfn-cicd-infra-template.yml
AWSTemplateFormatVersion: "2010-09-09"
Description:
  CI/CD Infra Create

# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------# 
Parameters:
  PJPrefix:
    Type: String
    Default: "game"

#Network
  ALBName:
    Type: AWS::SSM::Parameter::Value<String>
    Default: "/game/ALBName"
    
  TargetGroupUi1Name:
    Type: AWS::SSM::Parameter::Value<String>
    Default: "/game/TargetGroupUi1Name"
    
  TargetGroupUi2Name:
    Type: AWS::SSM::Parameter::Value<String>
    Default: "/game/TargetGroupUi2Name"
  
  TargetGroupServer1Name:
    Type: AWS::SSM::Parameter::Value<String>
    Default: "/game/TargetGroupServer1Name"
  
  TargetGroupServer2Name:
    Type: AWS::SSM::Parameter::Value<String>
    Default: "/game/TargetGroupServer2Name"
    
#Code Commit 
  RepositoryNameForUi:
    Type: String
    Default: "game-ui"

  RepositoryNameForServer:
    Type: String
    Default: "game-server"

  RepositoryNameForInfraUi:
    Type: String
    Default: "infra-ui"

  RepositoryNameForInfraServer:
    Type: String
    Default: "infra-server"
    
  CodeCommitGameUiArn:
    Type: String
    Default: "arn:aws:codecommit:ap-northeast-1:{AWSのアカウントID}:game-ui"

  CodeCommitGameServerArn:
    Type: String
    Default: "arn:aws:codecommit:ap-northeast-1:{AWSのアカウントID}:game-server"

  CodeCommitInfraUiArn:
    Type: String
    Default: "arn:aws:codecommit:ap-northeast-1:{AWSのアカウントID}:infra-ui"

  CodeCommitInfraServerArn:
    Type: String
    Default: "arn:aws:codecommit:ap-northeast-1:{AWSのアカウントID}:infra-server"
    
#CodePipeline
  CodePipelineS3Backet:
    Type: String
    Default: "codepipeline-ap-northeast-1-xxxxxxxxxxx"
    
#Role
  CodePipelineServiceRoleArn:
    Type: String
    Default: "arn:aws:iam::{AWSのアカウントID}:role/service-role/AWSCodePipelineServiceRole"

  CodeBuildServiceRoleArn:
    Type: String
    Default: "arn:aws:iam::{AWSのアカウントID}:role/CodeBuildServiceRole"

  CodeDeployServiceRoleArn:
    Type: String
    Default: "arn:aws:iam::{AWSのアカウントID}:role/CodeDeployECSRole"

  CloudWatchEventRoleArn:
    Type: String
    Default: "arn:aws:iam::{AWSのアカウントID}:role/CloudWatchEventRole"
    
# CodeDeploy (ECSへのBlue/GreenデプロイはCFnで未対応のためパラメータ化)

  CodeDeployAppForUi:
    Type: String
    Default: "game-deploy-ui"

  CodeDeployGroupForUi:
    Type: String
    Default: "game-deploy-group-ui"

  CodeDeployAppForServer:
    Type: String
    Default: "game-deploy-server"

  CodeDeployGroupForServer:
    Type: String
    Default: "game-deploy-group-server"

Resources:
# ------------------------------------------------------------#
#  CodeBuild
# ------------------------------------------------------------#
  CodeBuildProjectForUi:
    Type: AWS::CodeBuild::Project
    Properties:
      Artifacts:
        Type: "CODEPIPELINE"
      Source:
        Type: "CODEPIPELINE"
      Environment:
        PrivilegedMode: true
        ComputeType: "BUILD_GENERAL1_SMALL"
        Image: "aws/codebuild/amazonlinux2-x86_64-standard:2.0-20.03.13"
        Type: "LINUX_CONTAINER"
      Name: !Sub "${PJPrefix}-build-ui"
      ServiceRole: !Ref CodeBuildServiceRoleArn

  CodeBuildProjectForServer:
    Type: AWS::CodeBuild::Project
    Properties:
      Artifacts:
        Type: "CODEPIPELINE"
      Source:
        Type: "CODEPIPELINE"
      Environment:
        PrivilegedMode: true
        ComputeType: "BUILD_GENERAL1_SMALL"
        Image: "aws/codebuild/amazonlinux2-x86_64-standard:2.0-20.03.13"
        Type: "LINUX_CONTAINER"
      Name: !Sub "${PJPrefix}-build-server"
      ServiceRole: !Ref CodeBuildServiceRoleArn

# ------------------------------------------------------------#
#  CodeDeploy (ECSへのBlue/GreenデプロイはCFnで未対応)
#  https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-codedeploy-deploymentgroup-loadbalancerinfo.html
#   Note
#   AWS CloudFormation supports blue/green deployments on the AWS Lambda compute platform only.
#  -> AWS CLI で事前に作成
# ------------------------------------------------------------#
  # CodeDeployAppForUi:
  #   Type: AWS::CodeDeploy::Application
  #   Properties:
  #     ApplicationName: !Sub "${PJPrefix}-deploy-ui"
  #     ComputePlatform: "ECS"

  # CodeDeployAppForServer:
  #   Type: AWS::CodeDeploy::Application
  #   Properties:
  #     ApplicationName: !Sub "${PJPrefix}-deploy-server"
  #     ComputePlatform: "ECS"
  
  # CodeDeployGroupForUi:
  #   Type: AWS::CodeDeploy::DeploymentGroup
  #   Properties: 
  #     ApplicationName: !Ref CodeDeployAppForUi
  #     DeploymentConfigName: "CodeDeployDefault.AllAtOnce"
  #     DeploymentGroupName:  !Sub "${PJPrefix}-deploy-group-ui"
  #     DeploymentStyle:
  #       DeploymentOption: "WITH_TRAFFIC_CONTROL"
  #       DeploymentType: "BLUE_GREEN"
  #     LoadBalancerInfo: 
  #       ElbInfoList: 
  #         - Name: !Ref ALBName
  #       TargetGroupInfoList: 
  #         - Name: !Ref TargetGroupUi1Name
  #         - Name: !Ref TargetGroupUi2Name
  #     ServiceRoleArn: !Ref CodeDeployServiceRoleArn
  
  # CodeDeployGroupForServer:
  #   Type: AWS::CodeDeploy::DeploymentGroup
  #   Properties: 
  #     ApplicationName: !Ref CodeDeployAppForServer
  #     DeploymentConfigName: "CodeDeployDefault.AllAtOnce"
  #     DeploymentGroupName:  !Sub "${PJPrefix}-deploy-group-server"
  #     DeploymentStyle:
  #       DeploymentOption: "WITH_TRAFFIC_CONTROL"
  #       DeploymentType: "BLUE_GREEN"
  #     LoadBalancerInfo: 
  #       ElbInfoList: 
  #         - Name: !Ref ALBName
  #       TargetGroupInfoList: 
  #         - Name: !Ref TargetGroupServer1Name
  #         - Name: !Ref TargetGroupServer2Name
  #     ServiceRoleArn: !Ref CodeDeployServiceRoleArn

# ------------------------------------------------------------#
#  CodePipeLine
# ------------------------------------------------------------#
  PipelineForUi:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      RoleArn: !Ref CodePipelineServiceRoleArn
      Name: !Sub "${PJPrefix}-pipline-ui"
      ArtifactStore:
        Type: S3
        Location: !Ref CodePipelineS3Backet
      Stages:
        - Name: "Source"
          Actions:
            - Name: "InfraSourceAction"
              ActionTypeId:
                Category: "Source"
                Owner: "AWS"
                Version: "1"
                Provider: "CodeCommit"
              Configuration:
                RepositoryName: !Ref RepositoryNameForInfraUi
                PollForSourceChanges: false
                BranchName: "master"
              RunOrder: 1
              OutputArtifacts:
                - Name: "InfraSourceArtifact"
            - Name: "ImageSourceAction"
              ActionTypeId:
                Category: "Source"
                Owner: "AWS"
                Version: "1"
                Provider: "CodeCommit"
              Configuration:
                RepositoryName: !Ref RepositoryNameForUi
                PollForSourceChanges: false
                BranchName: "master"
              RunOrder: 1
              OutputArtifacts:
                - Name: "ImageSourceArtifact"
        - Name: "Build"
          Actions:
            - Name: "ImageBuildAction"
              ActionTypeId:
                Category: "Build"
                Owner: "AWS"
                Version: "1"
                Provider: "CodeBuild"
              Configuration:
                ProjectName: !Ref CodeBuildProjectForUi
              RunOrder: 1
              InputArtifacts:
                - Name: "ImageSourceArtifact"
              OutputArtifacts:
                - Name: "ImageBuildArtifact"
        - Name: "Deploy"
          Actions:
            - Name: "ImageDeployAction"
              ActionTypeId:
                Category: "Deploy"
                Owner: "AWS"
                Version: 1
                Provider: "CodeDeployToECS"
              Configuration:
                ApplicationName: !Ref CodeDeployAppForUi
                DeploymentGroupName: !Ref CodeDeployGroupForUi
                Image1ArtifactName: "ImageBuildArtifact"
                Image1ContainerName: "IMAGE1_NAME"
                TaskDefinitionTemplateArtifact: "InfraSourceArtifact"
                TaskDefinitionTemplatePath: "taskdef.json"
                AppSpecTemplateArtifact: "InfraSourceArtifact"
                AppSpecTemplatePath: "appspec.yaml"
              RunOrder: 1
              InputArtifacts:
                - Name: "InfraSourceArtifact"
                - Name: "ImageBuildArtifact"
  
  PipelineForServer:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      RoleArn: !Ref CodePipelineServiceRoleArn
      Name: !Sub "${PJPrefix}-pipline-server"
      ArtifactStore:
        Type: S3
        Location: !Ref CodePipelineS3Backet
      Stages:
        - Name: "Source"
          Actions:
            - Name: "InfraSourceAction"
              ActionTypeId:
                Category: "Source"
                Owner: "AWS"
                Version: "1"
                Provider: "CodeCommit"
              Configuration:
                RepositoryName: !Ref RepositoryNameForInfraServer
                PollForSourceChanges: false
                BranchName: "master"
              RunOrder: 1
              OutputArtifacts:
                - Name: "InfraSourceArtifact"
            - Name: "ImageSourceAction"
              ActionTypeId:
                Category: "Source"
                Owner: "AWS"
                Version: "1"
                Provider: "CodeCommit"
              Configuration:
                RepositoryName: !Ref RepositoryNameForServer
                PollForSourceChanges: false
                BranchName: "master"
              RunOrder: 1
              OutputArtifacts:
                - Name: "ImageSourceArtifact"
        - Name: "Build"
          Actions:
            - Name: "ImageBuildAction"
              ActionTypeId:
                Category: "Build"
                Owner: "AWS"
                Version: "1"
                Provider: "CodeBuild"
              Configuration:
                ProjectName: !Ref CodeBuildProjectForServer
              RunOrder: 1
              InputArtifacts:
                - Name: "ImageSourceArtifact"
              OutputArtifacts:
                - Name: "ImageBuildArtifact"
        - Name: "Deploy"
          Actions:
            - Name: "ImageDeployAction"
              ActionTypeId:
                Category: "Deploy"
                Owner: "AWS"
                Version: 1
                Provider: "CodeDeployToECS"
              Configuration:
                ApplicationName: !Ref CodeDeployAppForServer
                DeploymentGroupName: !Ref CodeDeployGroupForServer
                Image1ArtifactName: "ImageBuildArtifact"
                Image1ContainerName: "IMAGE1_NAME"
                TaskDefinitionTemplateArtifact: "InfraSourceArtifact"
                TaskDefinitionTemplatePath: "taskdef.json"
                AppSpecTemplateArtifact: "InfraSourceArtifact"
                AppSpecTemplatePath: "appspec.yaml"
              RunOrder: 1
              InputArtifacts:
                - Name: "InfraSourceArtifact"
                - Name: "ImageBuildArtifact"
                
# ------------------------------------------------------------#
#  CloudWatchEvent
# ------------------------------------------------------------#
  AmazonCloudWatchEventRuleForUi:
    Type: AWS::Events::Rule
    Properties:
      EventPattern:
        source:
          - "aws.codecommit"
        detail-type:
          - "CodeCommit Repository State Change"
        resources:
          - !Ref CodeCommitGameUiArn
        detail:
          event:
            - "referenceCreated"
            - "referenceUpdated"
          referenceType:
            - "branch"
          referenceName:
            - "master"
      Targets:
        - Arn: !Sub "arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${PJPrefix}-pipline-ui" # CFnのPipelineはArnを取得できないため直接記載
          RoleArn: !Ref CloudWatchEventRoleArn
          Id: "codepipeline"

  AmazonCloudWatchEventRuleForServer:
    Type: AWS::Events::Rule
    Properties:
      EventPattern:
        source:
          - "aws.codecommit"
        detail-type:
          - "CodeCommit Repository State Change"
        resources:
          - !Ref CodeCommitGameServerArn
        detail:
          event:
            - "referenceCreated"
            - "referenceUpdated"
          referenceType:
            - "branch"
          referenceName:
            - "master"
      Targets:
        - Arn: !Sub "arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${PJPrefix}-pipline-server" # CFnのPipelineはArnを取得できないため直接記載
          RoleArn: !Ref CloudWatchEventRoleArn
          Id: "codepipeline"

  AmazonCloudWatchEventRuleForUiInfra:
    Type: AWS::Events::Rule
    Properties:
      EventPattern:
        source:
          - "aws.codecommit"
        detail-type:
          - "CodeCommit Repository State Change"
        resources:
          - !Ref CodeCommitInfraUiArn
        detail:
          event:
            - "referenceCreated"
            - "referenceUpdated"
          referenceType:
            - "branch"
          referenceName:
            - "master"
      Targets:
        - Arn: !Sub "arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${PJPrefix}-pipline-ui" # CFnのPipelineはArnを取得できないため直接記載
          RoleArn: !Ref CloudWatchEventRoleArn
          Id: "codepipeline"

  AmazonCloudWatchEventRuleForServerInfra:
    Type: AWS::Events::Rule
    Properties:
      EventPattern:
        source:
          - "aws.codecommit"
        detail-type:
          - "CodeCommit Repository State Change"
        resources:
          - !Ref CodeCommitInfraServerArn
        detail:
          event:
            - "referenceCreated"
            - "referenceUpdated"
          referenceType:
            - "branch"
          referenceName:
            - "master"
      Targets:
        - Arn: !Sub "arn:${AWS::Partition}:codepipeline:${AWS::Region}:${AWS::AccountId}:${PJPrefix}-pipline-server" # CFnのPipelineはArnを取得できないため直接記載
          RoleArn: !Ref CloudWatchEventRoleArn
          Id: "codepipeline"

```

一括作成系の情報はこれで終わり。


## cleanup.sh

一括削除用のスクリプト。
S3バケットはCFnで削除されないので、別途削除している。

```cleanup.sh
#!/bin/sh

echo 'cleanup start'
echo 'delete cicd stack'
aws cloudformation delete-stack --stack-name cicd-stack
aws cloudformation wait stack-delete-complete --stack-name cicd-stack
echo 'cicd stack deleted'
echo 'delete code-deploy'
sh ./delete-codedeploy.sh
echo 'codedeploy deleted'
echo 'delete s3 stack'
aws cloudformation delete-stack --stack-name s3-stack
aws cloudformation wait stack-delete-complete --stack-name s3-stack
echo 's3 stack deleted'
echo 'delete s3 bucket'
aws s3 rb s3://game-ui-s3-hosting --force
echo 's3 bucket deleted'
echo 'delete ecs stack'
aws cloudformation delete-stack --stack-name ecs-stack
aws cloudformation wait stack-delete-complete --stack-name ecs-stack
echo 'ecs stack deleted'
echo 'delete network stack'
aws cloudformation delete-stack --stack-name network-stack  
aws cloudformation wait stack-delete-complete --stack-name network-stack
echo 'network stack deleted'
echo 'cleanup finish'
```

## delete-codedeploy.sh

コードデプロイ削除用のスクリプト。
CFnじゃないのでスクリプトからAWS CLIをたたいている。

```delete-codedeploy.sh
#!/bin/sh

# Parameter
PJ_PREFIX=game
APPLICATION_UI_NAME=${PJ_PREFIX}-deploy-ui
APPLICATION_SERVER_NAME=${PJ_PREFIX}-deploy-server
DEPLOYMENT_GROUP_UI_NAME=${PJ_PREFIX}-deploy-group-ui
DEPLOYMENT_GROUP_SERVER_NAME=${PJ_PREFIX}-deploy-group-server

# execute AWS CLI
aws deploy delete-deployment-group --application-name ${APPLICATION_UI_NAME} --deployment-group-name ${DEPLOYMENT_GROUP_UI_NAME}
aws deploy delete-deployment-group --application-name ${APPLICATION_SERVER_NAME} --deployment-group-name ${DEPLOYMENT_GROUP_SERVER_NAME}
aws deploy delete-application --application-name ${APPLICATION_UI_NAME}
aws deploy delete-application --application-name ${APPLICATION_SERVER_NAME}
```

一括削除は、稀にネットワーク系の情報がCFnで削除できずに残ることがあった。
ネットワーク利用中だと消せないのかもしれない。

あと、ECRのイメージとECSのタスク定義がたまりつづけるので、これも消せるようにした方がよかったかな。


# さいごに

これをやった時点ではCloudFormationでCodeDeployのECS/FargateのBlue/Greenデプロイがサポートされていなかったんですが、直後の5/19にできるようになったらしいです。

https://aws.amazon.com/jp/about-aws/whats-new/2020/05/aws-cloudformation-now-supports-blue-green-deployments-for-amazon-ecs/

今度、書き換えてみようと思います。

最後にAWSの利用料金を載せておきます。

5/5 ～5/11 (7日間)　※AWS利用開始～作業終了まで​

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/643184/b48897c4-25ed-424b-7bfe-9144e07af585.png)


ECSとCodeBuildだけ料金が発生していました。

ECSは無料枠がないので想定通り。

CodeBuildは毎月100hの無料枠がありますが、複数のプロジェクトがあるので、簡単に超えてしまいました。

他は無料枠に収まっています。