CircleCI Orbsを使ってRailsのECR・ECSへのデプロイ（マイグレーション込み）を自動化した話

# TL;DR

- ECS の RunTask を実行でマイグレーション込みのデプロイが自動化できた

# いきなりですが

2 ヶ月前ぐらいまで下記の手順でデプロイを行っていました

@ローカル

1. コンテナ作成 (docker build)
2. コンテナにタグづけ (docker tag)
3. ECR のコンテナイメージを更新 (docker push)
4. AWS の RDS にマイグレーション実行 (rails db:migrate)
5. AWS コンソール画面で ECS の手動でリビジョン更新
6. サービスでタスク定義の更新
7. 新旧タスクが入れ替わるのを見守る

現在は・・・

1. デプロイブランチ(production や statging)にプッシュ
2. slack で結果を待つ

# 一番の壁、マイグレーション実行

実は、今回のこの記事でお伝えしたいのは前述の中でも 「4 の RDS のマイグレーション」の部分です:santa:

ローカルで実行していたころは、許可された IP アドレスでマイグレーション実行できるのですが、

CircleCI は AWS の外部で実行されていて、かつ IP アドレスが不定です。
それ故、許可されていない IP アドレスで実行される CicleCI のマイグレーションコマンドを RDS が弾いてしまい、マイグレーション実行ができません:santa:

# Run Task で乗り越えましょ

