RailsアプリをEC2からECS/Fargate構成に移行してホストする【CircleCI連携編】

# はじめに

当記事は、【ECS準備編】【ECS起動編】【HTTPS/ドメイン編】の続編です。

https://qiita.com/Shuhei_Nakada/items/9e4c51bf9ae59f0b1a2b

https://qiita.com/Shuhei_Nakada/items/247d6f1781d6a3765482

https://qiita.com/Shuhei_Nakada/items/a7f2fd64f0af9663ed29


まだお読みでない方は、そちらからご確認ください。



# 使用技術

* ECS/Fargate(blue/greenデプロイメント)
* CircleCI: 2.1
* Rails: 6



# 作業内容

1. ECRへのpush
2. ECSのblue/greenデプロイメント
3. masterブランチにマージ時のみデプロイさせる


# 前提

当記事は、CircleCI Orbを活用した実装方法になります。

CircleCI Orbを使うとCircleCIの設定ファイルをシンプルに構成できるようになります。

前提条件として、CircleCIのバージョンは２.1を使う必要があります。



# 1. ECRへのpush

ここでは`ECRへのpushまで`を目標にして実装していきます。

`使用するOrb`は`circleci/aws-ecr@7.3.0`とします。

CircleCIの公式ドキュメントに軽く目を通してみると、
どんな記述をすれば良いかが明確に分かります。

https://circleci.com/developer/orbs/orb/circleci/aws-ecr

今回は
・公開すべきではない秘匿情報はCircleCIの環境変数
・それ以外はconfig.yml
に分けて実装しています。



## ⑴CircleCIに環境変数を登録する

まず、以下の表に従って、CircleCIに秘匿情報を登録していきましょう。

| 番号 | 項目 | 値 | 備考 |
|:-:|:-:|:-:|:-:|
| 1 | AWS_ECR_ACCOUNT_URL | AWSアカウントID.dkr.ecr.us-west-2.amazonaws.com | ECRからコピペ |
| 2 | AWS_ACCESS_KEY_ID | ランダムな文字列 | IAMからコピペ |
| 3 | AWS_SECRET_ACCESS_KEY | ランダムな文字列 | IAM生成時に個人保管しているもの |
| 4 | AWS_REGION | ap-northeast-1 | 東京リージョンなら左記 |
| 5 | RAILS_MASTER_KEY | ランダムな文字列 | Railsアプリ作成時に自動生成 |

補足すると
　1.と4.は、`ECRの対象リポジトリにアクセスするため`
　2.と3.は、`AWSの個人アカウントにアクセスするため`
　5.は、`Railsがcredential.ymlファイルの解錠に使うため`
で必要な情報です。

登録する手順は以下の通りです。

CircleCIの`Projects`タブから`対象のプロジェクト`をクリックします。

![スクリーンショット 2021-12-15 14.06.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/7a19440f-1eae-6388-6f99-40907797ea77.png)

右側の`Project Settings`をクリックします。

![スクリーンショット 2021-12-15 14.08.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/11a56fb7-2cb2-9b6b-b5b5-a065b0881495.png)

`Environment Variables`タブから`Add Environment Variable`をクリックします。

![スクリーンショット 2021-12-15 14.10.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/c95ec806-1eb1-c82a-26da-13ce5be1c8d4.png)

あとは、先ほどの表に従って`５つの秘匿情報`を登録します。

![スクリーンショット 2021-12-16 7.28.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/b8fb80a8-94e5-f27b-788f-d9830d5323f1.png)



## ⑵Dockerfileから.circleci/config.ymlに環境変数を渡す

まず、ここから各ファイルの編集に入りますので
作業ブランチを切っておきましょう。

```sh:ターミナル

# 現時点でファイル変更がないことを確認する
% git status

# 新しくブランチを作成して移動する
% git checkout -b new-branch-circleci-test

# 現在いるブランチの確認
% git branch

```

では、本番用のDockerfileを編集していきます。

先ほど、CircleCIに登録した環境変数のうち
`マスターキー`は`Railsコンテナに渡す`必要があります。
(他の４つはCircleCIが暗黙的にECRへのpush時に利用します。)

そのため、少々周りくどいですが、
`ARG`で`CircleCI環境変数をDockerfile.productionで受け取り、`
`ENV`で`それを.circleci/config.ymlに渡しています。`
(受け取った.circleci/config.yml内でRails環境変数にセットします。)

なお、他コードは当記事の趣旨に関わっている訳ではないですが、
生成に使うDockerイメージなので、参考のために載せております。

