GitHub Actions+ecspressoでECSにデプロイする

2020-10-16 追記）
GitHub Actionsでset-env, add-path が非推奨になった :scream: 
https://qiita.com/shonansurvivors/items/f256a0443d346f09448e
こちらの記事を参考に適宜書き換えてください（手抜き ^^;

# はじめに

サイドカー構成で動くECS(Fargate)へ`GitHubActions+ecspresso`を使用してデプロイするCD設定手順を紹介します。
（*CDとはContinuousDevelopment,継続的デプロイメント*のことです）

参考）CircleCI+ecspresso版の記事は[こちら](https://qiita.com/ken11_/items/08c439e2a6a064afe0cf)

# [ecspresso](https://github.com/kayac/ecspresso)を使う理由

* Goで作られている(ワンバイナリ)ので利用が簡単
* 環境別にタスク定義設定を二重管理したくない
    * 設定ファイルに[go-config](https://github.com/kayac/go-config)が使えるので環境ごとの差異を変数化して挿入できる
* サイドカー構成のデプロイに対応している
    * [AWS公式Actions](https://github.com/marketplace/actions/amazon-ecs-deploy-task-definition-action-for-github-actions)でも対応しようと思えば[できる](https://github.com/aws-actions/amazon-ecs-render-task-definition/issues/4)が無理やり感あったので

# 前提

### システム構成

* `ALB + ECS(Fargate)`という構成のWebアプリをデプロイします
    * ALB+ECS部分の構築はこの記事の本質とズレるのでCloudFormationを使います
* 本番／QA環境用にAWSアカウントが２つある前提です
    * マージするブランチによってデプロイ先環境を切り替えます
        * developブランチマージ -> QA環境へデプロイ
        * masterブランチマージ -> 本番環境へデプロイ
* [こちらのリポジトリ](https://github.com/miyaz/fargate-example)を[Fork](https://github.com/miyaz/fargate-example/fork)してください 
    * （リクエストヘッダを表示するだけの雑なアプリです）
    * もちろん構成が同じであれば自作アプリでもOKです

### ディレクトリ構成

デプロイするアプリのGitリポジトリ-ディレクトリ構成例です。
今回デプロイするアプリの名称は仮に`fargate-example`とします。

```console:fargate-exampleリポジトリ
.
├── .github
│   └── workflows
│       └── deploy.yml     # GitHubActionsワークフロー定義ファイル（デプロイ用） *後述*
├── .gitignore
├── README.md
├── cfn-alb-ecs.yml        # ALB+ECSを作るCloudFormationテンプレート
├── deploy
│   ├── ecs-task-def.json
│   └── ecspresso.yml
├── docker-compose.yml
├── deploy
│   ├── ecs-task-def.json  # タスク定義JSON *後述*
│   └── ecspresso.yml      # ecspresso設定ファイル *後述*
├── nginx                  # nginxコンテナ定義（Goアプリにリクエスト中継するためのサイドカー）
│   ├── Dockerfile
│   ├── goapp.conf.tmpl
│   └── nginx.conf
└── goapp                  # goappコンテナ定義（nginx経由でリクエストを処理するGoアプリ）
    ├── Dockerfile
    ├── Makefile
    ├── go.mod
    ├── go.sum
    └── main.go
```

### 作業環境

* ecspressoがインストール済みであること
    * macOSであれば `brew install kayac/tap/ecspresso`でインストールできます
* dockerが動く環境があること
    * macOSであれば `Docker for Mac`をインストールしてください
* aws-cliが使えるようにQA環境／本番環境用のcredentials設定が済んでいること
    * 一部aws-cliでAWSリソースを作る例にしていますが、もちろんマネジメントコンソールで実施しても問題ありません

### 具体的な値について

この記事内では下記値で記載します。
手順を実施する際には環境に合わせて読み替えてください。

* AWSアカウントID
    * QA環境：`123456789012`
    * 本番環境：`234567890123`
* AWSリージョン：`ap-northeast-1`
* アプリ名：`fargate-example`
    * GitHubリポジトリ名／ECSクラスタ・サービスの名称も同じ
    * 分かりやすさのため、ECRリポジトリ名／IAMロール名のprefixとしてもアプリ名を使用します（後述）

# アプリ動作環境構築

### ECRリポジトリ作成

aws-cliを使用して`{アプリ名}-{コンテナ名}`という名前でコンテナ数分作成します。
今回の例では nginx/goappの2つ必要です。

```terminal:nginx用リポジトリ作成例
# aws-cliのプロファイル、およびリージョンを指定
export AWS_PROFILE={プロファイル名}
export AWS_REGION=ap-northeast-1

aws ecr create-repository --repository-name fargate-example-nginx
```
```json:実行結果
{
    "repository": {
        "repositoryArn": "arn:aws:ecr:ap-northeast-1:123456789012:repository/fargate-example-nginx",
        "registryId": "123456789012",
        "repositoryName": "fargate-example-nginx",
        "repositoryUri": "123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/fargate-example-nginx",
        "createdAt": "2020-06-17T09:36:06+00:00",
        "imageTagMutability": "MUTABLE",
        "imageScanningConfiguration": {
            "scanOnPush": false
        }
    }
}
```
あとで使うので`repositoryUri`を控えておくこと・
同様にgoappも実行してください。

### ECRリポジトリへ仮コンテナイメージPush

ECSクラスタ構築及び動作確認で必要となるので、暫定でプレーンのnginxコンテナイメージをECRにPushしておきます。

```terminal
# nginxコンテナイメージ取得
docker pull nginx:1.17-alpine

# aws-cliのプロファイル、およびリージョンを指定
export AWS_PROFILE={プロファイル名}
export AWS_REGION=ap-northeast-1

ACCOUNT_ID=$(aws sts get-caller-identity | jq -r .Account)

# コンテナイメージをQA環境のECRにPush
aws ecr get-login-password |\
docker login --username AWS --password-stdin https://${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com
docker tag nginx:1.17-alpine ${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/fargate-example-nginx:latest
docker push ${ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/fargate-example-nginx:latest
```

プロファイル名を切り替えてQA/本番の２回実行する。

### CloudFormationスタック作成

CloudFormationの画面にアクセスし、スタック作成を開始します。
cfn-alb-ecs.ymlを指定して、`次へ`

<img width="600" alt="スクリーンショット 2020-06-18 15.44.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51108/d75746c6-f74f-0ae2-a00a-5ee4cf872c67.png">

下記の通りに指定して `次へ`
<img width="600" alt="スクリーンショット 2020-06-18 15.44.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51108/6da9f7b6-9d28-b5bd-c973-38b262386ae9.png">

再度 `次へ` 押して
`AWS CloudFormation によって IAM リソースがカスタム名で作成される場合があることを承認します。`
のチェックボックスにチェック入れて `スタックの作成` を押してしばらく待ちます

スタック一覧に `CREATE_COMPLETE`が表示されていれば完了です。(4,5分かかります）

<img width="370" alt="スクリーンショット 2020-06-18 18.58.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51108/71afa956-0af8-654e-d6d2-f966e4a6bba1.png">

出力タブをクリックし、`ALB URL`に表示されているリンクをクリックしてみてください。
<img width="600" alt="スクリーンショット 2020-06-18 19.02.21.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51108/039d8616-64dc-40fe-843d-d69dad23ee10.png">

すると下記のようなnginxの画面が表示されるはずです。
<img width="300" alt="スクリーンショット 2020-06-18 19.04.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51108/41a4143b-96f6-1c3a-929f-3b88d2181a76.png">

上記の作業をQA/本番両環境で実施してください。

↑ここまでがアプリ動作環境の構築手順です。

↓ここから先がGitHubActionsを使ったCD設定の手順・解説です。

### デプロイ用IAMユーザ作成

GitHubActionsからECSにデプロイするための権限を持ったアクセスキーが必要になるため、ここでIAMユーザを作成します。

まずIAMコンソールを開き、下記の通りに入力して`次のステップ`ボタンを3回押し最後に`ユーザーの作成`ボタンを押します。
<img width="800" alt="iam1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51108/038bb0a0-ae3b-665b-39bb-5314981532b1.png">

IAMアクセスキーとシークレットアクセスキーが表示されるので、控えておいてください。

次にIAMユーザー一覧から作成したユーザーを選択し、`インラインポリシーの追加`をクリックします。
<img width="400" alt="スクリーンショット 2020-06-20 11.34.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51108/36bf88e5-dfc9-b1d5-814f-988f1477e467.png">

JSONタブをクリックし下記を入力

```json:deployToECSポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "ecs:RegisterTaskDefinition",
            "Resource": "*"
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": [
                "ecs:UpdateService",
                "iam:PassRole",
                "ecs:DescribeServices"
            ],
            "Resource": [
                "arn:aws:ecs:ap-northeast-1:123456789012:service/fargate-example/fargate-example",
                "arn:aws:iam::123456789012:role/fargate-example-task-role",
                "arn:aws:iam::123456789012:role/ecsTaskExecutionRole"
            ]
        }
    ]
}
```

putImageToECR

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "ecr:CompleteLayerUpload",
                "ecr:UploadLayerPart",
                "ecr:InitiateLayerUpload",
                "ecr:BatchCheckLayerAvailability",
                "ecr:PutImage" 
            ],
            "Resource": [
                "arn:aws:ecr:ap-northeast-1:123456789012:repository/fargate-example-*" 
            ]
        },
        {
            "Sid": "VisualEditor1",
            "Effect": "Allow",
            "Action": "ecr:GetAuthorizationToken",
            "Resource": "*" 
        }
    ]
}
```

上記をQA環境／本番環境用の２回実施してください。(AWSアカウントIDの書き換えを忘れずに）

### GitHubSecrets登録

GitHubのSettingsメニューからSecretsを選択し、シークレットを登録します。
登録した値は、GitHubActions内からセキュアに参照することができます。

<img width="600" alt="スクリーンショッ2020-06-18 19.04.24.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51108/708ca915-9310-8f5f-d89f-18d52f97769d.png">


下記４つの設定を登録してください。

| シークレット名| 説明 |
|:-----------------|:------------------|
| AWS_ACCESS_KEY_ID_QA | QA環境のアクセスキー |
| AWS_SECRET_ACCESS_KEY_QA | QA環境のシークレットアクセスキー |
| AWS_ACCESS_KEY_ID | 本番環境のアクセスキー |
| AWS_SECRET_ACCESS_KEY | 本番環境のシークレットアクセスキー |

### 自動デプロイ設定

[GitHubActionsワークフローファイル](https://github.com/miyaz/fargate-example/blob/master/.github/workflows/deploy.yml)をベースに解説、及び修正が必要な箇所について説明します。

```yaml:fargate-example/github/workflows/deploy.yml
name: deploy
on:
  push:
    branches:
      - dummy
      #- master
      #- develop

jobs:

  deploy:
    name: deploy
    runs-on: ubuntu-latest

    steps:
    - name: set environment variables for QA
      if: github.ref == 'refs/heads/develop'
      env:
        AWS_ACCOUNT_ID: 123456789012
      run: |
        echo "::set-env name=AWS_ACCOUNT_ID::${AWS_ACCOUNT_ID}"

    - name: set environment variables for Production
      if: github.ref == 'refs/heads/master'
      env:
        AWS_ACCOUNT_ID: 234567890123
      run: |
        echo "::set-env name=AWS_ACCOUNT_ID::${AWS_ACCOUNT_ID}"

    - name: configure aws credentials for QA
      if: github.ref == 'refs/heads/develop'
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_QA }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_QA }}
        aws-region: ap-northeast-1

    - name: configure aws credentials for Production
      if: github.ref == 'refs/heads/master'
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ap-northeast-1

    - name: checkout
      uses: actions/checkout@v2

    - name: login to ecr
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@v1

    - name: build, tag, and push image to ecr
      id: build-image
      env:
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        IMAGE_TAG: ${{ github.sha }}
      run: |
        docker build -t fargate-example-nginx:latest nginx
        docker tag fargate-example-nginx:latest $ECR_REGISTRY/fargate-example-nginx:latest
        docker tag fargate-example-nginx:latest $ECR_REGISTRY/fargate-example-nginx:$IMAGE_TAG
        docker push $ECR_REGISTRY/fargate-example-nginx:latest
        docker push $ECR_REGISTRY/fargate-example-nginx:$IMAGE_TAG
        docker build -t fargate-example-goapp:latest goapp
        docker tag fargate-example-goapp:latest $ECR_REGISTRY/fargate-example-goapp:latest
        docker tag fargate-example-goapp:latest $ECR_REGISTRY/fargate-example-goapp:$IMAGE_TAG
        docker push $ECR_REGISTRY/fargate-example-goapp:latest
        docker push $ECR_REGISTRY/fargate-example-goapp:$IMAGE_TAG

    - name: download ecspresso
      env:
        ver: v0.13.6
      run: |
        curl -sLo ecspresso.zip \
          https://github.com/kayac/ecspresso/releases/download/$ver/ecspresso-$ver-linux-amd64.zip
        unzip ecspresso.zip
        mv $(ls -1 ecspresso-*-linux-amd64) ecspresso
      working-directory: ./deploy

    - name: deploy to ecs
      run: ./ecspresso deploy --config ecspresso.yml --tasks=-1
      working-directory: ./deploy