マイグレーション前に ECR にデプロイした最新の schema ファイルを持ったイメージを使用して[ECS の RunTask](https://docs.aws.amazon.com/cli/latest/reference/ecs/run-task.html)を行うことで、
AWS 内でのマイグレーション実行を行えるようになりました:santa:

これで実行元の IP アドレスは考えなくてもよくなります:santa:

Run Task は、AWS 内の ECR からイメージを使用して、docker run するような動きになります:santa:

```
現在動いているコンテナとは別に、マイグレーションするためだけのコンテナが立ち上がる📦

↓

マイグレーション実行🐳

↓

コンテナ終了
```

# ポイント

- Run Task 前に ECR に最新イメージをプッシュしていること(Run Task を行う前に、最新の schema ファイルを持ってないとマイグレーションが滑る)
  → [CircleCI の requires](https://circleci.com/docs/ja/2.0/workflows/#%E3%82%B7%E3%83%BC%E3%82%B1%E3%83%B3%E3%82%B7%E3%83%A3%E3%83%AB%E3%82%B8%E3%83%A7%E3%83%96%E3%81%AE%E4%BE%8B)で順番を担保
- 実行してもらいたいコマンドを override という形で json で渡す
  → AWS CLI を実行

# 対象読者

- CircleCI 2.1 以上
- ECS・ECR 設定済み
- Rails

# レシピ

さてさーて:santa:

長い文脈ではありましたが、どういうふうに調理していくか流れおさえます:santa:

デプロイの手作業は下記でした:santa:

1. コンテナ作成 (docker build)
2. コンテナにタグずけ (docker tag)
3. ECR のコンテナイメージを更新 (docker push)
4. AWS の RDS にマイグレーション実行 (rails db:migrate)
5. AWS コンソール画面で ECS の手動でリビジョン更新
6. サービスでタスク定義の更新
7. 新旧タスクが入れ替わるのを見守る

## 上記に加え、「8. 結果を slack 通知」も加えます:santa:

AWS のコンソール見守る手間省き受動的に対処するためですね:santa:

## CircleCI の Orbs で置き換えると

- 1 ~ 3 → CircleiCI Orbs の[aws-ecr/build-and-push-image](https://circleci.com/orbs/registry/orb/circleci/aws-ecr#commands-build-and-push-image)を使用
- 4 → CircleiCI Orbs の[aws-cli/setup](https://circleci.com/orbs/registry/orb/circleci/aws-cli#commands-setup)を使用
- 5 ~ 7 → CircleiCI Orbs の[aws-cli/install](https://circleci.com/orbs/registry/orb/circleci/aws-cli#commands-install)、[aws-cli/setup](https://circleci.com/orbs/registry/orb/circleci/aws-cli#commands-setup)、[aws-ecs/update-service](https://circleci.com/orbs/registry/orb/circleci/aws-ecs#commands-update-service)を使用
- 8 → CircleiCI Orbs の[slack/status](https://circleci.com/orbs/registry/orb/circleci/slack#usage-status)使用

Orbs だけでいけそうですね:santa:

Orbs を開発してくれている方々、本当に感謝です:santa:

### 1 ~ 3 → CircleiCI Orbs の[aws-ecr/build-and-push-image](https://circleci.com/orbs/registry/orb/circleci/aws-ecr#commands-build-and-push-image)を使用

config.yml は下記のようになりました:santa:

あくまで自分のプロジェクトの場合です:santa:
必須なパラメーターは[ドキュメント](https://circleci.com/orbs/registry/orb/circleci/aws-ecr#commands-build-and-push-image)をみてください〜

```.circleci/config.yml
version: 2.1

orbs:
  aws-ecr: circleci/aws-ecr@6.2.0

executors:
  (省略)

jobs:
  build-image-and-push:
    executor: container
    parameters:
      env:
        type: enum
        enum: ['stg', 'prod']
    steps:
      - aws-ecr/build-and-push-image:
          aws-access-key-id: AWS_ACCESS_KEY_ID
          aws-secret-access-key: AWS_SECRET_ACCESS_KEY
          region: AWS_REGION
          repo: '${AWS_RESOURCE_NAME_PREFIX}-<< parameters.env >>'
          extra-build-args: --target apps
          attach-workspace: true
          setup-remote-docker: true

```

Orbs 使って、パラメーター渡しただけで終わってしまいました:santa:

#### ポイント

- Orbs 使っているかどうかは、Orbs の key 名と、steps で`aws-ecr/build-and-push-image`で、どの job を参照しているかがわかります:santa:

- [parameters](https://circleci.com/docs/ja/2.0/reusing-config/#parameters-%E5%AE%A3%E8%A8%80%E3%81%AE%E4%BD%BF%E7%94%A8) で env を設定

parameters を使う意図としては、prodction → 本番環境、staging → テスト環境で同じ job を定義したいけど、違いが、AWS の ECR のリポジトリ名しかない時に、コンフィグを再利用できるので便利です:santa:

##### 例：parameters を使わない時

`repo`の違いだけなのに、2 倍定義しないといけない

```.circleci/config.yml
jobs:
  build-prod-image-and-push:
    executor: container
    steps:
      - aws-ecr/build-and-push-image:
          aws-access-key-id: AWS_ACCESS_KEY_ID
          aws-secret-access-key: AWS_SECRET_ACCESS_KEY
          region: AWS_REGION
          repo: '${AWS_RESOURCE_NAME_PREFIX}-prod'
          extra-build-args: --target apps
          attach-workspace: true
          setup-remote-docker: true

  build-stg-image-and-push:
    executor: container
    steps:
      - aws-ecr/build-and-push-image:
          aws-access-key-id: AWS_ACCESS_KEY_ID
          aws-secret-access-key: AWS_SECRET_ACCESS_KEY
          region: AWS_REGION
          repo: '${AWS_RESOURCE_NAME_PREFIX}-stg'
          extra-build-args: --target apps
          attach-workspace: true
          setup-remote-docker: true
```

##### 例：parameters を使うと ↓

config を再利用できますね:santa:

```.circleci/config.yml
jobs:
  build-image-and-push:
    executor: container
    parameters:
      env:
        type: enum
        enum: ['stg', 'prod']
    steps:
      - aws-ecr/build-and-push-image:
          aws-access-key-id: AWS_ACCESS_KEY_ID
          aws-secret-access-key: AWS_SECRET_ACCESS_KEY
          region: AWS_REGION
          repo: '${AWS_RESOURCE_NAME_PREFIX}-<< parameters.env >>'
          extra-build-args: --target apps
          attach-workspace: true
          setup-remote-docker: true

```

- 必要なパラメーター(AWS_ACCESS_KEY_ID など)は、CircleCI から設定した環境変数から渡しているだけですね:santa:

CircleCI での環境変数の設定は[こちら](https://circleci.com/docs/ja/2.0/env-vars/)

- [docker の multi stage build](https://docs.docker.com/develop/develop-images/multistage-build/)でも大丈夫
  `extra-build-args`で docker コマンドに引数を渡せるので対象の layer だけのビルドも可能ですね:santa:

〜〜〜余談〜〜〜

multi stage build は、コンテナを小さくする手段でもあり、最近発表された[[レポート] コンテナおよび Kubernetes のベストプラクティストップ 5 #reinvent #CON307](https://dev.classmethod.jp/cloud/aws/reinvent2019-container-best-practices/)にも載っていましたね:santa:

### 4 → CircleiCI Orbs の[aws-cli/setup](https://circleci.com/orbs/registry/orb/circleci/aws-cli#commands-setup)を使用

```.circleci/config.yml
orbs:
  aws-cli: circleci/aws-cli@0.1.16

jobs:
  db-migrate-on-task-run:
    executor: container
    parameters:
      env:
        type: enum
        enum: ['stg', 'prod']
    steps:
      - checkout
      - aws-cli/setup:
          aws-access-key-id: AWS_ACCESS_KEY_ID
          aws-secret-access-key: AWS_SECRET_ACCESS_KEY
          aws-region: AWS_REGION
      - run:
          name: "db migrate"
          command: |
            aws ecs run-task --region $AWS_REGION \
              --cluster hoge-<< parameters.env >> \
              --task-definition hoge-service-<< parameters.env >> \
              --overrides file://docker/run_task_db_migrate_<< parameters.env >>.json
```

Rails のフォルダには、RunTask に必要なファイルが入っております:santa:

```
docker
├── run_task_db_migrate_prod.json
└── run_task_db_migrate_stg.json
```

name は container 名が入り、それが環境違いで用意されているだけですね:santa:

```run_task_db_migrate_prod.json
{
  "containerOverrides": [
    {
      "name": "hoge-prod",
      "command": ["rails", "db:migrate"]
    }
  ]
}
```

```run_task_db_migrate_stg.json
{
  "containerOverrides": [
    {
      "name": "hoge-stg",
      "command": ["rails", "db:migrate"]
    }
  ]
}
```

#### ポイント

- aws-cli コマンドで run task を実行するため、aws-cli のセッティングをする

[ECS の Orbs](https://circleci.com/orbs/registry/orb/circleci/aws-ecs)でも run-task 実行できそうですね！
これ実装して検証してた時はなかったので、同じ Orbs でもできる内容が広がってますね！

できてしまえば、あっさりですね:santa:

### 5 ~ 8 → CircleiCI Orbs の[aws-cli/install](https://circleci.com/orbs/registry/orb/circleci/aws-cli#commands-install)、[aws-cli/setup](https://circleci.com/orbs/registry/orb/circleci/aws-cli#commands-setup)、[aws-ecs/update-service](https://circleci.com/orbs/registry/orb/circleci/aws-ecs#commands-update-service)、[slack/status](https://circleci.com/orbs/registry/orb/circleci/slack#usage-status)を使用

```.circleci/config.yml
orbs:
  aws-ecs: circleci/aws-ecs@0.0.11
  aws-cli: circleci/aws-cli@0.1.16
  slack: circleci/slack@3.3.0

  service-update-and-notify:
    executor: container
    parameters:
      env:
        type: enum
        enum: ['stg', 'prod']
    steps:
      - checkout
      - aws-cli/install
      - aws-cli/setup:
          aws-access-key-id: AWS_ACCESS_KEY_ID
          aws-secret-access-key: AWS_SECRET_ACCESS_KEY
          aws-region: AWS_REGION
      - aws-ecs/update-service:
          family: hoge-apps-service-<< parameters.env >>
          cluster-name: hoge-<< parameters.env >>
          container-image-name-updates: 'container=hoge-<< parameters.env >>,tag=latest'
          verify-revision-is-deployed: true
          max-poll-attempts: 300
          poll-interval: 10
      - slack/status:
          success_message: ':circleci-pass: $CIRCLE_BRANCH のデプロイが完了しました\n:github_octocat: User：$CIRCLE_USERNAME'
          failure_message: ':circleci-fail: $CIRCLE_BRANCH のデプロイが失敗しました\n:github_octocat: User：$CIRCLE_USERNAME'
          webhook: '${SLACK_WEBHOOK}'
```

- `max-poll-attempts`と`poll-interval`で新旧見守りしなくていいっすね

300 秒間、10 秒に 1 回切り替わったかねー？:santa:

って、聞いている処理してくれてる認識です:santa:

shell とかで実装している例とかありますが、これだけで OK ですね:santa:

- 結果は slack で

steps の`aws-ecs/update-service`が成功ステータスだと`success_message`、ダメだと`failure_message`が流れます:santa:

デプロイがうまくいかない理由はいろいろありますが、見守り時間内にデプロイ終わらなかったのがほとんどです:santa:

この slack の Orbs のいいところは、結果を条件分岐をする必要がないところがシンプルでいいですね:santa:

細かい設定方法については[こちら](https://qiita.com/k_bobchin/items/11f0d778de09502de1f3)

### これらを workflow で流れを定義

```.circleci/config.yml
workflows:
  db-migrate-and-deploy-to-stg:
    jobs:
      - build-image-and-push:
          env: stg
          filters:
            branches:
              only: staging
      - db-migrate-on-task-run:
          requires:
            - build-image-and-push
          env: stg
          filters:
            branches:
              only: staging
      - service-update-and-notify:
          requires:
            - db-migrate-on-task-run
          env: stg
          filters:
            branches:
              only: staging

  db-migrate-and-deploy-to-prod:
    jobs:
      - build-image-and-push:
          env: prod
          filters:
            branches:
              only: production
      - db-migrate-on-task-run:
          requires:
            - build-image-and-push
          env: prod
          filters:
            branches:
              only: production
      - service-update-and-notify:
          requires:
            - db-migrate-on-task-run
          env: prod
          filters:
            branches:
              only: production
```

#### ポイント

- 流れはあくまで、[workflow](https://circleci.com/docs/ja/2.0/workflows/)で
- filters で対象ブランチ絞る
- 実行の順番は[requires](https://circleci.com/docs/ja/2.0/workflows/#%E3%82%B7%E3%83%BC%E3%82%B1%E3%83%B3%E3%82%B7%E3%83%A3%E3%83%AB%E3%82%B8%E3%83%A7%E3%83%96%E3%81%AE%E4%BE%8B)で制御

例えば、staging に push が走ると

1. build-image-and-push
2. db-migrate-on-task-run
3. service-update-and-notify

の順番で job が実行されていきます:santa:

【直列実行イメージ図】
![screenshot 2019-12-06 14.18.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183059/79436602-0225-b2c0-fc43-9176d930fac6.png)


requires 定義しないと、すべての job が並列で実行されます:santa:

【並列実行イメージ図】
![screenshot 2019-12-06 14.18.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183059/2b9c3f01-6d14-3a03-5d25-38989674d9d1.png)

[イメージの参照元](https://circleci.com/docs/ja/2.0/workflows/#%E3%82%B7%E3%83%BC%E3%82%B1%E3%83%B3%E3%82%B7%E3%83%A3%E3%83%AB%E3%82%B8%E3%83%A7%E3%83%96%E3%81%AE%E4%BE%8B)

# 参考

- [ECS RunTask](https://docs.aws.amazon.com/cli/latest/reference/ecs/run-task.html)
- [CircleCI Orbs](https://circleci.com/docs/ja/2.0/orb-intro/)
- [CircleCI Workflow](https://circleci.com/docs/ja/2.0/workflows/)
- [[レポート] コンテナおよび Kubernetes のベストプラクティストップ 5 #reinvent #CON307](https://dev.classmethod.jp/cloud/aws/reinvent2019-container-best-practices/)

# あとがき

CircleCI という AWS の外でも、Run Task ができることで、乗り越えられる壁があることを知ることができました:santa:

これを応用すれば、E2E テストとか外部から DB にアクセスを拒否されるようなケースも
Run Task で E2E テストするSPA コンテナだけ立ち上げてテスト実行ができそうですね:santa:

これを Orbs なしで実装することになっていたら背筋に稲妻が走りそう（白目）

Orbsの開発者様たち、本当にありがとうございます:santa: