CodePipelineで自動テスト＆ビルドして、ECSへBlue/Greenデプロイしてみた

#０．はじめに
CI/CDの勉強をする一環で、AWSCodePipelineを使って自動テスト＆ビルドして、ECSへのBlue/Greenデプロイさせるまでを試してみたので、やり方を備忘録も兼ねて残しておくものです。

初投稿故、イマイチなところがあってもお許しください。
#１．事前準備：アプリケーション & ECSクラスター & ECSタスク定義作成
####アプリケーション
アプリ自体はDjangoで作成した、ただのノーマルな感じのAPIなため割愛。
####ECSクラスター
こちらも特筆すべき点なしのため割愛。
####ECSタスク定義
以下3コンテナイメージの構成で、ecs-cli composeを使ってタスク定義しています。

- balance_app　⇒　今回デプロイ対象とするアプリケーション用コンテナ
- nginx
- xray-daemon　

```yml:ecs-compose.yml（ご参考。パラメータファイルは割愛）
version: '2'
services:
  balance_app:
    image: '<AccountId>.dkr.ecr.us-west-2.amazonaws.com/msa_balance_app:latest'
    mem_limit: 256000000
    ports:
      - '5000:5000'
    command: ./uwsgi.sh
    environment:
      AWS_REGION: us-west-2
      AWS_XRAY_DAEMON_ADDRESS: '127.0.0.1:2000'
    logging:
      driver: awslogs
      options:
        awslogs-group: msa_app
        awslogs-region: us-west-2
        awslogs-stream-prefix: balance_app
  balance_nginx:
    image: '<AccountId>.dkr.ecr.us-west-2.amazonaws.com/msa_nginx:latest'
    mem_limit: 128000000
    links:
      - balance_app
    ports:
      - '80:80'
    command: >
      /bin/sh -c "envsubst '$$SERVER_NAME'
      < /etc/nginx/nginx.conf.template >
      /etc/nginx/nginx.conf
      && nginx -g 'daemon off;'"
    environment:
      SERVER_NAME: localhost
    logging:
      driver: awslogs
      options:
        awslogs-group: msa_app
        awslogs-region: us-west-2
        awslogs-stream-prefix: balance_nginx
  aws-xray-daemon:
    image: 'amazon/aws-xray-daemon'
    mem_limit: 128000000
    ports:
      - '2000:2000/udp'
    command:
      - "/usr/bin/xray"
      - "--bind"
      - "0.0.0.0:2000"
      - "--region"
      - "us-west-2"
      - "--buffer-memory"
      - "64"
      - "--log-level"
      - "dev"
      - "--log-file"
      - "/dev/stdout"
    logging:
      driver: awslogs
      options:
        awslogs-group: msa_app
        awslogs-region: us-west-2
        awslogs-stream-prefix: balance_xray-daemon
```

また、Codeシリーズでテストやビルドやデプロイをするための各定義ファイルも配置して、CodeCommitへgit pushしておきます。

```text:プロジェクト全体のディレクトリ構成
project/
  ├ .git
  ├ root/
  │  ├ msa_app/
  │  ├ balance/
  │  ├ manage.py
  │  └ requirements.txt
  ├ appspec.yaml　  ⇒ Blue/Greenデプロイ用
  ├ buildspec.yml　 ⇒ アプリケーション自動ビルド用
  ├ Dockerfile
  ├ taskdef.json　  ⇒ Blue/Greenデプロイ用
  ├ testspec.yml　  ⇒ アプリケーション自動テスト用
  └ uwsgi.sh
```


#２．CodeBuildでアプリケーションをUNITテスト
####テスト実行用specファイルの作成
まず、リポジトリルート直下に配置したtestspec.ymlをこんな感じで書いて、アプリ稼働に必要なライブラリをpip installした後、テストコードを実行するように定義します。

```yml:testspec.yml
version: 0.2
phases:
  install:
    commands:
      - echo install started
      - cd root
      - pip install -r requirements.txt
  build:
    commands:
      - echo test started
      - python manage.py  test --settings msa_app.ci_settings
```
####テスト用のCodeBuildプロジェクト作成
次に、CodeBuildでビルドプロジェクトを作っていきます。