```

#### 解説１）環境の分岐

```yaml
    - name: set environment variables for QA
      if: github.ref == 'refs/heads/develop'
      env:
        AWS_ACCOUNT_ID: 123456789012
      run: |
        echo "::set-env name=AWS_ACCOUNT_ID::${AWS_ACCOUNT_ID}"
```

`if: github.ref == 'refs/heads/develop'` の部分でマージされたブランチを判定しています。
上記の場合は、マージされたブランチによって、環境変数`AWS_ACCOUNT_ID`の値を変えています。

#### 解説2）aws-cliのcredentials設定

```yaml
    - name: configure aws credentials for QA
      if: github.ref == 'refs/heads/develop'
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_QA }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_QA }}
        aws-region: ap-northeast-1
```
AWS公式の`configure-aws-credentials`というActionsを使用して、アクセスキー認証情報を環境変数にセットします。
以降、その権限でaws-cliを使うことができるようになります。
ここでも前述の分岐が使われており、マージされたブランチによってQA／本番環境のどのAWSリソースにアクセスするかを切り替えています。

#### 解説3）docker build,tag,push

```yaml
    - name: build, tag, and push image to ecr
      id: build-image
      env:
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        IMAGE_TAG: ${{ github.sha }}
      run: |
        docker build -t fargate-example-nginx:latest nginx
        docker tag fargate-example-nginx:latest $ECR_REGISTRY/fargate-example-nginx:latest
        docker tag fargate-example-nginx:latest $ECR_REGISTRY/fargate-example-nginx:$IMAGE_TAG
        docker push $ECR_REGISTRY/fargate-example-nginx:latest
        docker push $ECR_REGISTRY/fargate-example-nginx:$IMAGE_TAG
