AWS CodePipeline で ECS(Fargate) へ Blue/Green デプロイする

[追記:この構成だとひとつ古いDockerイメージしか使われないな:thinking_face:]

AWS CodePipeline を使って Rails アプリケーションを Fargate を利用した ECS に Blue/Green デプロイします。

## デプロイの流れ

1. Rails アプリのソースコードを変更
1. GitHub に Push または Pull Request をマージ
1. CodePipeline のパイプラインが起動
1. CodeBuild でアプリケーションをビルド
1. CodeBuild で Docker イメージをビルド
1. CodeBuild から ECR に Docker イメージを `latest` タグを付けて登録
1. CodeDeploy で ECR から Docker イメージを取得して ECS にデプロイ

Rails アプリの Docker イメージは `app` と `web` の2つ使用します。

## CodePipeline 構成

利用するサービスは以下の通りです。

- AWS Elastic Container Service (ECS)
  - Fargate
- AWS CodeBuild
- AWS CodeDeploy
  - ECS(Fargate) Blue/Green Deploy
- AWS Elastic Container Registry (ECR)
- GitHub

CodeBuild と CodeDeploy は事前に作成しておいてください。

### 全体像

パイプラインの完成形は下図のようになります。
Source ステージでは GitHub と ECR を並列で指定します。Docker イメージ複数使うときは全て追加しておきます。
![codepipeline_outline.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126845/46f60c54-a737-b320-0c3f-0d83ff9878e6.png)

次は GitHub Source です。
GitHub と連携してリポジトリとブランチを指定します。図のブランチは動作確認用に `ecs` としていますが本番利用時は `master` にしています。
GitHub Source の **出力アーティファクト** はこの先のステージで使用しないのでデフォルト値にしています。
![codepipeline_github.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126845/adaadcec-e557-84f9-8077-d69c1f9d9d4d.png)

次は ECR Source です。利用する ECR のリポジトリとタグを指定してください。今回は常に `latest` をデプロイします。
**アクション名**と**出力アーティファクト**はこの先のステージと設定ファイルで使用するので `web` を指定します。
![codepipeline_ecr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126845/93531fe3-c2b1-fb4e-1ae6-42271e1bff33.png)

次は Build ステージです。
**入力アーティファクト** には GitHub Source の出力である `SourceArtifact` を選択します。
**プロジェクト名**には事前に作成した CodeBuild のプロジェクトを選択してください。
ビルド結果は `BuildArtifact` として S3 にアップロードします。アップロードされるファイルは `taskdef.json` `appspec.yaml` になります。
![codepipeline_build.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126845/9d6595e4-f32e-8d7e-8829-b6974a19645c.png)

最後は Deploy ステージです。
**アクションプロバイダー**に ECS のBlue/Green を選択してください。専用の設定項目が表示されます。
**入力アーティファクト**には ECR アクションと Build ステージのアーティファクトを選択しています。今回は `SourceArtifact` は使用しません。
CodeDeploy に関する設定は事前に作成したものを選択してください。
![codepipeline_deploy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/126845/23afc26c-4737-1a13-d32c-1db124eedde0.png)

**Amazon ECS タスク定義** は `BuildArtifact` の `taskdef.json` を指定します。AppSpec ファイルも同様に `BuildArtifact` のファイルを指定してください。
この2つのファイルは `buildspec.yaml` の `files` セクションで指定されているので、ビルド後に S3 にアップロードされて CodePipeline が利用できるようになります。

AWS の公式ドキュメントでは `SourceArtifact` を指定していますが、以下のエントリにあるように `SourceArtifact` が 3MB 以上あるとエラーになるので `BuildArtifact` を参照しています。

[CodeDeployでtask definitionが読み込めない時の対処法 - Qiita](https://qiita.com/hareku/items/a4575de31571071d879e)

最後に `taskdef.json` で Docker イメージが参照できるように**入力アーティファクトを持つイメージの詳細**に ECR Source のイメージを設定します。

## 設定ファイル

CodePipelie/CodeBuild/CodeDeploy には以下の3ファイルが必要です。すべて GitHub のリポジトリに追加しています。

- `taskdef.json`
- `appspec.yaml`
- `buildspec.yaml`

Blue/Green デプロイを利用するので `imagedefinitions.json` は不要です。ECR Source を利用するので `imageDetail.json` も不要です。

*各種設定値は環境に合わせて読み替えてください*

### taskdef.json
`.family` の値は ECS のタスク定義で設定した名前にします。
`.containerDefinitions[0].name` は Build ステージで入力した「入力アーティファクトを持つイメージの詳細」に合わせます。
`.containerDefinitions[0].image` が「タスク定義のプレースホルダー文字」になります。`<>` も含めて設定します。

```json
{
      "family": {{ ここは ECS task の family に合わせる }},
      "taskRoleArn": "arn:aws:iam::000000000000:role/ecsTaskRole",
      "executionRoleArn": "arn:aws:iam::000000000000:role/ecsTaskExecutionRole",
      "networkMode": "awsvpc",
      "containerDefinitions": [
          {
              "name": "app",
              "image": "<app>",
              "essential": true,
          },
          {
              "name": "web",
              "image": "<web>",
              "portMappings": [
                  {
                      "containerPort": 80,
                      "hostPort": 80,
                      "protocol": "tcp"
                  }
              ],
              "essential": false,
          }
      ],
      "requiresCompatibilities": [
          "FARGATE"
      ],
      "cpu": "256",
      "memory": "512"
}
```

### appspec.yaml
`Resources[0].TargetServices.Properties.TaskDefinition` は `<>` も含めて以下の通り設定します。
`Subnets` や `SecurityGroups` は環境に合わせて変更してください。

```yaml
version: 0.0
Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: <TASK_DEFINITION>
        LoadBalancerInfo:
            ContainerName: web
            ContainerPort: 80
        NetworkConfiguration:
          AwsvpcConfiguration:
            Subnets:
              - subnet-00000000
            SecurityGroups:
              - sg-00000000
            AssignPublicIp: ENABLED
```

### buildspec.yaml
`files` セクションに `appspec.yaml` と `taskdef.json` が必要です。
 
```yaml
version: 0.2

phases:
  install:
    runtime-versions:
      docker: 18
  pre_build:
    commands:
      - echo Logging in to Amazon ECR...
      - $(aws ecr get-login --no-include-email --region ap-northeast-1)
  build:
    commands:
      - echo Build started on `date`
      - echo Compiling assets...
      - echo Building the Docker image...
      - docker-compose -f docker-compose.production.yml build --parallel
  post_build:
    commands:
      - echo Build completed on `date`
      - echo Pushing the Docker image...
      - docker-compose -f docker-compose.yml push

artifacts:
  files:
    - appspec.yaml
    - taskdef.json
```