リポジトリはCodeCommitを選択。
![source.png](https://qiita-image-store.s3.amazonaws.com/0/285420/98575e31-6c5a-4122-0edc-71aafd6b6fc4.png)
UbuntuのPython3.6を選択。
![env.png](https://qiita-image-store.s3.amazonaws.com/0/285420/117748d8-2ffb-2552-cceb-df766f4bc690.png)

Buildspecには、先ほど作成したtestspec.ymlを指定。
今回はテスト実行時には成果物を何も作らないので、Artifactsは初期値のままでOK。
![spec.png](https://qiita-image-store.s3.amazonaws.com/0/285420/528f737e-978e-3774-25b1-4ec1439c3b2c.png)

#３．CodeBuildでDockerイメージをビルド
####ビルド実行用specファイルの作成
テスト時と同じように、リポジトリルート直下に配置したbuildspec.ymlをこんな感じに書きます。

```yml:buildspec.yml
version: 0.2
phases:
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - $(aws ecr get-login --no-include-email --region $AWS_DEFAULT_REGION)
  build:
    commands:
      - echo Build started on `date`
      - echo Building the Docker image...
      - 'docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .'
      - >-
        docker tag $IMAGE_REPO_NAME:$IMAGE_TAG
        $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker image...
      - >-
        docker push
        $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
      - >-
        printf '[{"name":"balance_app","imageUri":"%s"}]'
        $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
        > artifacts.json
artifacts:
  files: artifacts.json
```
buildステップでdocker buildコマンドによるdockerイメージ作成＆タグ付けを行い、
post_buildステップでECRリポジトリへの登録を行っています。

なお、Dockerfileはこんな感じで特に変哲もない、
必要ライブラリをpip install後、uwsgiでDjangoアプリを動かすものです。

```text:Dockerfile
FROM python:3.6
RUN groupadd -r uwsgi && useradd -r -g uwsgi uwsgi
COPY /root/requirements.txt /requirements.txt
RUN pip install -r /requirements.txt
COPY /root /app
RUN chown -R uwsgi /app
COPY uwsgi.sh /uwsgi.sh
RUN chmod +x /uwsgi.sh
```
####Dockerイメージビルド用のCodeBuildプロジェクト作成
デプロイ用のDockerイメージをビルドする準備ができたので、ビルドプロジェクトを作成していきます。

リポジトリはテスト時と同じくCodeCommitを選択。
![source.png](https://qiita-image-store.s3.amazonaws.com/0/285420/459f368d-cda0-6906-0434-0f96b8c6d338.png)
今度はDockerイメージを作成するので、ランタイムにDockerを指定。
![env.png](https://qiita-image-store.s3.amazonaws.com/0/285420/9af90bce-3be5-4662-92f0-950ba7da4be3.png)
ビルド時に環境変数からリポジトリ名等を設定するので、環境変数も設定しておきます。
![env_para.png](https://qiita-image-store.s3.amazonaws.com/0/285420/42f20c18-8ebe-2cd1-ada6-e9312a7ce68f.png)
Buildspecには、先ほど作成したbuildspec.ymlを指定。
今回はビルド成果物としてDockerイメージがありますが、ECRへ直接pushしているため、Artifactsは初期値でOK。
![spec.png](https://qiita-image-store.s3.amazonaws.com/0/285420/47ebd8f6-71ca-c281-062d-6ed85f618833.png)

これでテスト＆ビルド用のプロジェクトがそれぞれ作成できました。

#４．CodeDeployでBlue/Greenデプロイ
ここからはデプロイ用のアプリケーション＆デプロイグループの作成ですが、こちらはCodeDeployのコンソール画面ではなく、ECSサービスを作成することで自動作成できます。
####ECSサービスの作成
デプロイ方式としてBlue/greenを選択することで、Blue/Greenデプロイ対応のサービスが作成され、
合わせて自動的にCodeDeploy用のアプリケーション＆デプロイグループも作成されます。

![service1.png](https://qiita-image-store.s3.amazonaws.com/0/285420/051dd437-5b44-85fb-db9f-cedbca68032a.png)
なお、Blue/Greenデプロイを選択するには、以下の権限を持ったIAMロールを事前に作成しておく必要があります。

- ポリシーのアタッチ : AWSCodeDeployRoleForECS
- 信頼ポリシー設定

```json:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "codedeploy.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
```

ネットワークはawsvpcモードでタスク定義しているので、配置するVPCとサブネット、適用するセキュリティグループを選択します。
![service2.png](https://qiita-image-store.s3.amazonaws.com/0/285420/e229cb88-be0f-1408-e1fe-b8f6385f2b0c.png)
事前に作成しておいたロードバランサー（今回は後々APIGatewayにVPCリンク張りたかったため、NLBを作成済）を選択し、負荷分散用のコンテナとしてロードバランサーからトラフィックを流す先のコンテナを設定します。（ここで設定したコンテナ名:ポートを、appspec.yamlで設定することになります)  

次にロードバランサー側のポート設定として、本番トラフィックを受け付けるポートをリスナーポートに、Blue/Greenデプロイ時にテスト用トラフィックを受け付けるポートをテストリスナーポートへ設定します。
![service3-1.png](https://qiita-image-store.s3.amazonaws.com/0/285420/f2a095a4-3858-5589-4572-6ecf83617d5e.png)

TargetGroupを二つ設定します。（CodeDeployからECSへBlue/Greenデプロイが走った際、ここのターゲットが自動で切り替わることで本番／テスト用トラフィックが入れ替わることになります。）
![service3-2.png](https://qiita-image-store.s3.amazonaws.com/0/285420/796bf369-70fb-7f3c-5f8a-d08e4cf9e25e.png)
今回は特に必要ないですが、DNSの設定も行っています。
![service4.png](https://qiita-image-store.s3.amazonaws.com/0/285420/b95246bf-90de-4360-05b8-d01fa50d7811.png)

####CodeDeployのデプロイ設定修正
これでECSサービス作成が完了し、CodeDeploy側でもアプリケーション＆デプロイグループが自動作成されましたが、デフォルトではデプロイ時に即トラフィックが本番へ切り替わる設定になっているため、せっかくのBlue/Greenデプロイの利点が活かせません。  
そこで、本番への切り替え前にテスト用トラフィックで検証できるよう、CodeDeployのコンソール画面から設定を修正します。

下記の設定ではデプロイ後の6時間はトラフィックが切り替わらず、テスト用ポート8080を使っての検証が可能となります。
6時間以内に検証を終え、リリース可能となった場合にはCodeDeployコンソール上から手動で再ルーティングすることで、本番トラフィックへの切り替えが行われます。
一方、6時間以内に検証が終わらずに手動で再ルーティングボタンできなかった場合には、Deploy失敗と見なされてその時点でDeploy処理が中断されます。
![deploy_setting.png](https://qiita-image-store.s3.amazonaws.com/0/285420/0763e007-62fa-6e50-9346-eba3a75b78d8.png)

#５．CodePipelineでテスト&ビルド&デプロイを自動化
最後に、CodePipelineで全部繋げていきます。
なお、本当は1本のPipelineで繋げようとしたのですが、Deployステージでのコンテナイメージ名取得がうまいこといかず、テスト～ビルド&デプロイの二本立てになってます。
（BuildステージからDeployステージへのイメージ名の受け渡しがうまくできなかった...）

##テスト～ビルド
###①Sourceステージ
リポジトリへのpushをトリガーにPipeline起動させるためにCloudWatch Eventsで変更検出を行い、リポジトリに格納されているソースコードを後続のTestステージ&Buildステージで使用するためにSourceArtifactを出力します。
![source.png](https://qiita-image-store.s3.amazonaws.com/0/285420/d84f6f5f-2550-907b-f735-25e846506206.png)

###②Testステージ
入力アーティファクトにはSourceステージの出力を指定、プロジェクト名には事前作成済のテスト用ビルドプロジェクトを指定します。
出力成果物はないので、出力アーティファクトは空欄でOK。
![test.png](https://qiita-image-store.s3.amazonaws.com/0/285420/d13cc1b7-6e94-f4e8-bd1a-c638c23196b0.png)

###③Buildステージ
テスト同様、入力アーティファクトにはSourceステージの出力を指定、プロジェクト名には事前作成済のビルド用ビルドプロジェクトを指定します。
ここも出力アーティファクトは空欄でOKです。
![build.png](https://qiita-image-store.s3.amazonaws.com/0/285420/219089cf-6dd5-2b2e-b40e-b73ce1066ac4.png)

ここまでで、テスト～ビルドを行うPipelineの完成です。
![test&build.png](https://qiita-image-store.s3.amazonaws.com/0/285420/a40dfaeb-5476-b104-6810-f3a27d2f928e.png)

##デプロイ
次に、デプロイ用のPipeline作成です。

Deployステージの入力ファイルとしてappspec&taskdef.jsonが必要となるため、事前に作成しておきます。

```yaml:appspec.yaml
version: 0.0
Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: <TASK_DEFINITION>
        LoadBalancerInfo:
          ContainerName: "balance_nginx"
          ContainerPort: 80
```
TaskDefinition部分はCodeDeployが良しなに変換してくれるので、上記の記載でOK。
ContainerName&Portはロードバランサーからトラフィックを流す先のものを設定します。（ECSサービス作成時に負荷分散用に選択したコンテナ名:ポート）
今回はポート80のnginxコンテナ経由でアプリケーションコンテナへトラフィックを流すので、上記の設定としています。

```json:taskdef.json
{
	"executionRoleArn": null,
	"containerDefinitions": [
略
			"image": "<IMAGE1_NAME>",
			"name": "balance_app"
略
    },
```
タスク定義は長いので省略しますが、ポイントはデプロイするアプリケーション用コンテナのimageを上記の記載とする点で、ここをCodeDeployが良しなにイメージURIを変換してくれます。
その他は事前に作成したタスク定義のJson定義をECSコンソールから引っ張ってきて作るのでOKです。
###①Sourceステージ
インプットになるSourceとして、ECRのDockerイメージとCodeCommitのソースリポジトリを指定します。
####イメージ
デプロイ対象のイメージを指定。
出力アーティファクトは後続のDeployステージにて、イメージ名の取得に使用されます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/285420/526c6ce1-a7d9-8145-334f-649f3c259eab.png)
####ソース
ビルド時と同様にCodeCommitのソースリポジトリを指定しますが、Deploy用Pipelineはソースのpush時ではなくBuild用Pipelineの完了後に実行させたいので、変更検出オプションにCloudWatchEventsは選択しません。
出力アーティファクトは後続のDeployステージにて、appspec.yamlとtaskdef.jsonを入力ファイルとするために使用されます。
![source2.png](https://qiita-image-store.s3.amazonaws.com/0/285420/0fcbbdc3-c709-7a62-e525-f10b3e34c309.png)

###②Approvalステージ
一応、デプロイ前の手動承認も加えておきます。
![approval.png](https://qiita-image-store.s3.amazonaws.com/0/285420/f5dd8130-4e59-74c1-23f1-79549360a9f4.png)

###③Deployステージ
最後にデプロイステージです。

●アクションプロバイダ　⇒　ECSへのBlue/Greenデプロイを選択
●入力アーティファクト　⇒　Sourceステージで出力したSource&ImageArtifactを指定
●CodeDeployアプリケーション&デプロイグループ　⇒　ECSで自動作成されたものがリストに出てくるので選択
●ECSタスク定義　⇒　ソースリポジトリから持ってきたtaskdef.jsonを指定
●AppSpecファイル　⇒　ソースリポジトリから持ってきたappspec.yamlを指定
●入力アーティファクトとイメージ詳細　
　⇒　Sourceステージで出力したImageArtifactを選択し、プレースホルダテキストにはIMAGE1_NAMEを指定。
　　　ここの指定により、appspec.yaml内でIMAGE1_NAMEと指定した部分がイメージURIに置き換わります。　　　　　　　　　　　　　　　　　
![deploy.png](https://qiita-image-store.s3.amazonaws.com/0/285420/e2b97816-6f76-07e1-9727-c38de2941fc7.png)

以上でデプロイ用のPipeline作成も完了です。
![deploy.png](https://qiita-image-store.s3.amazonaws.com/0/285420/66743ed7-f268-44bd-74e2-6d104e1e41a5.png)

#６．Pipelineを実行
これで、ソースリポジトリへgit pushするとPipelineが実行され、テスト＆ビルド＆デプロイが自動実行されます。

ちなみにPipelineが2本に分かれていますが、1本目のBuildステージでECRへのpushが行われたことをトリガーに2本目のPipelineも動き出すようです。（ECRリポジトリをSourceに指定しているとそうなる仕様...？）

無事、テスト&ビルドが自動実行され、Blue/Greenデプロイできました。
![deploy.png](https://qiita-image-store.s3.amazonaws.com/0/285420/41f7cd46-d203-4780-4de3-052a3e2b3f07.png)

#７．最後に
各specファイルの作成やらいろいろ悩んだ（詰まった）ところはありましたが、手順としては割とシンプルにできるあたりマネージドサービス群の威力を感じました。