```

docker buildしてtag付けしてECRにPushしています。
`latest`と`$IMAGE_TAG`の２つタグ付け＆Pushしている理由は、運用上個別に戻したい場合に備えて`$IMAGE_TAG`をつけています。
常に最新でよければlatestだけでも問題ないです。

#### 解説4）ecspressoによるデプロイ

```yaml
    - name: download ecspresso
      env:
        ver: v0.13.6
      run: |
        curl -sLo ecspresso.zip \
          https://github.com/kayac/ecspresso/releases/download/$ver/ecspresso-$ver-linux-amd64.zip
        unzip ecspresso.zip
        mv $(ls -1 ecspresso-*-linux-amd64) ecspresso
      working-directory: ./deploy

    - name: deploy to ecs
      run: ./ecspresso deploy --config ecspresso.yml --tasks=-1
      working-directory: ./deploy
```

`download ecspresso`の部分でecspressoバイナリをダウンロードしています。
`deploy to ecs`の部分でecspressoを実行しています。
--tasks=-1を指定することで、現在のタスク実行数を引き継いでくれます。（つまり、オートスケールに対応できます）

#### 修正１）対象ブランチ指定

ブランチマージ時にワークフローが動く条件となるブランチを指定します。
下記差分の通りに修正します。

```diff:
on:
  push:
    branches:
-      - dummy
-      #- master
-      #- develop
+      - master
+      - develop
```
これにより、develop/masterブランチへのマージでこのワークフローが動きます。

#### 修正2）ECSに渡す環境変数を修正

ecspresso(+その先のコンテナ)に渡す環境変数を、環境(QA/本番)に応じて切り替えることができます。
ワークフローファイルの下記の部分を実際のAWSアカウントIDに修正します。
また、コンテナに渡す環境変数を追加したい場合は以下のGOAPP_TEST_VARを参考に追加してください。
Actionsでstepを跨いで有効な環境変数を設定する場合は `::set-env name=~`を使います。 [参考](https://help.github.com/ja/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable)

```diff:
    - name: set environment variables for QA
      if: github.ref == 'refs/heads/develop'
      env:
-       AWS_ACCOUNT_ID: 123456789012
+       AWS_ACCOUNT_ID: 123456789012
+       GOAPP_TEST_VAR: "test var in qa"
      run: |
        echo "::set-env name=AWS_ACCOUNT_ID::${AWS_ACCOUNT_ID}"
+       echo "::set-env name=GOAPP_TEST_VAR::${GOAPP_TEST_VAR}"

    - name: set environment variables for Production
      if: github.ref == 'refs/heads/master'
      env:
-       AWS_ACCOUNT_ID: 234567890123
+       AWS_ACCOUNT_ID: 234567890123
+       GOAPP_TEST_VAR: "test var in prod"
      run: |
        echo "::set-env name=AWS_ACCOUNT_ID::${AWS_ACCOUNT_ID}"