```dockerfile:Dockerfile.production

FROM ruby:2.7.3-alpine

ARG RUNTIME_PACKAGES="bash imagemagick nodejs yarn tzdata mysql-dev mysql-client git"
ARG DEV_PACKAGES="build-base curl-dev"

### CircleCI経由でイメージのプッシュ/デプロイを行う際に使用
ARG RAILS_MASTER_KEY
ENV RAILS_MASTER_KEY ${RAILS_MASTER_KEY}

WORKDIR /app

ENV RAILS_ENV="production"

COPY Gemfile Gemfile.lock /app/

RUN apk update && \
    apk upgrade && \
    apk add --no-cache ${RUNTIME_PACKAGES} && \
    apk add --virtual build-dependencies --no-cache ${DEV_PACKAGES} && \
    bundle install -j4 && \
    apk del build-dependencies

COPY . /app

RUN SECRET_KEY_BASE=placeholder bundle exec rails assets:precompile \
    && yarn cache clean \
    && rm -rf node_modules tmp/cache

ENV RAILS_SERVE_STATIC_FILES="true"

COPY entrypoint.production.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.production.sh
ENTRYPOINT ["entrypoint.production.sh"]
EXPOSE 80

CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0", "-p", "80"]

```

ちなみに、私はマスターキーを登録していなかったので下記エラー文が出ました。

ローカルにはmaster.keyは当然ありますが、
CircleCIにはgitignoreで除外されているので複製されていないので
`CircleCIがマスターキーありませんよー`って忠告してくれてるんですね…

■エラー文：

```
Missing encryption key to decrypt file with. Ask your team for your master key and write it to /app/config/master.key or put it in the ENV['RAILS_MASTER_KEY'].
```


## ⑶CircleCIの設定

では、本題のOrbを用いたCircleCIの設定です。

正直言って、ほとんどCircleCIの公式ドキュメント通りです。

![スクリーンショット 2021-12-15 14.54.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/b41a7e5f-1679-bed8-dcf1-e4b7f8a2affc.png)

以下の2点だけ個人的に編集しています。

`1. イメージは本番用のDockerfile.productionとしているのでファイル名を指定する`
`2. Dockerfile.productionで設定したマスターキー情報を受け取ってセットしている`

`repo:は、AWSのECRページからリポジトリ名をコピペ`すればOKです。


```yml:.circleci/config.yml
version: 2.1
orbs:
  aws-ecr: circleci/aws-ecr@7.3.0
workflows:
  build_and_push_image:
    jobs:
      - aws-ecr/build-and-push-image:
          # ECRに実在するリポジトリ名(任意名称は不可)
          repo: ECRリポジトリ名
          # イメージ生成のもとになるDockerfile
          dockerfile: Dockerfile.production
          # Dockerfile.productionから渡された秘匿情報を受け取ってRails環境変数に登録する
          extra-build-args: '--build-arg RAILS_MASTER_KEY=${RAILS_MASTER_KEY}'
```



## ※イメージpushの検証

これでターミナルから以下コマンドを実行して検証します。

```shell:ターミナル

# 現在のtestブランチを確認
% git branch

# 変更をステージングへ移行
% git add .

# メッセージをつけてコミットする
% git commit -m 'CircleCIからECRにイメージをpushできるか検証する'

# GitHubへプッシュしてCircleCIの検証
% git push origin ローカルリポジトリ

```

あとはAWSのECR画面で新しくイメージが生成されているか確認できればOKです。

![スクリーンショット 2021-12-15 15.10.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/a1d23a74-75c4-ccc5-4775-68845b02846e.png)




# 2. ECSのblue/greenデプロイメント

次は、blue/greenデプロイメントの実装です。

`使用するOrb`は`circleci/aws-ecs@2.2.1`とします。

こちらもCircleCI公式ドキュメントに目を通しましょう。

https://circleci.com/developer/orbs/orb/circleci/aws-ecs

ただし、やることはシンプルで
　`⑴.circleci/config.ymlを編集する`
　`⑵CircleCI環境変数を登録する`
の2作業だけです。



## ⑴.circleci/config.ymlを編集する

`- aws-ecs/deploy-service-update:`以降のコードから解説します。

まず、`requires:`によって
　**`ECRへのイメージをpush後に`**
　**`ECSでデプロイを行う`**
という順番を整えています。

その後は、ECSが動作するように必要な設定です。

以下に表をまとめましたので、整理しながら
ECSやCodeDeployの画面から間違えがないように
コピペしていきましょう。

