AWS ECS でDBMigrationを自動化する

# はじめに
こんにちわ。wano株式会社エンジニアのnariと申します。
今回はECSのrun taskを利用した、**CD pipeline上でのDBmigrationの実行**について記事にしたいと思います。

# システム全体像
<img width="687" alt="スクリーンショット 2019-09-11 02.31.56.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321850/7be6212e-bebc-d097-d45e-9d412b510908.png">

# 前提
- 今回のprojectではCD pipelineをCodeシリーズで構築していたので、**ついでにbuild終了時にoptionでdb migrateが走るようにもしてみた**
- 正直テーブルの変更は込み入った事象が多く、複雑なオペレーションが必要とされることが多いため、これだけのオペレーションで済むかは定かではない(その実験も兼ねた運用)
- [GitHub - golang-migrate/migrate: Database migrations. CLI and Golang library.](https://github.com/golang-migrate/migrate) をmigrationツールとして使用(Go製でバイナリーで取ってこれる)


# どうオペレーションするか
## 1. ${PROJECT_DIR}/resources/db/migrations に変更したい内容のsqlを入れる
スキーマのアップグレード用のマイグレーションファイルは`YYYYMMDD_$ファイル名.up.sql`というファイル名にする。逆にダウングレード用は`YYYYMMDD_$ファイル名.down.sql`といった命名規則。ここのNはマイグレーションバージョンを意味する。
## 2.${PROJECT_DIR}/infra/build_ci/ad/buildspec.ymlのmigrate箇所のコメントアウトを外す

```sh
  post_build:
    commands:
      ...
      ## db migrationしたい場合は以下3行のコメントアウトを外す
      - aws ecs run-task --launch-type EC2 --cluster cluster-hoge-${ENV} --task-definition ${ENV}-db-migration-ecs  > run-task.log
      - TASK_ARN=$(jq -r '.tasks[0].taskArn' run-task.log)
      - aws ecs wait tasks-stopped --cluster arn:aws:ecs:ap-northeast-1:${ACCOUNT_ID}:cluster/cluster-${ENV} --tasks $TASK_ARN
      ...
```

## 3.stage環境ならremote(gitlab)のstage/xxxx,prod環境ならprod/xxxxにpushする(本番環境の場合要pullrequest)

- 上記CD pipelineのcodebuildのpost_buildフェーズで、db migrateのtaskが実行される

## 4.結果をslack通知で確認する

<img width="491" alt="スクリーンショット 2019-08-28 13.18.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321850/35f4f42e-3f2e-6b37-7488-7b1cd2a4e7f9.png">


# どう実装したか
- アプリケーション用のイメージをtask_definitionで**entrypointを上書きする事**でdb-migration用タスクを定義する
- この上書きするentrypointは、**事前にコンテナ内でプロビジョニングしたentry_point.shを実行する**

```container_definition.json
[
  {
    "name": "db-migration",
    "image": "xxxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/prod-app:latest",
    "essential": true,
    "secrets": [
      {
        "name": "MIGRATION_DB",
        "valueFrom": "/prod/migration"
      }
    ],
    "environment": [
      {
        "name": "ENV",
        "value": "prod"
      }
    ],
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-region": "ap-northeast-1",
        "awslogs-stream-prefix": "prod",
        "awslogs-group": "/ecs/migration/"
      }
    },
    "entrypoint": ["./entry_point.sh"]
  }
]

```

- entry_point.shでは、**migrationの実行と、slackへの結果通知を行なっている**
- multi stage buildフェーズでmigrationツールのバイナリを入れている
- migration downの実装は、**expectで対話型コマンド自動化**している。少し無理やりな気もするので、auto-approveフラグを是非実装追加した欲しい。。(プルリクしろって話ですね。。)

```Dockerfile
# アプリケーション用のイメージ
ARG GO_VERSION=1.12.4

FROM golang:${GO_VERSION}-alpine AS builder

RUN apk add --no-cache git

##### source build
WORKDIR /root/go/src/xxxxxxxxx/hoge/hoge/


# こうするとmodファイルに変更があった時しかdownload走らない
ENV GO111MODULE=on
ENV GOPROXY=https://proxy.golang.org
ENV GOPRIVATE=*.gitlab.hoge.co.jp

COPY go.mod .
COPY go.sum .
# Because of how the layer caching system works in Docker, the  go mod download
# command will _ only_ be re-run when the go.mod or go.sum file change
RUN go mod download

FROM builder AS app_builder
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go install  ./server_src/app/ad_server


###### multi stage build
FROM alpine

RUN apk --update add tzdata && \
    cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
    apk del tzdata && \
    rm -rf /var/cache/apk/*

# dbmigrateツールdownload
ENV MIGRATE_VERSION="v4.6.1"
RUN apk --update add curl expect && \
    curl -L https://github.com/golang-migrate/migrate/releases/download/$MIGRATE_VERSION/migrate.linux-amd64.tar.gz | tar xvz && \
    rm -rf /var/cache/apk/*

ARG ENV=stage
ARG MIGRATION=/resources/db/migrations/
ARG SLACK_SH=/script/migration_report_to_slack/migration_result_to_slack.sh

COPY --from=app_builder /go/bin/ad_server ad_server
COPY --from=app_builder ${MIGRATION} migrations/
COPY --from=app_builder ${SLACK_SH} migration_result_to_slack.sh
RUN chmod 755 migration_result_to_slack.sh

# dbmigrate用のentry_point.shを作成
RUN echo $'#!/bin/sh \n\
#migrate downしたかったら以下のexpect部分をコメントアウトすべし\n\
#expect -c "\n\
#spawn ./migrate.linux-amd64 -database $MIGRATION_DB -path ./migrations down\n\
#expect \\"Are you sure you want to apply all down migrations? \[y/N\]\\"\n\
#send -- \\"y\\n\\"\n\
#expect \\"Applying all down migrations\\"\n\
#expect {\n\
#    -regexp \"\\n.*\\r\" {\n\
#        send \"exit\\n\"\n\
#    }\n\
#}\n\
#"\n\
\n\
#migrate upと結果およびversionをslackに通知\n\
./migrate.linux-amd64 -database $MIGRATION_DB -path ./migrations up 2>> migration_result.log \n\
chmod 644 migration_result.log \n\
export MIGRATION_RESULT=$(cat migration_result.log) \n\
./migrate.linux-amd64 -database $MIGRATION_DB -path ./migrations version 2> migration_version.log \n\
chmod 644 migration_version.log \n\
export MIGRATION_VERSION=$(cat migration_version.log) \n\
./migration_result_to_slack.sh' >> entry_point.sh
RUN chmod 755 entry_point.sh

# 後でecs-taskの定義で上書きされる
ENTRYPOINT ["./ad_server"]


```

# 参考文献
- [EC2 起動タイプを使用してタスクを実行する - Amazon Elastic Container Service](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs_run_task_ec2.html)
- [運用中のシステムにも導入可なGo製のマイグレーションツール「mattes/migrate」 - Qiita](https://qiita.com/suin/items/68629018017141155f06) 
- [LaravelをElastic BeanstalkからFargateに移行しました - Qiita](https://qiita.com/hareku/items/73f7730c1adc01bbe5a0)