docker-composeでRailsを開発しCircleCI 2.0でテストしてHerokuにdeployする

# はじめに
だいたいタイトルに詰め込みましたが、より具体的にはこういう構成です。

- ローカル開発
  - docker-composeでRailsとDBを立ち上げて開発
- CircleCI
  - docker-composeでローカルと同じ構成でのテスト
- deploy
  - masterブランチをpushするとCircleCIがテスト後にHerokuへ自動でdeploy
- Heroku
  - Railsが動くdockerコンテナがそのままdeployされる

特筆すべきはCircleCI上でdocker-composeを使っているところです。
これによりローカル開発と差が無い環境でCIを回すことができ、CircleCI用のDBを設定するなどのCIに依存した設定をする必要がなくなります。

今回使用したdockerとRailsの構成はほぼdocker公式のサンプルを紹介しているドキュメントのままです。
ただ何も変えていないのもつまらなすぎるかなと思い、Railsでuserというモデルだけscaffoldしてあります。
https://docs.docker.com/compose/rails/


実際にできあがったリポジトリはこちら
https://github.com/Kesin11/docker_rails_sample


# Dockerfile

```Dockerfile
FROM ruby:2.3.3
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs
RUN mkdir /myapp
WORKDIR /myapp
ADD Gemfile /myapp/Gemfile
ADD Gemfile.lock /myapp/Gemfile.lock
RUN bundle install
COPY . /myapp

CMD ["rails", "server", "-b", "0.0.0.0"]
```

この記事を書いてる時点（2017/10/21）で既にRuby 2.3.3とRails 5.0.0.1は若干バージョンが古いですが、公式ドキュメントがそのバージョンだったのでそのままにしてあります。

特筆するところは特にない最小構成となっており、最後の`.　/myapp`を`ADD`から`COPY`に変更したのと、最後の`CMD`を追加した以外は公式ドキュメントのままです。
Herokuにdeployされた後、Railsを起動する必要があるので`CMD`を追加してあります。

最小構成のDockerfileでは足りないという場合は、探せばRailsのためのDockerfileはいくらでも見つかるので必要に応じてカスタムしていきましょう。

# docker-compose.yml


```docker-compose.yml
version: '3'
services:
  db:
    image: postgres:9.6
    ports:
      - '5432:5432'
    volumes:
      - postgresql-data:/var/lib/postgresql/data
  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/myapp
    ports:
      - "3000:3000"
    depends_on:
      - db
volumes:
  postgresql-data:
    driver: local
```

主にローカルの開発が便利になるようにdocker公式ドキュメントからpostgresqlの設定を少し変更しています。

DBのバージョンは固定しておきたいのでpostgres:9.6に変更。
開発中はpostgresqlに接続して中身を確認したいのでローカルから接続できるように`ports`を追加。
公式のままだと`docker-compose down`で終了するたびにDBの中身が消えてしまうので`volumes`を使ってローカルに永続化します。

`docker-compose up`でDBとRailsが立ち上がるので後はローカルで開発するのとほぼ変わらずに開発することができます。

# CircleCI 2.0
今回のメインとも言える部分です。

```./circleci/config.yml
version: 2
jobs:
  build:
    machine:
      image: circleci/classic:edge
    steps:
      - checkout
      - run:
          name: docker-compose build
          command: docker-compose build
      - run:
          name: docker-compose up
          command: docker-compose up -d
      - run:
          name: sleep for waiting launch db
          command: sleep 1
      - run:
          name: "before_test: setup db"
          command: docker-compose run web rails db:create db:migrate
      - run:
          name: test
          command: docker-compose run web rails test
      - run:
          name: docker-compose down
          command: docker-compose down
  deploy:
    machine:
      image: circleci/classic:edge
    steps:
      - checkout
      # see: https://devcenter.heroku.com/articles/container-registry-and-runtime#using-a-ci-cd-platform
      - run:
          name: "build docker image"
          command: docker build --rm=false -t registry.heroku.com/${HEROKU_APP_NAME}/web .
      - run:
          name: setup heroku command
          command: bash .circleci/setup_heroku.sh
      - run:
          name: heroku maintenance on
          command: heroku maintenance:on --app ${HEROKU_APP_NAME}
      - run:
          # HEROKU_AUTH_TOKEN is generated by `heroku auth:token`
          name: "push container to registry.heroku.com"
          command: |
            docker login --username=_ --password=$HEROKU_AUTH_TOKEN registry.heroku.com
            docker push registry.heroku.com/${HEROKU_APP_NAME}/web
      - run:
          name: heroku db migrate
          command: heroku run rails db:migrate --app ${HEROKU_APP_NAME}
      - run:
          name: heroku maintenance off
          command: heroku maintenance:off --app ${HEROKU_APP_NAME}
workflows:
  version: 2
  build_and_deploy:
    jobs:
      - build
      - deploy:
          requires:
            - build
          filters:
            branches:
              only: master
```

