【サーバーサイド一式】Docker + Rails + Circle CI + Terraformでインフラをコードで環境構築 & ECSへ自動コンテナデプロイ【前半】


- (主にサーバーサイドの)アプリを**Docker**を使って環境構築して
- **terraform**でインフラの構成をコード化して
- (Kubernetesなどのコンテナオーケストレーションツールを用いて)なるべく開発環境との差異が無いまま**コンテナデプロイ**をして
- **CircleCI**でテストからデプロイまで自動化

といった事がサーバーサイドエンジニアとしては求められている、いや最早**知らないようではやっていくのは難しい**、といったご意見が結構な頻度で見られたりします。

（そういったご意見の是非はまた別の機会に預けるとして）だとしたら初心者のサーバーサイド志望のエンジニアさん達の為には、これらの

- **Docker**
- **CircleCI**
- **Terraform**
- コンテナデプロイ(今回は**ECS**)

といった要素を**チュートリアル形式で学んでいく**のが手っ取り早いのでは無いか(そして自分の理解の整理にもなる)と思い立ちました！　どうでしょうか？ 
つまりこの記事で**現時点でナウいサーバーサイドの技術が一通り触れちゃう！**ということです！

## 目次
| 目次        |
|:-----------|
| [全体像を把握しよう](#全体像を把握しよう)|
| [Dockerを用いてRailsの環境を構築](#dockerを用いてrailsの環境を構築)|
| [CircleCIでCIの設定](#circleciでciの設定)|
| [Terraformを使うための準備](#terraformを使うための準備)|
| [前半はここで終わり](#前半はここで終わり)|

##この記事を読むにあたっての前提
- gitとgithubを使った事がある
- Dockerをなんとなく理解している
- Railsも触ったことある(触ったことなくてもできるかも)
- AWSのコンソールでEC2やVPCを立てた事がある

もちろん、それ以外の方も是非是非挑戦してみてください。理解なんて後から帳尻合わせればいいんです。

##できるようになること

**railsアプリをDockerで構築できて、pushしたタイミングでCircleCIが走って、予めTerraformで構築しておいたAWS環境(ECS)に自動デプロイする事ができるようになる！**

##注意してほしい事
- DBはsqliteです。RDSとつないでません。
- Webサーバもpumaで兼用してます。nginxなどのWebサーバーとつないでません
=>これらはまた次回やろうと思います！

##自分の環境
- Ruby 2.6.3
- Rails 6.0.1
- Docker 19.03.4
- Git 2.14.1
- Terraform 0.12.8


##全体像を把握しよう
| 目次        |
|:-----------|
| [全体像を把握しよう](#全体像を把握しよう)←今ココ|
| [Dockerを用いてRailsの環境を構築](#dockerを用いてrailsの環境を構築)|
| [CircleCIでCIの設定](#circleciでciの設定)|
| [Terraformを使うための準備](#terraformを使うための準備)|
| [前半はここで終わり](#前半はここで終わり)|


様々なツールやサービスを繋げていく事は、全体像の把握がとても大事になってきます。
まず大きい流れを確認しましょう。時系列順に列挙すると以下となります。

###アプリが自動でコンテナデプロイされるまでの流れ

>(0.TerraformでAWS上のリソースを定義)
1.RailsアプリをGithubにpush
2.CircleCIでCI開始
3.CIでDockerイメージをビルド
4.DockerイメージをECRへpush
5.ECSのTaskDefinitionを更新
6.CIでmigration
**7.アプリがデプロイされる！**

画像に表すと、以下となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208038/7fc5a139-5aeb-8744-6035-d86ff0601ae0.png)


ざっくり各ツールの役割も解説すると

####terraform
>インフラストラクチャ定義ツール。
クラウド上のリソースを定義ファイルの状態になるように生成・操作してくれる。
画面上でポチポチやってたインフラの操作をコードにできる。

####rails
>デプロイしたいアプリそのもの。

####github
>バージョン管理ツール。
>アプリとCircleCIをつなぐ為のハブみたいな役割も担っている。

####CircleCI
>ビルド/テスト/デプロイなどについて自動実行できるサービス。
>これを用いて色々面倒くさい事を自動化しよう。

####Docker
>アプリを動かす為の環境が一式詰まった仮想化プラットホーム。
ここでrailsが動く事はだいたい担保されてるので、Railsアプリが入ったDockerコンテナをそのまま本番環境にのっけたい

####ECR
>コンテナイメージをprivateな環境に格納しておけるサービス。
Railsアプリの環境を入れたDockerコンテナイメージをここにぶっ込みたい。

####ECS
>dockerコンテナを通して処理をしたりサービスを立ち上げたりと行った挙動を、EC2上で容易に行うためのAWSのサービス。EC2上での操作は全部AWSでやってくれるので、デプロイが簡単(のはずだがそうでもない)。

となります！非常に盛りだくさんですね。
はい、いつも通り順番に解説していきますのでご安心ください。

ではまずはDockerを用いて、Railsアプリを動かせる環境を作っていきましょう！

##Dockerを用いてRailsの環境を構築

| 目次        |
|:-----------|
| [全体像を把握しよう](#全体像を把握しよう)|
| [Dockerを用いてRailsの環境を構築](#dockerを用いてrailsの環境を構築)←今ココ|
| [CircleCIでCIの設定](#circleciでciの設定)|
| [Terraformを使うための準備](#terraformを使うための準備)|
| [前半はここで終わり](#前半はここで終わり)|

とりあえずローカル環境で一度作ります。
terraformとrailsアプリを一つのディレクトリにまとめちゃいたいので、 `terraform_ecs_deploy`
というディレクトリを作っちゃいましょう(githubのリポジトリは別にするので注意してください！)

~~~terminal:ターミナル
$ mkdir terraform_ecs_deploy
$ cd terraform_ecs_deploy
$ rails new terraform_ecs_app --skip-javascript # webpackerは今回いらないです
$ cd terraform_ecs_app
$ rails db:migrate
~~~

さて、このタイミングでRailsアプリをDocker化します。
`Dockerfile`を作り、公式のRubyのイメージを取得し、以下のように書きましょう。
(今回簡単なDockerfileしか用意していないのはご了承ください。)

[Dockerのインストールはこちら](https://qiita.com/kurkuru/items/127fa99ef5b2f0288b81)

~~~Dockerfile:Dockerfile
# 公式のイメージから取得
FROM ruby:2.6.3

# Dockerfile内部で使える変数として定義
ARG RAILS_ENV
ARG RAILS_MASTER_KEY

# コンテナ内のルートとする変数を/appと定義
ENV APP_ROOT /app

# 環境変数化
ENV RAILS_ENV ${RAILS_ENV}
ENV RAILS_MASTER_KEY ${RAILS_MASTER_KEY}

# コンテナ内のルートとする。
WORKDIR $APP_ROOT

# ローカルのGemfile, Gemfile.lockをコンテナ内のルートへコピー
ADD Gemfile $APP_ROOT
ADD Gemfile.lock $APP_ROOT

# bundle install実行。
# (バージョンのエラーが出る為、一応bundler 2.0.2を指定)
RUN \
    gem install bundler:2.0.2 && \ 
    bundle install && \
    rm -rf ~/.gem

# バンドルインストールが終わってから他のファイルもコンテナ内へコピー
ADD . $APP_ROOT

# 本番環境の場合プロダクション
RUN if ["${RAILS_ENV}" = "production"]; then bundle exec rails assets:precompile; else export RAILS_ENV=development; fi

# ポート3000番を公開
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]

~~~

では、この`Dockerfile`を元にDockerコンテナを`build`し、`rails`を立ち上げてみます。

~~~terminal:ターミナル
$ docker build -t terraform_ecs_app:latest . # -tでタグを指定
$ docker run -it -p 3000:3000 terraform_ecs_app:latest # -it
~~~

`docker run`時のオプションは以下の通りです。
>-i, --interactive・・・コンテナのSTDINにアタッチする
>-t, --tty・・・疑似ターミナルを割り当てる

このようになっていたらOKです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208038/6e3ce05f-a363-4940-f95e-bbd3b3a55bac.png)

`http://localhost:3000`にアクセスするとこのようにおなじみの画面になっているはずです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208038/c8a6b44b-b41a-43e9-6201-734ee4dcb8fc.png)

### ***※注意！*** docker run -it -p 3000:3000 terraform_ecs_app:latestが通らない場合

もしここで

~~~
docker: you are not authorized to perform this operation: server returned 401.
See 'docker run --help'.
~~~

このようなエラーが出る場合は、ターミナルで以下のコマンドを打ち、環境変数を設定してください。

~~~terminal:ターミナル
export DOCKER_CONTENT_TRUST=0
~~~


これにより、**Dockerイメージが改ざんされていないか、という整合性判定をスキップ**でき、上のコマンドが通るようになるはずです。

[Docker Content Trust（DCT）について](https://docs.docker.com/engine/security/trust/content_trust/)

### Railsの詳細設定

さて、今回Railsの話が本題では無いので、機能的には簡素なもので良いです。なので、
- 表示用のトップ画面
- `ALB(Application Load Balancer)`の`HealthCheck`用に、jsonで`status: ok`が帰ってくるURL

の二つだけ実装しましょう。(もちろんALBとヘルスチェックについても後述します。)

~~~terminal:ターミナル
$ rails g controller top index 
$ rails g controller health_check
~~~

`topコントローラ` はアクションとビューが自動生成されているはずなので、`health_checkコントローラ` だけ変更します。

~~~ruby:app/controllers/health_check_controller.rb
class HealthCheckController < ApplicationController
  # ALBにステータスokを返す為のアクション
  def index
    render json: '{ "status": "ok" }'
  end
end
~~~

ルーティングも変更します。

~~~ruby:config/routes.rb
Rails.application.routes.draw do
  root to: 'top#index'
  resources :health_check, only: [:index]
end
~~~

それぞれ以下のように表示されればOKです。

`http://localhost:3000`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208038/84460000-8dbc-d381-3a9d-09533954765e.png)


`http://localhost:3000/health_check`
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208038/08ff0ab5-71d1-9502-3b96-bfb466b7aca0.png)

また、本番環境で `tmp/pids`と`tmp/sockets`が作成されない事がある為、念のため`.gitignore`に以下の記述を追加しましょう。

~~~gitignore:.gitignore
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
#   git config --global core.excludesfile '~/.gitignore_global'

# Ignore bundler config.
/.bundle

# Ignore the default SQLite database.
/db/*.sqlite3
/db/*.sqlite3-journal
/db/*.sqlite3-*

# Ignore all logfiles and tempfiles.
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep
# ===============追加================
!/tmp/pids
!/tmp/sockets
# =============ここまで===============
# Ignore uploaded files in development.
/storage/*
!/storage/.keep

/public/assets
.byebug_history

# Ignore master key for decrypting credentials and more.
/config/master.key

~~~


最低限の機能が実装できたので、githubにpushしましょう。

###githubにrailsアプリをpush

今回はgitとgihubの解説は省略します。


[gitのインストール](https://git-scm.com/book/ja/v2/%E4%BD%BF%E3%81%84%E5%A7%8B%E3%82%81%E3%82%8B-Git%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)
[githubの使い方](https://qiita.com/nnahito/items/565f8755e70c51532459)


カレントディレクトリがrailsアプリなのを確認してから、以下のコマンドを打ってください。
(自分は今回リポジトリ名は `terraform_ecs_app` としています。)

~~~terminal:ターミナル
$ git init
$ git commit -m "initial commit"
$ git remote add origin リポジトリのURL
$ git push origin master
~~~

githubのリポジトリが以下のようになっていたらOKです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208038/8e55728f-2b1e-7c27-3c54-4ee41f076caa.png)

それではいよいよCircleCIのCI設定をおこなっていきましょう！

##CircleCIでCIの設定

| 目次        |
|:-----------|
| [全体像を把握しよう](#全体像を把握しよう)|
| [Dockerを用いてRailsの環境を構築](#dockerを用いてrailsの環境を構築)|
| [CircleCIでCIの設定](#circleciでciの設定)←今ココ|
| [Terraformを使うための準備](#terraformを使うための準備)|
| [前半はここで終わり](#前半はここで終わり)|

CircleCIとは、Saas型のCI/CDサービスであり、CI/CDでやることの基本であるビルドとテストとデプロイの3点が自動化可能なサービスです。

今回はこちらを使って**testとデプロイ(ECRへのpush)の自動化**を行なっていきたいと思います。

以下、CircleCIならではの特徴や料金についてはこちらをご参照ください。
[いまさらだけどCircleCIに入門したので分かりやすくまとめてみた](https://qiita.com/gold-kou/items/4c7e62434af455e977c2)


###CircleCIの導入

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208038/630aa158-b1a6-546f-baed-df110712a4b8.png)

↓こちらのURLの右上あたりにある`Login`をクリックし、`Log in with Github`を押してログインしましょう。
すると、自動的にgithubと紐づくはずなので、CIの設定画面まで行きましょう。
[CircleCI](https://circleci.com/)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208038/94bfaa11-3bff-b309-d109-313521314a62.png)


こちらの画像のように、`Add Projects`を押すと、自分のgithubのリポジトリが出てくるので、
その中から先ほど作成した`terraform_ecs_app`を選択します。`Set Up Project`を押しましょう。

<img width="1445" alt="スクリーンショット_2019-12-04_10_39_28.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208038/66579a37-b13e-f21f-f632-5d10df497bdf.png">

すると、プロジェクトの設定画面になると思います。
`Operating System`は`Linux`
`Language`も`Ruby`のままで大丈夫です。

そして、そのすぐ下に英語でチュートリアルが書いてあります。和訳すると

>1. `.circleci`という名前のフォルダーを作成し、`config.yml`ファイルを追加します（ファイルパスが`.circleci/config.yml`になるように）。
>2. `config.yml`に`sample.yml`の内容を入力します（以下を参照）
>3. `sample.yml`を更新して、プロジェクトの構成を反映します。 
>4. この変更を`GitHub`にプッシュします。
>5. 構築を始めましょう！これにより、`CircleCI`でプロジェクトが起動し、Webhookが作業の更新をリッスンします。 

とのことです。実際にそのすぐ下に、あらかじめCircleCIが用意してくれてたrails用の設定ファイル`sample.yml`があります。
指示通り、これをコピペして、ローカル環境で`.circleci`というフォルダを作り、その中に`config.yml`という名前のファイルを作り、そこに貼り付ければOKです。

ただし、これだとrubyのバージョンが違うのと、テストフレームワークが`rspec`になっているので、今回はrailsデフォルトのテストフレームワークである`minitest`に変えた以下のものにしてください。

~~~yml:.circleci/config.yml
# Ruby CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-ruby/ for more details
#
version: 2
# jobsの中にタスクを定義。一番下のworkflowのjobsのなかで定義したタスクを使う。
jobs:
  # buildという名前のタスク定義
  build:
    docker:
      # specify the version you desire here
      - image: circleci/ruby:2.6.3-node-browsers


      # Specify service dependencies here if necessary
      # CircleCI maintains a library of pre-built images
      # documented at https://circleci.com/docs/2.0/circleci-images/
      # - image: circleci/postgres:9.4

    working_directory: ~/repo

    # 実際の処理内容
    steps:
      - checkout

      # Download and cache dependencies
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "Gemfile.lock" }}
            # fallback to using the latest cache if no exact match is found
            - v1-dependencies-
      # runのたびに実行
      - run:
          name: install dependencies
          command: |
            gem install bundler -v 2.0.2
            bundle install --jobs=4 --retry=3 --path vendor/bundle

      - save_cache:
          paths:
            - ./vendor/bundle
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}

      # Database setup
      - run: bundle exec rake db:create
      - run: bundle exec rake db:schema:load

      # run tests
      - run:
          name: run tests
          command: |
            DISABLE_SPRING=true bundle exec rails test
            

      # collect reports
      - store_test_results:
          path: /tmp/test-results
      - store_artifacts:
          path: /tmp/test-results
          destination: test-results

~~~


ここまでできたら、先ほどのCircleCIの設定画面に戻り、5の右側にある`Startbuilding`を押してください。CircleCIが動き出します。

<img width="1424" alt="スクリーンショット_2019-12-04_11_04_15.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208038/b83069f1-c1bc-683d-b0ce-c2baa236c5db.png">


こうすることによって次回以降はpushされたタイミングでCircleCIがJobし始めます！


(ちなみにroutes.rbを変えたせいでtestが失敗してしまうので、該当のtestをコメントアウトするか、正しい記述(pathを`root_pathに変えます`)に直してからpushとbuildをしてください)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208038/aa3ba8eb-8d77-3256-77c5-96f346ea3b8c.png)

成功したらcircleCIのJOBSのタブの表示がこのようになります。(一番上の緑色がそうです。)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208038/a184afd0-b57c-15e8-4f45-c0878e199918.png)

さて、ここまできたらようやく`Terraform`に入ります！！


##Terraformを使うための準備

| 目次        |
|:-----------|
| [全体像を把握しよう](#全体像を把握しよう)|
| [Dockerを用いてRailsの環境を構築](#dockerを用いてrailsの環境を構築)|
| [CircleCIでCIの設定](#circleciでciの設定)|
| [Terraformを使うための準備](#terraformを使うための準備)←今ココ|
| [前半はここで終わり](#前半はここで終わり)|

###Terraformのインストール
[Terraform公式](https://www.terraform.io/)
Terraformを用いることで、今まで手続き的だったインフラの工程をコード化することできます。

`homebrew`でもインストールできますが、`Terraform`のバージョンマネージャである`tfenv`を使うのがおすすめです。バージョンアップに追従しやすいです。
まずは`tfenv`自体をインストールしましょう。

~~~terminal:ターミナル
$ brew install tfenv
$ tfenv --version
tfenv 1.0.1
~~~

`list-remote`コマンドでインストール可能なTerraformのバージョンを確認できます。
ここでは、`0.12.8`をインストールしましょう。

~~~terminal:ターミナル
$ tfenv list-remote
.
.
0.12.8
$ tfenv install 0.12.8
$ terraform -v
Terraform v0.12.8
~~~

###TerraformでAWSを扱うためのIAMユーザの設定

terraformでAWSを扱うには**IAMユーザのACCESS KEY、SECRET KEY(とDEFAULT REGION)が必要**です。

ここだけはterraformの管理下におけないので、**AWSのコンソールでIAMユーザを作成しし、アクセスキーとシークレットキーを発行**してください。

[IAMユーザの作成](https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_users_create.html#id_users_create_console)

発行できたら、ターミナルで以下のようにアクセスキーとシークレットキーを**環境変数に設定**してください。

~~~terminal:ターミナル
 $ export AWS_ACCESS_KEY_ID=AKIxxxxxxxxx
 $ export AWS_SECRET_ACCESS_KEY=wJalxxxxxxxxxxxxxxxx
 $ export AWS_DEFAULT_REGION=ap-northeast-1
~~~
(PCの電源落としたり、ターミナル落としたりしたら消えてしまうのでもう一度入力し直してください。)


今回自分は**AdministratorAccess ポリシー**をアタッチした**IAMユーザ**のアクセスキーを用いていますが、こちら相当に強力な権限なので扱いには注意してください。
(間違っても**GitHubなどで公開してはダメ**です！)
AdministratorAccessポリシー以外では、権限不足で Terraform の実行が失敗することがあるので、その場合はエラーメッセージを参考に、必要な権限を付与しましょう。

これでTerraformを扱う準備ができました！

### terraform用のgithubのリポジトリの作成

アプリ開発やCIとインフラ構成は別の話なので、リポジトリを分けるのがセオリーです。
今回はterraform_ecsという名前でgithubのリポジトリを作成します。

秘匿情報を扱うので、プライベートリポジトリにすることに注意してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208038/7454dc08-25b0-1a5a-fe02-3f265e7c1b1c.png)

さて、terraformのディレクトリ構造に関する考えは色々あるのですが
https://dev.classmethod.jp/devops/directory-layout-bestpractice-in-terraform/
https://qiita.com/anfangd/items/1b84f69fa2a4f8a29fbc
https://future-architect.github.io/articles/20190903/

今回は簡単のため**環境だけディレクトリを分けて、その中に全てのtfファイルを突っ込む**方式でいこうと思います。

~~~
terraform_ecs_deploy
├──terraform_ecs_app
└──terraform_ecs
    ├──prod
    └──stg
~~~

また、今回はステージング環境の構築は行いません！ほぼ同じことをやるだけなので、一度productionで構築できたらすぐできちゃうと思います。

では早速、terraform用のディレクトリを作成しましょう。

~~~terminal:ターミナル
$ cd .. # railsアプリにいた場合
$ pwd
/Users/matsumotokazuki/Desktop/terraform_ecs_deploy # terraform_ecs_deployにいることを確認。
$ mkdir terraform_ecs
$ mkdir terraform_ecs/prod
$ cd terraform_ecs/prod
~~~

このようなディレクトリ構成になっていれば大丈夫です。

~~~
terraform_ecs_deploy
├──terraform_ecs_app
└──terraform_ecs
    └──prod # カレントディレクトリ
~~~

ここで、githubにpushする前に、terraformのインフラ構成ファイルを表すterraform.tfstateや秘匿情報を表すterraform.tfvarsなどをgitの管理下から除外します。(こちらも後述します)

~~~terminal:ターミナル
$touch .gitignore
~~~

~~~.gitignore:terraform_ecs/prod/.gitignore
/.terraform/*
/terraform.tfvars
/terraform.tfstate
~~~

それではpushしましょう。

~~~terminal:ターミナル
$ cd .. # terraform_ecsディレクトリに移動
$ git init
$ git add -A
$ git commit -m "initial commit"
$ git remote add origin リポジトリ名
$ git push origin master
~~~

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208038/55d472a8-53bb-c4a0-5335-551b83513c87.png)

## 前半はここで終わり

| 目次        |
|:-----------|
| [全体像を把握しよう](#全体像を把握しよう)|
| [Dockerを用いてRailsの環境を構築](#dockerを用いてrailsの環境を構築)|
| [CircleCIでCIの設定](#circleciでciの設定)|
| [Terraformを使うための準備](#terraformを使うための準備)|
| [前半はここで終わり](#前半はここで終わり)←今ココ|

諸々の準備が終わった時点で一旦区切ります！
後半からいよいよTerraformを使ってインフラを構築していきますので、気長にお待ちください。

後半はコチラ(にする予定)