Rails + Docker + ECSのビルド〜デプロイをCodePipelineで自動化した


Docker + ECS + RailsのプロジェクトでCodePipelineを使用してデプロイまでを自動化したので、その知見をまとめたい。（ブルーグリーンデプロイではなく、通常のデプロイ時の方法）

RailsのプロジェクトだけれどCodePipeline基本的な使い方は、他の言語でもそれほど変わらないと思う


## デプロイの流れ

githubにpushすれば自動でデプロイが開始される。デプロイは以下の流れで行うように作った。

1. GitHubの特定のブランチ(masterなど)にpushする
1. pushされたことがCodepipelineに通知されビルドが開始
    1. docker-composeを利用して、Dockerをbuildする
    2. Dockerイメージタグにコミット番号を付与して一意にする
    2. ビルドが完了したらECRにpushする
1.  ビルド完了後にECSにデプロイ通知がいく

## CodePipelineの設定

CodePipelineはソース管理、ビルド、デプロイをパーツのようにつなげてCD/CIを管理することができるAWSのサービス。以下のサービスをつなぎ合わせて連携することができる

* CodeCommit
* CodeBuild
* CodeDeploy

![スクリーンショット_2020-03-09_14_55_31-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59008/d633e433-367c-b70a-40e3-ca1ee96c596d.png "スクリーンショット_2020-03-09_14_55_31-2.png")

### CodeCommit

![スクリーンショット_2020-03-09_15_24_30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59008/45311404-a4c2-c76b-f028-1a50fdfb8c4a.png "スクリーンショット_2020-03-09_15_24_30.png")

まずはGithubで特定のリポジトリにpushされたときに検知できるようにする。ここではmasterがpushsされたときにビルドされる設定した。

ちなみにCodeCommitはGithub以外にも、ECRやS3などと連携することもできる。CodeCommit自体にコードを管理させることも可能。

### CodeBuild

![スクリーンショット_2020-03-09_15_29_41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59008/9c4f28d7-6bbb-8d77-56c1-e01b4ba8ce89.png "スクリーンショット_2020-03-09_15_29_41.png")

CodeBuildではビルドプロジェクトというものを作成する。このビルドプロジェクトはOS環境や、ビルドコマンドを記載するbuildspec.ymlのパスを設定していく。ようはビルドの設定を管理している感じだ。

![スクリーンショット 2020-03-09 15.38.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59008/b18b0289-9c56-7237-c95f-c9c2c69b7f6b.png "スクリーンショット 2020-03-09 15.38.01.png")

Ubuntuでイメージが最新バージョンのものを使っておけば特に問題はないかと思う。buildspec.ymlはGithubにあげたプロジェクトに入れておく。そのパスをビルドプロジェクトで設定すればOK

#### buildspec.yml

ビルドするコマンドをyamlに書いていく。ビルドは以下のような流れで行う。

1. ECRからDockerのイメージを取得
2. コミットハッシュを取得する(コミットハッシュはDockerイメージタグとして使用する)
3. .envに環境変数を追加していく
4. docker-composeを利用してビルドする
5. dbのmigrateを行う
6. デプロイを通知する

```yaml
version: 0.2

phases:
  install:
    runtime-versions:
      docker: 18
  pre_build:
    commands:
      - echo -------- Logging in to Amazon ECR... --------
      - aws --version
      - $(aws ecr get-login --region $AWS_DEFAULT_REGION --no-include-email)
      - REPOSITORY_URI=$AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/hogehoge
      - COMMIT_HASH=$(echo $CODEBUILD_RESOLVED_SOURCE_VERSION | cut -c 1-7)
      - IMAGE_TAG=${COMMIT_HASH:=latest}
  build:
    commands:
      - echo -------- Build started on `date` --------
      - echo -------- Building the Docker image... --------
      - echo AWS_ACCESS_KEY=$AWS_ACCESS_KEY >> .env
      - echo AWS_SECRET_KEY=$AWS_SECRET_KEY >> .env
      - echo ECS_ENV_NAME=$ECS_ENV_NAME >> .env
      - docker-compose -f docker-compose-$ECS_ENV_NAME.yml build
      - docker-compose -f docker-compose-$ECS_ENV_NAME.yml run --name hogehoge-image web sh -c "bundle exec rake db:create && bundle exec rake db:migrate"
      - docker commit hogehoge-image $REPOSITORY_URI:latest
      - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:$IMAGE_TAG
  post_build:
    commands:
      - echo -------- Build completed on `date` --------
      - echo -------- Pushing the Docker images... --------
      - docker push $REPOSITORY_URI:latest
      - docker push $REPOSITORY_URI:$IMAGE_TAG
      - echo [\{\"name\":\"hogehoge\",\"imageUri\":\"$REPOSITORY_URI:$IMAGE_TAG\"\}] > imagedefinitions.json
artifacts:
  files: imagedefinitions.json
```

buildspec.ymlはpre_build、build、post_buildという3段階で処理を行う。一個ずつ分解して説明していく。

##### pre_build

 ビルドする前の下準備。 GithubのURLとか、コミットハッシュとかをあとで使うので変数に入れている。ちなみにコミットハッシュはDockerイメージタグとして後で使う。
 
##### build

```yml
  - echo AWS_ACCESS_KEY=$AWS_ACCESS_KEY >> .env
  - echo AWS_SECRET_KEY=$AWS_SECRET_KEY >> .env
  - echo S3_BUCKET=$S3_BUCKET >> .env
```

環境変数を.envに書き込むようにしている。僕のRailsプロジェクトでは.envで環境変数を管理しており、CodeBuildでもdocker-composeが使用したいという理由でこの形にしている。このやり方がベストプラクティスではないような気がするので、もっと良い方法を見つけたい。

ちなみに環境変数はSystem Managerで管理している。環境変数についてはのちほどもう少し詳しく記載する。


```
  - docker-compose -f docker-compose-$ECS_ENV_NAME.yml build
  - docker-compose -f docker-compose-$ECS_ENV_NAME.yml run --name hogehoge-image web sh -c "bundle exec rake db:create && bundle exec rake db:migrate"
  - docker commit hogehoge-image $REPOSITORY_URI:latest
  - docker tag $REPOSITORY_URI:latest $REPOSITORY_URI:
```

あとはdocker-composeでビルドして、migrateして、Dockerイメージをcommitしているだけ。

ちなみに僕はDockerfile内に`assets:precompile`を行っているため、buildspec.ymlにはコマンドが書いていない。

##### post_build

ECSにデプロイするためには最終的にimagedefinitions.jsonというファイルを作成する必要がある。

```
  - docker push $REPOSITORY_URI:latest
  - docker push $REPOSITORY_URI:$IMAGE_TAG
  - echo [\{\"name\":\"hogehoge\",\"imageUri\":\"$REPOSITORY_URI:$IMAGE_TAG\"\}] > imagedefinitions.json
artifacts:
  files: imagedefinitions.json
```

imagedefinitions.jsonはnameとimageUirを関連付けたjsonを書いていく。複数環境あるときは当たり前だけど複数書いていく。

* name
	* ECSのコンテナ名
* imageUri
	* ECRのURL

ECSのタスク定義との関連は以下のようになる。


![スクリーンショット_2020-03-09_16_34_33-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59008/8887b6b9-ad24-4795-dce7-a7862a023f2c.png "スクリーンショット_2020-03-09_16_34_33-2.png")


### 補足

#### 環境変数について

環境変数は秘匿化する必要があるためSystem Managerでパラメータを管理するようにした。

![68017af5-fa1b-4fb2-8307-aadc174d004a-960x690r.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59008/380e1221-743d-bfa1-3158-af439f28f062.png "68017af5-fa1b-4fb2-8307-aadc174d004a-960x690r.png")

安全な文字列を選択してパラメータを設定する。「名前」欄で設定した値をCodeBuildで使用する。


![d9424b69-0d44-4230-afa8-c769f4a2e55a-960x573r-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59008/23702f4b-ada0-01f8-3559-fe03352022f3.png "d9424b69-0d44-4230-afa8-c769f4a2e55a-960x573r-2.png")

System Managerで設定した値をbuildspec.ymlで使用するために、CodeBuildに環境変数として設定する必要がある。これを設定しておくとbuildspec.ymlの中で`$HOGEHOGE`という値で使用できるようになる。

* 名前
	* buildspec.ymlで使用する環境変数名
* 値
	* System Managerで設定した名前
* 入力
	* 『パラメータ』を選択する

```yml
- echo AWS_ACCOUNT_ID=$AWS_ACCOUNT_ID >> .env
- echo AWS_ACCESS_KEY=$AWS_ACCESS_KEY >> .env
- echo AWS_SECRET_KEY=$AWS_SECRET_KEY >> .env
```

これでbuildspec.ymlにファイルの中でCodeBuildで設定した環境変数が利用できるようになる


#### デプロイしたときにタスク定義のバージョンは更新されていく

![0ad3ec1b-5711-4351-a838-654d76ef1f81-960x596r.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59008/680f0796-adb8-841f-bb8e-ab9bc792a597.png "0ad3ec1b-5711-4351-a838-654d76ef1f81-960x596r.png")


デプロイされるとimagedefinitions.jsonで設定したコンテナ名とイメージのURLでタスク定義のイメージが変更され、リビジョンが新しく更新されていく。タスクをリビジョンで管理するメリットとして「切り戻しが簡単になる」という点がある。

もしも本番で障害が発生したとき場合にもリビジョンを戻すだけで動作する。ただしDBのカラム変更などしているときは、DBをロールバックする必要もあるので注意が必要。

#### CodePipelineからデプロイを実行する

なんらかの理由でソースコードをpushせずにデプロイしたい場合は、Codepipelineの画面から直接行うことができる

![c7177b94-b6e1-408f-84c4-de5ea8b817ad-960x598r.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59008/94d7d922-f3cf-ed91-1408-331e64ed0b87.png "c7177b94-b6e1-408f-84c4-de5ea8b817ad-960x598r.png")


## 終わり

ECSは少人数の開発にこそ向いていると思う。ECSでスケールアップから障害復旧までまかせ、CodePipelineでデプロイを自動化しておけばインフラの運用をそれほど考慮しなくて済むようになる。アプリケーション層に集中して開発ができるようになる。

まだまだECSやCodePipelineに対しての知見が足りないので、また気づきがあったら書いていきたい