アプリケーションサーバに引きこもるDBを自立させてDocker Composeで開発環境を作る


[mixiグループ Advent Calendar 2017](https://qiita.com/advent-calendar/2017/mixi)の9日目は、XFLAGスタジオCREチーム[^cre]の[@upscent](https://qiita.com/upscent)がお送りします。

開発環境構築は何をするにしても通らなければいけない道です。
Docker Composeを使うと便利だなあと改めて思ったので、開発環境を構築する上でこの記事が参考になれば幸いです。
# Docker Composeを使おうと思った理由
先に私の所属するCREチームの開発フローについて説明します。
## CREチームの開発フロー
CRE(Customer Reliability Engineering)チーム[^cre]は、XFLAGスタジオが提供する様々なプロダクトにおいて、お客様の信頼を最大化することをミッションとして日々業務を行っています。

[CREチームを設立しました！](https://career.xflag.com/report/engineer/cre/)でも述べたように、カスタマーサポート(CS)スタッフがお客様の疑問や不安、抱える問題を常に解決できる状態を維持するために、CREチームはプロダクトの新規リリース・アップデートに合わせてCSスタッフ向け管理ツール(CSツール)の立ち上げと改修を行っています。つまり、プロダクトの数だけ並行してアプリケーションの開発を行うことになるのです。
## 現状の課題
複数の異なるプロダクトのアプリケーションを1台のマシンで動かすとなると、やはりDockerを使ってプロダクトごとにコンテナを用意するのが常套手段かと思います。
私がこれまで関わった多くのプロダクトにおいて、開発環境向けの構成は、アプリケーションもDBもKVSも全て同一のマシンで起動するものとなっていました。そのため、従来のDockerを使った開発環境では、Dockerの基本思想である1コンテナ1プロセスの構成に反し、アプリケーションもDBもKVSもすべて同じコンテナで起動させている状態でした。

このようなケースでは下記のような問題が発生します。

**Docker Hubにある公式コンテナの恩恵を受けられない**
1つのコンテナに複数のサービスをインストールする場合、Dockerfileにシェルコマンドでインストール手順を記述しなければいけません。

例: 「Elixirのインストールね、はいはい。exenvとerlenv使えば楽勝・・・え、erlenvだとインストールできないの・・・手作業インストールェ・・・」

**サービス起動をよく忘れる**
コンテナを起動したら、アプリケーション起動に必要なサービスの起動も手動で行わなければなりません。

例: 「すごくすごく時間のかかるデータロードタスクが最後で落ちる、なんでじゃ・・・あ、Redis起動してない・・・」

**`docker run` 時にオプションを付け忘れる**
ポートを開け忘れたり、ボリュームを付け忘れたりするとつらいです。

例: 「よっしゃー！コンテナでサーバ起動できたぞ。ホストマシンのブラウザから動作確認してみるか・・・あ、ポートあけてない」



・・・大半は私の凡ミスですね、すみません。



上記の問題を解決するために

- [(1) 1コンテナ1プロセスの構成に切り替える](#1-1コンテナ1プロセスの構成に切り替える)
- [(2) 構成をDocker Composeで管理する](#2-構成をdocker-composeで管理する)

とすることにしました。
# Docker Composeの導入

以下、アプリケーションフレームワークにPadrino、DBにMySQL、KVSにRedisを利用した環境構築を例に話を進めていきます。
## (1) 1コンテナ1プロセスの構成に切り替える
ネックとなるのは、下図左のように1コンテナに複数プロセスを立ち上げる構成に固定されていることです。これを下図右のように1コンテナ1プロセスの構成に切り替えます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/43836/9d7b971c-1b8e-dd3a-8d6c-7dde94c3ff04.png)


Padrinoから別のコンテナで起動しているMySQL、Redisに接続するためにはアプリケーションコードに書かれているホスト名を変えなければいけません。この変更は手元のマシンで開発環境を構築するための変更なのでコミットしたくありません。
そこで、今回は少し特殊ですが


- [(1-a) Padrinoコンテナから別コンテナで起動しているMySQL、Redisへ接続する](#1-a-padrinoコンテナから別コンテナで起動しているmysqlredisへ接続する)
- **[(1-b)ホスト名の変更差分を `git add` できないようにする](#1-b-ホスト名の変更差分を-git-add-できないようにする)**

という対応を入れてみます[^replace_host]。
### (1-a) Padrinoコンテナから別コンテナで起動しているMySQL、Redisへ接続する
Docker Composeには[linksオプション](https://docs.docker.com/compose/compose-file/#links)があります。
linksオプションを使うと他のコンテナへの接続設定が追加できます。この設定にはエイリアスをつけることができ、エイリアスをホスト名として他のコンテナに接続できるようになります。

今回の例では、Padrinoサーバを立てるコンテナからMySQLサーバを立てるコンテナに対して `db_container` 、Redisサーバを立てるコンテナに対して `kvs_container` というエイリアスを設定しておきます(Docker Composeの設定ファイルの書き方`docker-compose.yml`の書き方は後述します)。そうすると、アプリケーションコードに書かれているDBサーバ、Redisサーバのホスト名をそれぞれ `db_container` 、 `kvs_container` に置換するだけでPadrinoからそれぞれのサーバに接続できるようになります。
### (1-b) ホスト名の変更差分を `git add` できないようにする
ホスト名を置換してしまうと、Git管理されているファイルであればコミット対象となります。
ですがこの差分はコミットしたくないので、そもそも `git add` ができないようにGitの設定を下記の通り変更します[^ignorediff]。

```:$APP_ROOT/.git/attributes
<MySQLのホスト名が記載されているファイルのパス1> filter=replace_db_filter
<MySQLのホスト名が記載されているファイルのパス2> filter=replace_db_filter
<MySQLのホスト名が記載されているファイルのパス3> filter=replace_db_filter
・
・
<Redisのホスト名が記載されているファイルのパス1> filter=replace_kvs_filter
```

```.gitconfig:$APP_ROOT/.git/config
[filter "replace_db_filter"]
  smudge = cat
  clean = sed 's/db_container/<元のMySQLサーバのホスト名>/g'
[filter "replace_kvs_filter"]
  smudge = cat
  clean = sed 's/kvs_container/<元のRedisサーバのホスト名>/g'
```

上記の設定を行うと、例えば `<Redisのホスト名が記載されているファイルのパス1>` を `git add` する際には、configファイルに定義されたフィルター `"replace_kvs_filter"` の cleanコマンドが実行されるようになります。
前の手順で `kvs_container` に書き変えられた箇所がcleanコマンドによって `<元のRedisサーバのホスト名>` に戻されるので、変更がなかったように見える仕組みです。
## (2) 構成をDocker Composeで管理する
### docker-compose.yml
コンテナの定義と構成をyaml形式で記述します。
MySQL、RedisはDocker Hubの公式コンテナをそのまま利用します。
Padrinoは後述の[Dockerfile](#padrinoコンテナ用のdockerfile)をビルドしたイメージで作ったコンテナを利用します。 `links` オプションも忘れずに設定しておきます。

```yaml:docker-compose.yml
version: '2'
services:
  kvs:
    image: redis:4.0
  db:
    image: mysql:5.7
    envirionment:
      MYSQL_ROOT_PASSWORD: xxx
  app:
    build: app
    links:
      - db:db_container
      - redis:kvs_container
    volumes:
      - <ホストのアプリケーションコードを置いているパス>:/app
    ports:
      - <ホストのポート>:3000
    command: '/bin/bash -c "sh /replace_host.sh && bundle install --path vendor/bundler && bundle exec padrino s -h 0.0.0.0"'
```
### Padrinoコンテナ用のDockerfile
ここで特筆すべきは、Padrinoコンテナ内に`replace_host.sh`というシェルスクリプトを配置することです。このスクリプトは[アプリケーションサーバから別コンテナに接続する](#アプリケーションサーバから別コンテナに接続する)で説明した置換を行います。
これをコンテナ内に配置し、アプリケーションの起動前に実行することで、この置換処理を気にすることなく開発作業ができるようになります。

```Dockerfile:Dockerfile
FROM ruby:2.2

ENV APP_ROOT /app
WORKDIR $APP_ROOT

ADD ./replace_host.sh /.

RUN gem install bundler -v '1.12.5'
```

```sh:replace_host.sh
sed 's/<元のMySQLサーバのホスト名>/db_container/g' <MySQLのホスト名が記載されているファイルのパス1>
sed 's/<元のMySQLサーバのホスト名>/db_container/g' <MySQLのホスト名が記載されているファイルのパス2>
sed 's/<元のMySQLサーバのホスト名>/db_container/g' <MySQLのホスト名が記載されているファイルのパス3>
・
・
sed 's/<元のRedisサーバのホスト名>/kvs_container/g' <Redisのホスト名が記載されているファイルのパス1>
```
# Docker Compose導入でよかったこと
**バージョン管理が楽になった**
1コンテナ1プロセスの原理に従っていれば、Docker Hubにある[公式のコンテナ](https://hub.docker.com/explore/)が使えます。
インストール用のシェルコマンドを自分で記述する必要がなく、バージョンアップが必要な場面でもDockerイメージを変更することで簡単に対応できます。

**コンテナ作成に必要な設定がすべてDocker Composeで管理できるようになった**
解放するポートや接続するボリュームなどの設定忘れがなくなります。

また、少し話がそれますが、Docker for Macを利用している場合、ホストのディレクトリをマウントして使うと死ぬほど遅い問題がありました。これを解決するためにDocker Composeを導入して [docker-syncでホスト-コンテナ間を爆速で同期する](https://qiita.com/pocari/items/0340049742927f3a94b7) をやりたかったという意図もありました。

**アプリケーションの起動が楽になった**
`docker-compose up -d` で必要なサービスがすべて起動できます。
忘れん坊の方もこれで安心です。
# おわりに
開発環境の構築方法は試行錯誤していましたが、今のところこれが一番しっくりきています。
サンプルコードを作ったので実際に試してみたい方は[upscent/devenv-composer-sample](https://github.com/upscent/devenv-composer-sample)をご利用ください。
不便になったらちょこちょこ改善していきたいと思います。


[^cre]: CREについて詳しく知りたい方は mixiグループ Advent Calendar 2017の2日目の [CREチームを設立しました！](https://career.xflag.com/report/engineer/cre/) をご覧ください :smile:
[^replace_host]: 環境変数等で向き先を変えられるのであればそれが一番です！
[^ignorediff]: 参考: [gitでファイルの一部だけコミットから無視する](https://qiita.com/0829/items/dd87c00942e366c56a41)