+       echo "::set-env name=GOAPP_TEST_VAR::${GOAPP_TEST_VAR}"
```

Actionsで指定された環境変数が、ecspresso実行時に[タスク定義JSON](https://github.com/miyaz/fargate-example/blob/master/deploy/ecs-task-def.json)に渡されるように修正します。

```diff:deploy/ecs-task-def.json
      "environment": [
        {
          "name": "GOAPP_HOST",
          "value": "127.0.0.1"
        },
+       {
+         "name": "GOAPP_TEST_VAR",
+         "value": "{{ must_env `GOAPP_TEST_VAR` }}"
+       },
        {
          "name": "TZ",
          "value": "Asia/Tokyo"
        }
      ],
```

上記のように、GitHubActionsのワークフローに、ブランチ（環境）ごとに環境変数を定義することで、ビルドコマンドやデプロイ先を切り替えたり、コンテナに渡す環境変数を切り替えることができます。

これらの修正が完了したら、developブランチにマージ(orプッシュ)してください。
GitHubサイト上の`Actions`タブを表示するとワークフローが動いていると思います。
下記は、デプロイまで正常修正した場合の表示です。（スキップされたステップ以外は緑の✔︎がつきます）

<img width="600" alt="deploy.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/51108/087d0d28-6662-dbeb-1566-5c088e4ac689.png">

ALBのURLに再度アクセスしてみると、アプリが更新され表示が変わっている（リクエストヘッダ一覧が表示されている)と思います。
あとは developをmasterブランチにマージすると本番環境へのデプロイが動くと思います。

# おまけ

### 既存ECSクラスタをCDする場合

今回は、ALB+ECSを作成するところからの手順でしたが、既存のECSクラスタをGitHubActionsのCDに対応させたい場合があります。
その場合は、ecspresso initを実行してください。既存環境の情報を吸い上げて設定ファイルを作成してくれます。

```terminal
$ ecspresso init --region ap-northeast-1 --cluster fargate-example --service fargate-example --config ecspresso.yml
2020/06/22 17:47:39 fargate-example/fargate-example save service definition to ecs-service-def.json
2020/06/22 17:47:39 fargate-example/fargate-example save task definition to ecs-task-def.json
2020/06/22 17:47:39 fargate-example/fargate-example save config to ecspresso.yml
```

ecs-task-def.jsonに対して、go-configの形式で環境変数の埋め込めばOKです。
ecs-service-def.jsonは今回の使い方（タスク入れ替えのみ）であれば不要なので消して問題ありません。

### デプロイに時間かかる場合

ALBのヘルスチェック閾値を調整するとecspressoがタスクを追加した時にターゲットグループへの追加が早くなります。
登録解除の遅延を短くした場合にも、デプロイが早くなります。
（運用上問題のない値にしてください）

# まとめ

GitHubActionsは、環境変数の扱いなど少しクセありますが、慣れてしまえばCircleCIに劣らず使いやすいと思います。
またecspressoはシンプルでサイドカーにも対応していて使いやすいのでこれからも使っていこうと思います。