| 番号 | 項目 | 値 | 備考 |
|:-:|:-:|:-:|:-:|
| 1 | cluster-name: | '作成済みのクラスター名' | ECSからコピペ |
| 2 | service-name: | '作成済みのサービス名' | ECSからコピペ |
| 3 | family: | '作成済みのタスク定義名' | ECSからコピペ |
| 4 | deployment-controller: | 'CODE_DEPLOY' | 左記そのままコピペ |
| 5 | codedeploy-application-name: | '生成されるアプリケーション名' | CodeDeployからコピペ |
| 6 | codedeploy-deployment-group-name: | '生成されるグループ名' | CodeDeployからコピペ |
| 7 | codedeploy-load-balanced-container-name: | '生成されるコンテナ名' | タスク定義からコピペ |


```yml:.circleci/config.yml

version: 2.1
orbs:
  # ECR用のOrb
  aws-ecr: circleci/aws-ecr@7.3.0
  # ECS用のOrb
  aws-ecs: circleci/aws-ecs@2.2.1
workflows:
  # デプロイまで行うので作業名を変更
  build-and-deploy:
    jobs:
      # 先の実装と同じ
      - aws-ecr/build-and-push-image:
          repo: ECRリポジトリ名
          dockerfile: Dockerfile.production
          extra-build-args: '--build-arg RAILS_MASTER_KEY=${RAILS_MASTER_KEY}'
      # ECS用の実装コード
      - aws-ecs/deploy-service-update:
          # 先に「aws-ecr/build-and-push-image」を実行させる依存関係を作る
          requires:
            - aws-ecr/build-and-push-image
          # ECS上に存在するクラスター名
          cluster-name: 'クラスター'
          # ECS上に存在するサービス名
          service-name: 'サービス'
          # ECS上に存在するタスク定義の名称
          family: 'タスク定義'
          # デプロイを実行するコントローラーを指定する
          deployment-controller: 'CODE_DEPLOY'
          # CodeDeploy内のアプリケーション名
          codedeploy-application-name: 'アプリケーション名'
          # CodeDeploy内のグループ名
          codedeploy-deployment-group-name: 'グループ名'
          # タスク定義で設定しているコンテナ名
          codedeploy-load-balanced-container-name: 'コンテナ名'

```

## ⑵CircleCI環境変数を登録する

私の場合は、⑴の作業だけでは下記エラーが発生しました。

どうも`AWS_DEFAULT_REGION`というCircleCI環境変数が設定できてなさそうです。

■エラー文：

```
#!/bin/bash -eo pipefail
aws configure set default.region $AWS_DEFAULT_REGION \
  --profile default
```

ということで、CircleCI環境変数に
下記の通り、**`6 AWS_DEFAULT_REGION`**を追加しました。


| 番号 | 項目 | 値 | 備考 |
|:-:|:-:|:-:|:-:|
| 1 | AWS_ECR_ACCOUNT_URL | AWSアカウントID.dkr.ecr.us-west-2.amazonaws.com | ECRからコピペ |
| 2 | AWS_ACCESS_KEY_ID | ランダムな文字列 | IAMからコピペ |
| 3 | AWS_SECRET_ACCESS_KEY | ランダムな文字列 | IAM生成時に個人保管しているもの |
| 4 | AWS_REGION | ap-northeast-1 | 東京リージョンなら左記 |
| 5 | RAILS_MASTER_KEY | ランダムな文字列 | Railsアプリ作成時に自動生成 |
| 6 ※追加※ | AWS_DEFAULT_REGION | ap-northeast-1 | 東京リージョンなら左記 |



## ※blue/greenデプロイメントの検証

先ほどと同様にターミナルからpushコマンドを実行して検証します。

```shell:ターミナル

# 現在のtestブランチを確認
% git branch

# 変更をステージングへ移行
% git add .

# メッセージをつけてコミットする
% git commit -m 'CircleCIからblue/greenデプロイメントが走るか検証する'

# GitHubへプッシュしてCircleCIの検証
% git push origin ローカルリポジトリ

```

コマンド実行後に、数分待ってから
ECSのサービス画面のイベントタブで
`has reached a steady state`のメッセージが確認できればOKです。


![スクリーンショット 2021-12-15 16.49.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/38e3ebe5-417c-fe67-0d04-b9e2dffaa36a.png)



# 3. masterブランチにマージ時のみデプロイさせる

最後にイメージのプッシュ＆デプロイを行うブランチを制限します。

