CIツールでecspressoを使いつつAWS CodePipelineの承認フローを通してECSへデプロイする

# はじめに

[ecspresso](https://github.com/kayac/ecspresso)を使いつつAWS CodePipeline上での承認フローステージを通してECSへデプロイする方法を検証しました。

# 前提

* 既存ですでに以下のリソースが存在している前提になります。
  * Docker Image on ECR
  * ALB
  * ECS Cluster
  * ECS Task Definition
  * ECS Service
  * CodePipelineに付与するIAMサービスロール
  * 上記を構成するためのVPC、サブネット、セキュリティグループなど諸々
* CIツールはGitLab CI
* ECSはFargateタイプであり、ECS ServiceのデプロイではCodeDeploy(Blue/Green)を利用している。

# ローカル環境

```terminal
$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.15.6
BuildVersion:   19G2021

$ ecspresso version
ecspresso v1.2.1
```

# Contents

## 概要

概要として今回は全体として以下のようなアプリケーションのデプロイフローを構築しました。

CIツールであるGitLab CIにてECRへDockerイメージをPushすることとS3へecspressoからtaskdef.jsonとappspec.yamlを出力。
CodePipelineでは承認フローを設定し承認されたらECS(Blue/Green)にてタスク定義が反映されCodeDeployからアプリケーションがデプロイされます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91562/8b768d4e-2679-ddea-7456-95a9abd02a23.png)


## ecspressoの初期設定

ecspressoの初期設定を行います。

既存でECSには以下のようなECS ClusterとECS Serviceがあり、このアプリケーションのデプロイフローを構築する場合、以下のようなに実行します。[^1]

[^1]: 詳細は https://zenn.dev/fujiwara/articles/f2314651691adcae5215.md 参照

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91562/53214cb4-f3f9-be58-c2a6-2642ed776158.png)

```
$ ecspresso init --config config.yaml --region ap-northeast-1 --cluster ecspressopoc --service rjpf-ecspressopoc-service
```

上記を実行することで以下の3つのファイルが生成されます。

* `config.yaml` -> ecspresso 設定ファイル
* `ecs-service-def.json` -> ECSサービスの構成を定義するecspresso用ファイル
* `ecs-task-def.json` -> aws cliで得られるのと同様なECSタスク定義ファイル

#### `config.yaml` と `ecs-service-def.json`

[こちらの公式ドキュメント](https://zenn.dev/fujiwara/articles/f2314651691adcae5215.md)と同じ項目のファイルになっています。

`config.yaml`と`ecs-service-def.json`に関しては修正する部分は特にありません。

#### `ecs-task-def.json`

`ecs-task-def.json`も[こちらの公式ドキュメント](https://zenn.dev/fujiwara/articles/f2314651691adcae5215.md)と同様に出力されています。

ここでCIツールを使って継続的にECS Task Definitionバージョンを更新できるように修正します。

ecspressoのテンプレート機能を利用します。[^2]

[^2]: テンプレート機能に関しては[こちらの公式ドキュメント](https://zenn.dev/fujiwara/articles/ecspresso-20201205)に詳細があります。


ECR上のImageバージョンのimmutable性を保証するためにも`"Image"`フィールドは以下のように修正します。

```json:ecs-task-def.jsonの一部
      "image": "{{ must_env `ECR_IMAGE_URL` }}",
```

後述のCIツールのフロー内で`ECR_IMAGE_URL`環境変数を設定しCIでのecspressoコマンドにてECRへPushしたURLに置換してECS Task Definitionへ更新します。

また、アプリケーションの環境変数をデプロイ先の環境ごとに変えるためにECS Task Definitionの環境変数を設定する箇所も以下のようにしています。CIツールのフローは後述します。

```json:ecs-task-def.jsonの一部
      "environment": [
        {
          "name": "DB_HOST",
          "value": "{{ must_env `DB_HOST` }}"
        }
      ],
```

## CodePipelineのトリガーとなるS3バケットの作成

今回はCodePipelineのSource ActionとしてS3を利用するのでそのためのS3バケットを作成します。[^3]

[^3]: 公式ドキュメント https://docs.aws.amazon.com/codepipeline/latest/userguide/action-reference-S3.html

このS3は後述するCDKにて管理対象のリソースとして作成しています。

## CIツール(GitLab CI) + ecspresso

以下が今回利用したGitLab CIでのYamlファイルになります。

ステップとして以下になります。

0. `push`ステージにて
  0. `docker build`で対象アプリケーションをビルドする。
  0. AWS ECRへ対象イメージをタグ名(Gitコミットハッシュ)付きでDOCKER PUSHする。
0. `deploy`ステージにて
  0. `ecspresso render`でECS Task Definitionファイルの`taskdef.json`をテンプレートから生成する。
  0. `ecspresso appspec`によりECS(Blue/Green)でのCodeDeployのための`appspec.yaml`を生成する。
  0. 上記2つのファイルをZIP化し上述のS3バケットへアップロードする。

```yml:.gitlab-ci.yml
variables:
    IMAGE_TAG: ${CI_COMMIT_SHA}
    ECSPRESSO_VERSION: "v1.2.1"
    AWSCLI_VERSION: "1.18.134"
    ECR_NAME: "ecspressopoc"

stages:
    - push
    - deploy

.push_variables: &push_variables
    DOCKER_DRIVER: overlay2

.push_setup: &push_setup
    image: docker:stable
    services:
        - docker:18.09-dind
    before_script:
        - apk add git
        - apk -Uuv add groff less python3 py-pip
        - pip install awscli

.push: &push
    stage: push
    script:
        - docker build -t ${ECR_NAME} .
        - aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com
        - docker tag ${ECR_NAME}:latest ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${ECR_NAME}:${IMAGE_TAG}
        - docker push ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${ECR_NAME}:${IMAGE_TAG}

.deploy_setup: &deploy_setup
    image: python:3.8
    before_script:
        # install AWS CLI
        - pip install --upgrade pip
        - pip install --upgrade awscli==${AWSCLI_VERSION}
        # install commands
        - apt-get update -y && apt-get install -y curl unzip zip
        # install ecspresso
        - curl -sL -o ecspresso-${ECSPRESSO_VERSION}-linux-amd64.zip https://github.com/kayac/ecspresso/releases/download/${ECSPRESSO_VERSION}/ecspresso-${ECSPRESSO_VERSION}-linux-amd64.zip
        - unzip ecspresso-${ECSPRESSO_VERSION}-linux-amd64.zip
        - install ecspresso-${ECSPRESSO_VERSION}-linux-amd64 /usr/local/bin/ecspresso
        - ecspresso version

.deploy: &deploy
    stage: deploy
    script:
        - export ECR_IMAGE_URL=${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${ECR_NAME}:${IMAGE_TAG} # For ecspresso
        - export DB_HOST=${DB_HOST}
        - ecspresso --config config.yaml render --task-definition > taskdef.json
        - ecspresso --config config.yaml appspec | sed -e '/TaskDefinition/ s/arn.*$/"<TASK_DEFINITION>"/' > appspec.yaml
        - zip ecspresso-deploy-poc.zip appspec.yaml taskdef.json
        - aws s3 cp ecspresso-deploy-poc.zip s3://dev-ecspresso-poc

.dev_variables: &dev_variables
    AWS_ENV: "dev"
    AWS_ACCOUNT_ID: "123456789012"
    DB_HOST: ${DEV_DB_HOST} # variable is in GitLab CI setting

.dev_config: &dev_config
    only:
        - develop
    tags:
        - my-platform_dev_docker
    allow_failure: false

dev-push:
    variables:
        <<: *dev_variables
        <<: *push_variables
    <<: *dev_config
    <<: *push_setup
    <<: *push
    dependencies:
        - dev-migrate-task

dev-deploy:
    variables:
        <<: *dev_variables
    <<: *dev_config
    <<: *deploy_setup
    <<: *deploy
    dependencies:
        - dev-push
```

`ecspresso render`と`ecs appspec`のそれぞれのサブコマンドについてはそれぞれ以下のページで詳細を確認できます。

* https://zenn.dev/fujiwara/articles/ecspresso-20201212
* https://zenn.dev/fujiwara/articles/ecspresso-20201218

## CodePipeline with 承認フロー

CodePipelineを[AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/home.html)を利用して構築しました。

**注意** ただし注意点として今回対象にしていたCodePipelineのECS Blue/Green Action Providerが**CDKでは2021年1月時点で未対応**なのでその部分に関しては、私は現状ではそれ以前のステージまではCDKで対応し`cdk deploy`後に手動で追加修正している状況です。GitHubでは https://github.com/aws/aws-cdk/issues/1559 のIssueが最新の動向になっています。

この記事ではCDKのインストール方法と基本的な利用方法には言及しません。

今回はCDKではTypeScriptを利用しました。対象のコードとしては以下になります。

```lib/ecspressopoc-cpipe-stack.ts
import { Role } from "@aws-cdk/aws-iam";
import { Bucket, BlockPublicAccess } from "@aws-cdk/aws-s3";
import { Stack, Construct, StackProps } from "@aws-cdk/core";
import { Pipeline, Artifact } from "@aws-cdk/aws-codepipeline";
import {
  S3SourceAction,
  ManualApprovalAction
} from "@aws-cdk/aws-codepipeline-actions";

export class EcspressopocCpipeStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // The code that defines your stack goes here

    // Source step
    //
    // S3 bucket as source
    const sourceBucket = new Bucket(this, 'ecspressopoc-bucket', {
      versioned: true, // a Bucket used as a source in CodePipeline must be versioned
      blockPublicAccess: BlockPublicAccess.BLOCK_ALL
    });
    // Source Action
    const sourceOutput = new Artifact();
    const sourceAction = new S3SourceAction({
			actionName: 'S3Source',
			bucket: sourceBucket,
			bucketKey: 'ecspresso-deploy-poc.zip',
			output: sourceOutput,
    });

    // Approval step
    //
    // Manual Approval Action
    const approvalAction = new ManualApprovalAction({
      actionName: 'ManualApproval',
    });

    // ECS Deploy step
    // TODO: Waiting for ECS Blue Green Deployment in CDK

    // Assemble pipeline
    new Pipeline(this, "Pipeline", {
      pipelineName: "ecspressopoc-cdk",
      role: Role.fromRoleArn(this, 'plRole', 'arn:aws:iam::12345678901:role/ecspresso-poc'); // role which can access to ECS Blue/Green Deployment,
      stages: [
        {
          stageName: "S3-Source",
          actions: [sourceAction],
        },
        {
          stageName: "Approval",
          actions: [approvalAction],
        }
      ],
    });
  }
}
```

上記では前述のようにGitLab CIがアップロードする先のS3バケットを以下の設定で作成しています。

* `versioned: true` ← CodePipeline Source用のS3バケットは必ずVersioningをEnabledにしている必要がある。
* `blockPublicAccess: BlockPublicAccess.BLOCK_ALL` ← 内部でしか利用しないのでPublicアクセスはすべてブロックにしておく。

手動承認に関して今回は最小限の設定です。より詳しい仕様は[公式ドキュメント](https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-codepipeline-actions.ManualApprovalAction.html)で確認できます。

上記を`cdk deploy`によりデプロイした後に前述の理由から手動でECS(Blue/Green)でのデプロイ設定をします。以下スクショのようにしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91562/8f336340-1810-2b15-35bd-ff9ff2d8f6ca.png)

CodePipelineでは以下のスクショのようになり、GitLab CIがS3へZIPファイルをアップロードすると処理が開始され、手動承認をするとアプリケーションがECSへデプロイされることを確認しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91562/cfe8517e-f334-1279-c2e4-40e3b1742075.png)

# 参考

* https://adventar.org/calendars/5916
* https://dev.classmethod.jp/articles/ecs-deploy-using-cdk/