## ビルド・テスト
CircleCIではテストを実行する環境として任意のdockerイメージを複数指定することが可能なので、今回のようなRailsアプリならrubyとpostgresなどのdockerイメージを一緒に立ち上げればdocker-compose.ymlで設定したのと似たような環境を作ることは可能です。
[ruby+postgresのCircleCI公式のサンプルリポジトリ](https://github.com/CircleCI-Public/circleci-demo-ruby-rails/blob/master/.circleci/config.yml)


ですがdocker-compose.ymlで設定したのと同じような設定を今度はCircleCIのyamlで書き直すことになり、dockerの設定を2重に管理することとなるためできれば避けたいです。

CircleCI上でdocker-composeが動いてくれれば全て解決なのに・・・ということはCircleCIも分かっているようでちゃんとドキュメントがあります
https://circleci.com/docs/2.0/docker-compose/

最初に目に留まるサンプルコードがdocker-composeを自分でインストールするという方法なので面倒くさそうな雰囲気なのですが、下の方まで読むとどうやら`machine`を使えば最初からdocker-composeが使えるらしいということが書いてあります。
この`machine`が何者なのかというと、これはCircleCIの環境をdockerではなくて従来の1.0と同様に仮想マシンで動かすためのパラメータです。
https://circleci.com/docs/2.0/executor-types/#using-machine

つまり仮想マシンならdocker-composeが既にインストール済みなので自分で何もしなくても最初から使えるというわけです。
ちなみに仮想マシンのdockerのバージョンはちょっと古めなようなので、ローカル開発のdockerとバージョン違いで問題が出ないように`machine: true`ではなくて

```
machine:
      image: circleci/classic:edge
```
とすることで最新バージョンの仮想マシンを指定しています。


docker-composeさえ使えてしまえば、あとはローカル開発と同じようにdocker-composeを使ってビルドしてDBをセットアップし、`rails test`が実行できます。


## deploy
dockerが出始めの頃、Herokuはまだdockerコンテナを動かすことはできませんでしたが、素晴らしいことに現在ではbuildpackの代わりにdockerコンテナをそのまま動かすことができます :tada: 
https://devcenter.heroku.com/articles/container-registry-and-runtime

ただし、ドキュメントの下の方のKnown issuses and limitationsに書かれているようにHerokuにはコンテナをビルドする機能が無いです。
なのでCircleCIにコンテナをビルドさせましょう。ビルド後はHerokuのドキュメント通りに`docker push`でHerokuにコンテナをdeployすることができます。


コンテナのdeployはこれで完了なのですが、Railsアプリの場合はDBのマイグレーションが必要です。これもCircleCIで行わせたい。
CircleCI 1.0のときは`heroku`コマンドがそのまま使えたのですが、残念ながら2.0では標準機能としてサポートはされていません。
ですが、CircleCIのドキュメントに`heroku`コマンドを使う方法のサンプル[^1]があるので今回はこれを使わせていただきました。
[^1]: docker-composeのサンプルコードと同じように自分で`heroku`コマンドのインストールから行うという・・・
https://circleci.com/docs/2.0/deployment_integrations/#heroku

手順が多いので`.circleci/setup_heroku.sh`というファイルに分割しています。

これで`heroku`コマンドが使えるので`rails db:migrate`などdeployの前後に必要な処理を書いてCircleCIでdeployも完結させることができました。

リポジトリの[README.md](https://github.com/Kesin11/docker_rails_sample/blob/master/README.md)に書いてありますが、`HEROKU_APP_NAME`や`HEROKU_AUTH_TOKEN`は各自の設定をCircleCIのサイトから設定してください。

## Workflows
deployはmasterブランチのときだけ実行されてほしいので、2.0から導入されたWorkflows機能を使います。

今回は詳しく解説しませんが、buildのジョブが終わったあとにmasterブランチのときだけdeployのジョブが走るような設定になっています。

詳しくはCircleCIのドキュメント参照してください。
https://circleci.com/docs/2.0/workflows/#workflows-configuration-examples
https://circleci.com/docs/2.0/configuration-reference/#workflows

# Heroku
本来はCircleCIの設定の前にHerokuでappを作っておく必要があるので順番が前後してしまっていますが、Heroku側の設定にも軽く触れておきます。

ローカルで開発が終わってdockerコンテナをビルドしたら以下のコマンドを実行します。
Herokuにアプリを作り、postgresqlアドオンの無料プランを接続してDBのセットアップをしています。

```bash
# dockerコンテナをdeployするためのプラグインをインストール
heroku plugins:install heroku-container-registry

# herokuのコンテナレジストリにログイン
heroku container:login

# 新しいappを作成
heroku create

# Railsが動く`web`のビルド済みdockerコンテナをdeploy
heroku container:push web

# postgresqlアドオンの無料プランを追加
heroku addons:create heroku-postgresql:hobby-dev

# DBセットアップ
heroku run rails db:migrate

# 実際にアクセスして/usersを確認してみる
heroku open
```

Herokuにdeployされたコンテナは起動するとDockerfileの`CMD ["rails", "server", "-b", "0.0.0.0"]`が実行されます。
今回はサンプルなのでこのままにしてありますが、実際にはunicornなどに置き換えて運用することになるでしょう。

# まとめ
ローカルではdocker-composeで開発、CircleCI上ではdocker-composeでローカルと同じ環境でテストを行い、そのコンテナをHerokuにdeployするというサンプルを紹介しました。

今回はサンプルということで可能な限りシンプルにすることを心がけました。そのためパッと思いつくような以下の問題は今回扱っていません。

- 今どきRailsでもReact使うのが普通なのでjsはどうする？
- CircleCIでテスト動かす前のセットアップだけで数分かかるのでキャッシュを使えないか？
- deploy時のアセット周りどうするのか
- HerokuでRAILS_ENV=productionになってないけど？？

実際に運用に乗せるとなるとこのあたりの問題も解決する必要があると思いますが、プロダクトによって状況が異なるはずなので各自でいい感じに設定していきましょう。