現行のままでは、`どの作業ブランチでもgit pushした瞬間に
イメージのプッシュ〜デプロイまで走ってしまい、`無駄です…

そのため、`masterブランチが他ブランチからのプルリクエストを
マージした時に実行される`ように設定します。

実装自体は簡単で、CircleCI設定ファイルの編集のみで完了しますので
ご安心ください。

では、下記からCircleCI設定ファイルをご確認ください。

ちょっとコードが長くなってしまってますが、気にせずに
`- aws-ecr/build-and-push-image:`以降の`filters:`に注目します。

そうすれば、`masterブランチのみで指定を行っていること`が分かります。
(- aws-ecs/deploy-service-update:も同様です。)

```yml:.circleci/config.yml
version: 2.1
jobs:
  build-and-test:
    docker:
      - image: circleci/ruby:2.7.3-node-browsers
        environment:
          RAILS_ENV: 'test'
      - image: circleci/mysql:8.0
        command: mysqld --default-authentication-plugin=mysql_native_password
        environment:
          MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
          MYSQL_ROOT_HOST: '%'
    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "Gemfile.lock" }}
            - v1-dependencies-
      - run:
          name: install dependencies
          command: |
            bundle install --jobs=4 --retry=3 --path vendor/bundle
      - save_cache:
          paths:
          - ./vendor/bundle
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}
      - run: yarn add @fortawesome/fontawesome-free
      - run: mv config/database.yml.ci config/database.yml
      - run: bundle exec rake db:create
      - run: bundle exec rake db:schema:load
      - run:
          name: Rubocop
          command: bundle exec rubocop
      - run:
          name: RSpec
          command: |
            mkdir /tmp/test-results
            TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \
              circleci tests split --split-by=timings)"
            bundle exec rspec \
              --format progress --format RspecJunitFormatter \
              --out /tmp/test-results/rspec.xml \
              $TEST_FILES
      - store_test_results:
          path: /tmp/test-results
      - store_artifacts:
          path: tmp/screenshots
          destination: test-screenshots

orbs:
  aws-ecr: circleci/aws-ecr@7.3.0
  aws-ecs: circleci/aws-ecs@2.2.1
workflows:
  build-and-deploy:
    jobs:
      # 開発用イメージをベースにビルド&テストする
      - build-and-test
      # 本番用イメージをベースにビルド&デプロイする
      - aws-ecr/build-and-push-image:
          # masterブランチのみ実行する
          filters:
            branches:
              only: master
          repo: ECRリポジトリ名
          dockerfile: Dockerfile.production
          extra-build-args: '--build-arg RAILS_MASTER_KEY=${RAILS_MASTER_KEY}'
      - aws-ecs/deploy-service-update:
          # masterブランチのみ実行する
          filters:
            branches:
              only: master
          requires:
            - aws-ecr/build-and-push-image
          cluster-name: 'クラスター'
          service-name: 'サービス'
          family: 'タスク定義'
          deployment-controller: 'CODE_DEPLOY'
          codedeploy-application-name: 'アプリケーション名'
          codedeploy-deployment-group-name: 'グループ名'
          codedeploy-load-balanced-container-name: 'コンテナ名'

```

### **`テスト部：参考まで`**
`orbs:`以前のコードは、今回の趣旨とは違うテスト部となりますので
無視して頂いて問題ありません。
(紛らわしくして、申し訳ありません…)

ちょっとだけ補足すると、workflows:で
- build-and-test (開発用イメージのビルドとテスト)
- aws-ecr/build-and-push-image: (本番用イメージのビルドとデプロイ)
を並走させています。

以上です。

# 検証作業

filtersを加えただけなので、同じように
イメージのプッシュも
blue/greenデプロイメントも
できるはずです。

心配であれば、`git push`して確認してみてください。

作業完了です！お疲れ様でした！


# 参考記事

https://nishinatoshiharu.com/circleci-ecs-deploy/

https://qiita.com/kbind/items/a357c02a28c0bbd78cc7

https://circleci.com/developer/orbs/orb/circleci/aws-ecr

https://circleci.com/developer/orbs/orb/circleci/aws-ecs

https://circleci.com/docs/ja/2.0/workflows/



# 終わりに

Orbを用いて実装してみましたが、terraformで構成しようとすると
どうなるのやら…

ようやくGUI上でのインフラ構成をQiita記事で
アウトプットできたので、terraform学習に入れます！

最後まで、お読みいただき、ありがとうございました！