ECS Fargateでブランチ毎にQA環境を作れるようにする

## 目次
1. [前書き](#前書き)
    1. [概要](#概要)
    2. [動機](#動機)
2. [本文](#本文) 
    1. [利用した主なツール](#利用した主なツール)
    2. [1の解説](#1の解説)
    3. [2の解説](#2の解説)
    4. [3の解説](#3の解説)
    5. [4の解説](#4の解説)
    6. [5の解説](#5の解説)
3. [終わりに](#終わりに)
4. [参考文献](#参考文献)

## 前書き
### 概要
ECSでブランチごとの動作確認用の環境を準備したので、そのノウハウを共有します。
作った環境はこんな感じです。

![Untitled Diagram (4).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/158622/1bf4f1c8-6006-6709-0d76-ce8f6b8dfbde.png)

## 動機
ディレクターが、エンジニアが開発中のブランチの挙動確認をするのに「 `git pull` して `docker-compose up` して、メモリが足りなくてビルド終わらなくて…」というのを見て「大変だなー」と思い、パッと動作確認できる環境を作ろうと思いました。

## 本文
### 利用した主なツール

- aws-cli
- ecs-cli
- docker
- docker-compose

### コンテナの構成
fargateで起動するコンテナ群はこんな感じです

| コンテナ名 | 内容                       |
|------------|----------------------------|
| app        | Nginx+プロダクションコード |
| mysql      | DB                         |
| redis      | バッチジョブ用のキュー     |

今回は、これを一つのタスクとして起動させています。
docker-compose.ymlで80番ポートを空けて `インターネット → nginx → プロダクションコード` という流れで通信が行われます。

またfargate特有の事情としては、コンテナ間通信の名前解決がコンテナ名でなくlocalhostとなることなので、それは注意が必要です。
（自分はここで結構ハマりました…）

### 1の解説
現在、ブランチがプッシュされたらCircleCIでphpunitを実行させています。
その工程に、docker imageのビルドとECRへのプッシュを追加します。
実際のコードはこんな感じです。

```config.yml
      - run:
          name: Install Docker client
          command: |
            set -x
            VER="19.03.5"
            curl -L -o /tmp/docker-$VER.tgz https://download.docker.com/linux/static/stable/x86_64/docker-$VER.tgz
            tar -xz -C /tmp -f /tmp/docker-$VER.tgz
            mv /tmp/docker/* /usr/bin
      - run:
          name: Install ecs-cli
          command: |
            set -x
            curl -o /usr/bin/ecs-cli https://amazon-ecs-cli.s3.amazonaws.com/ecs-cli-linux-amd64-latest
            chmod +x /usr/bin/ecs-cli
      - run:
          name: push docker image to AWS ECR
          command: |
            BRANCH_HASH=`echo $CIRCLE_BRANCH | sed 's:/:_:g'`-`git rev-parse HEAD`
            docker build -f Dockerfile_Ecs -t {{registory_id}}.dkr.ecr.ap-northeast-1.amazonaws.com/{{repository}}:${BRANCH_HASH} .
            ecs-cli push {{registry_id}}.dkr.ecr.ap-northeast-1.amazonaws.com/{{repository}}:${BRANCH_HASH}
```

ECRの運用としては、一つのレポジトリに対して、「ブランチ名+コミットハッシュ」でタグ付けをしたイメージをプッシュすることにしています。
これでレポジトリにライフサイクルポリシーを設定することで、ECRの容量が肥大化することが防げます。
ただ、イメージのタグに `/` が使えないので `_` で置換しています。

Dockerfile_Ecsの内容としては、ビルド済みのプロダクションコードを `COPY` して、nginxの設定ファイル等を 適切な場所に配置って感じです。

### 2の解説
ユーザーがブランチ名を引数としてスクリプトを実行したら、URLが出力されるようにします。
このURLからECSで起動したサービスにアクセスすることができれば、ユーザーはビルドやgit操作が不要となります。

### 3の解説
今回の肝となるのは、4の 

```bash 
ecs-cli compose --project-name ${branch+hash} service up
``` 
となります。
しかし、そのためには入力された、ブランチ名から `${branch+hash}` を取得する必要があります。
そのために ECRでイメージ一覧を取得し、ブランチ名の前方一致でフィルターし、そのイメージのタグを取得することで `${branch+hash}` を取得します。

### 4の解説
今回で一番重要なのが、この処理となります。
図ではスペースの都合でコマンドを省略しましたが、省略しないと下記になります。

```bash
BRANCH_HASH=${branch+hash} ecs-cli compose \
  -c {{cluster_name}} \
  -f docker-compose-ecs-base.yml -f docker-compose-ecs.yml \
  --ecs-params docker/ecs/ecs-params.yml \
  --project-name ${branch+hash} \
  service up \
  --launch-type FARGATE
```

環境変数 `BRANCH_HASH` に値を渡して、それを下記の docker-compose.yml で取得しています。

```yml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile_Ecs
    image: "{{registry_id}}.dkr.ecr.ap-northeast-1.amazonaws.com/{{repository}}:${BRANCH_HASH}"
    ports:
      - "80:80"
    command: docker/ecs/entrypoint-app.sh
```

これによって `ecs-cli compose service up` の実行時に、どのタグのイメージをECSで立てるかを決定しています。
ECSはこのコマンドを受けて、タスク定義の作成、ECRからimageの取得、サービス+タスクの起動を行ってくれます。
そして、最後に `entrypoint-app.sh` で、migration、シードデータの作成、nginx起動などのコンテナ起動に必要な処理を行っています。

### 5の解説
今回は、4でECSのサービス+タスクがパブリックサブネットで作成されています。
そのため、そのタスクにはパブリックIPが付与されています。
そのパブリックIPを `aws ecs describe-tasks` 等を利用して取得して、そのIPと `${branch+hash}` を紐付けて、 Route53でAレコードを作成します。

ユーザーは、このAコードのURLを使うことで、ECSのサービスにアクセスできるようになります。

（パブリックIPが自動的に変更されて、URLとIPの紐付けが変わる可能性が高いような気はしますが、サービスは毎晩に削除しようと思ってるので、まぁ大丈夫かな？という予想です）

## 終わりに

続きの作業としては

- slackopsにする
- ECSのサービスを自動的に削除する
- Route53のレコードを自動的に削除する

をやりたいなーと思います。

ただslackops対応が意外と難しい…

## 参考
下記を参考にさせていただきました。
ありがとうございました :bow:

- https://y-ohgi.com/introduction-docker/3_production/dockerfile/
- http://iga-ninja.hatenablog.com/entry/2018/11/04/133540
- https://blog.frevo-works.co.jp/entry/2019/03/04/133336
- https://techblog.lclco.com/entry/2018/11/02/120000
- https://circleci.com/docs/ja/2.0/building-docker-images/