CrowdWorksのDocker開発環境

# はじめに

この記事は[CrowdWorks Advent Calendar 2016](http://qiita.com/advent-calendar/2016/crowdworks) 3日目の記事です:christmas_tree:

今回はクラウドワークスが運営しているクラウドソーシングサービスである[CrowdWorks](https://crowdworks.jp)のRails開発環境について紹介します。

[Qiitaにもたくさん記事が投稿されている](http://qiita.com/search?utf8=%E2%9C%93&sort=&q=Docker+%E9%96%8B%E7%99%BA%E7%92%B0%E5%A2%83)とおり、Dockerを開発環境に利用するのはだいぶ一般的になってきたように感じます。クラウドワークスでもローカルでDocker Composeを利用して開発環境を構築できるようにしています。

Docker化の範囲は、各人の好みがあるため2パターン用意しています。

1. RailsはmacOS上で動かしてミドルウェアのみDocker化
1. ミドルウェアに加えてRailsもDocker化

現状では「ミドルウェアのみDocker化している人」、「RailsもDocker化している人」、「まだ移行できていない人」の割合は1/3ずつぐらいです。新規にジョインするメンバーは基本的にDocker環境を利用しています。

本記事では上記の2パターンのDocker開発環境の構成とポイントについて紹介します。


# ミドルウェアのみDocker化するパターン

## 構成

![image](https://qiita-image-store.s3.amazonaws.com/0/7055/4ff1f790-e3e8-2620-43aa-ba6be886cecc.png)

Railsに直接接続するのではなく、Nginxも設定してローカル開発環境で利用しています。
このRailsとNginxを除いたミドルウェア群をDocker化する構成です。

Nginxは通信の方向からDocker化できないためHomebrewでインストールしたものを利用しています。

この構成では`docker-compose up`とするだけで、すべてのミドルウェアが起動するようになっています。


## ポイント

### ポート番号定義の分離

設定を定義した`docker-compose.yml`はリポジトリにコミットしています。ここにポート定義を含めてコミットしてしまうと、もしローカルで同一ポートを利用するサービスを立ち上げる場合に困ります。

そこで、Docker Composeはデフォルトで`docker-compose.yml`に加え、`docker-compose.override.yml`を上書き用ファイルとして読み込むこと[^1]を利用します。

[^1]: [Share Compose configurations between files and projects - Docker](https://docs.docker.com/compose/extends/#/understanding-multiple-compose-files)

`docker-compose.yml`に公開用ポートを指定せずにサービスだけ定義します。

```yaml:docker-compose.yml
# ミドルウェアのみを定義したComposeファイル
version: '2'
services:
  # 永続化領域
  storage-elasticsearch:
    image: busybox
    volumes:
      - /usr/share/elasticsearch/data
  storage-mysql:
    image: busybox
    volumes:
      - /var/lib/mysql

  # ミドルウェア
  elasticsearch:
    image: quay.io/crowdworks/elasticsearch
    volumes_from:
      - storage-elasticsearch
  elasticsearch-test:
    image: quay.io/crowdworks/elasticsearch
  fake_sqs:
    image: quay.io/crowdworks/fake_sqs
    hostname: crowdworks.localdomain
    command: /usr/local/bundle/bin/fake_sqs --bind crowdworks.localdomain
  memcached:
    image: memcached
  mysql:
    image: quay.io/crowdworks/mysql
    volumes_from:
      - storage-mysql
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: "true"
```

そして`docker-compose.override.yml`に公開用ポートを定義します。

```yaml:docker-compose.override.yml
# Composeファイルの設定を上書きする設定
#
# docker-compose.override.ymlにコピーして利用してください。
# Composeファイルが明示的に指定されていない場合は自動的に読み込まれます。
#
version: '2'
services:
  # ホスト側にバインドするポート番号の設定
  elasticsearch:
    ports:
      - "9200:9200"
  elasticsearch-test:
    ports:
      - "9250:9200"
  fake_sqs:
    ports:
      - "4568:4568"
  memcached:
    ports:
      - "11211:11211"
  mysql:
    ports:
      - "3306:3306"
```

`docker-compose.override.yml`は`.gitignore`に追加しておき、リポジトリには`docker-compose.override.yml.sample`をコミットしておき、メンバーには初めに一度だけコピーして貰います。ポート番号を変更したい場合は、この`docker-compose.override.yml`とRails側の設定を変更します。


## 問題点

`docker-compose.override.yml`は上書き用なのだから、`docker-compose.yml`にデフォルトの定義をしておけばポート番号を変更しない限り不要なのでは？と考えます。しかし、それはうまくいきません。

例えば次のように`docker-compose.yml`と`docker-compose.override.yml`を定義したとします。

```yaml:docker-compose.yml
version: '2'
services:
  mysql:
    image: mysql
    ports:
      - "3306:3306"
```

```yaml:docker-compose.override.yml
version: '2'
services:
  mysql:
    ports:
      - "13306:3306"
```

このとき、公開用ポートの設定は上書きではなく追加され、両方とも有効になってしまいます。

```shell-session
$ docker-compose config
networks: {}
services:
  mysql:
    image: mysql
    ports:
    - 13306:3306
    - 3306:3306
version: '2.0'
volumes: {}
```

そのため、`docker-compose.yml`にデフォルトの定義ができないというわけです。

これに対し、上書き用の書式を用意して対応するという提案のPull Requestが出されています。
[Add overwrite strategy options to config by CarstenHoyer · Pull Request #3939 · docker/compose](https://github.com/docker/compose/pull/3939)

もし、このような機能が実装されれば、`docker-compose.override.yml`はオプションにすることができます。


# ミドルウェアに加えてRailsもDocker化するパターン

## 構成

![image](https://qiita-image-store.s3.amazonaws.com/0/7055/f9206d2d-4282-1bbe-e1cd-7fe132b854a2.png)

ミドルウェアに加えて、RailsもDocker上で動かします。RailsがDocker化されることにより、通信の方向上Docker化できなかったNginxもDocker化できます。

この構成ではRailsのData-onlyコンテナも用意しています。このData-onlyコンテナの元となるDockerイメージには、あらかじめ`bundle install`されたGemファイルが含まれています。このData-onlyコンテナを`volumes_from`で指定することにより、コンテナを再構築する場合でも高速に`bundle install`が完了します。


## ポイント

### ミドルウェア版とのComposeファイルの組合わせ

この構成では、先述したミドルウェア版の`docker-compose.yml`とRailsを定義した`docker-compose.rails.yml`を組み合わせて使います。

Docker Composeでは複数のComposeファイルを渡すことによって組み合わせて使うことが可能です。ミドルウェア版では自動的に`docker-compose.override.yml`が読み込まれることを利用していましたが、これを明示的に行うことで別のComposeファイルによる上書きが可能となっています。

```yaml:docker-compose.rails.yml
# Railsアプリケーションを追加で定義したComposeファイル
version: '2'
services:
  # 永続化領域
  storage-rails:
    image: quay.io/crowdworks/crowdworks
    volumes:
      - /usr/local/bundle

  # ミドルウェア
  nginx:
    image: quay.io/crowdworks/nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - .:/opt/app
      - /sockets

  # Railsアプリケーション
  rails:
    image: quay.io/crowdworks/rails
    volumes:
      - .:/usr/src/app
    volumes_from:
      - storage-rails
      - nginx
    links:
      - elasticsearch:elasticsearch
      - elasticsearch-test:test_elasticsearch
      - fake_sqs:fakesqs.localdomain
      - fluentd:fluentd
      - memcached:memcached
      - mysql:mysql
      - nginx:nginx
    command: bundle exec rails server

  # ジョブ
  delayed_job:
    image: quay.io/crowdworks/rails
    volumes:
      - .:/usr/src/app
    volumes_from:
      - storage-rails
    links:
      - elasticsearch:elasticsearch
      - fake_sqs:fakesqs.localdomain
      - fluentd:fluentd
      - memcached:memcached
      - mysql:mysql
    command: bundle exec rake jobs:work
  shoryuken:
    image: quay.io/crowdworks/rails
    volumes:
      - .:/usr/src/app
    volumes_from:
      - storage-rails
    links:
      - elasticsearch:elasticsearch
      - fake_sqs:fakesqs.localdomain
      - fluentd:fluentd
      - memcached:memcached
      - mysql:mysql
    command: bundle exec shoryuken -R -C config/shoryuken.yml
```

上記のように定義した`docker-compose.rails.yml`と、ミドルウェアを定義した`docker-compose.yml`を`-f`オプションで複数渡すことで指定します。

```shell-session
$ docker-compose -f docker-compose.yml -f docker-compose.rails.yml run --rm rails bundle exec rails console
```

しかし、このオプションを毎回指定するのは面倒です。そこでDocker Composeには環境変数`COMPOSE_FILE`が用意されています。[^2]

[^2]: [Compose CLI environment variables - Docker](https://docs.docker.com/compose/reference/envvars/#/composefile)

この環境変数にはComposeファイルを`:`で区切ることにより、複数渡すことができます。(Windowsの場合は`;`)
direnvなどを使ってあらかじめこの環境変数に設定しておけば、`-f`オプションを毎回付与せずに済みます。

```bash:.envrc
export COMPOSE_FILE=docker-compose.yml:docker-compose.rails.yml
```

ここで注意点として、`-f`オプションや`COMPOSE_FILE`を指定した場合は、自動で`docker-compose.override.yml`は読み込まれなくなります。

この構成の場合、公開用のポートは`docker-compose.rails.yml`でNginxのポートだけを指定しており(このポートだけは専有してしまうことを許容してしまっています:sweat_smile:)、Dockerのlink機能によって通常はNginx以外の公開用ポートを指定する必要はありませんが、必要に応じて`docker-compose.override.yml`を渡すことでホスト側にポートを公開することが可能です。


## 問題点

この構成の場合、Railsアプリケーションのコードをホスト側から取得するため、Docker for Macを使うとコードが巨大な場合に遅くて使い物にならない問題があります。そのため、この構成は[dinghy](https://github.com/codekitchen/dinghy)を使っているメンバーのみが選択しています。

次のPull Requestで`~/Library/Containers/com.docker.docker/Data/database/`配下の設定を書き換えて改善させるワークアラウンドが紹介されていました。
[Severe Docker 1.12.1 performance regression with DB2 images (~10x slower) · Issue #668 · docker/for-mac](https://github.com/docker/for-mac/issues/668)

しかし、[fio](https://github.com/axboe/fio)を使ってベンチマークを取ってみましたが、劇的な改善は見られませんでした。

ベンチマーク用fioファイル:

```ini:myjob.fio
[Sequential-Read] # jobの名前
rw=read           # シーケンシャルでreadする
directory=/tmp/   # ベンチマークで使うディレクトリ (Docker上で測定する場合はマウントしたパスに変更)
size=1g           # ベンチマークで使用するデータサイズ。キャッシュサイズを考慮して決める。
#ioengine=libaio   # 非同期I/Oでテストする。指定しないとsync(同期I/O)になる。

[Sequential-Write]
rw=write          # シーケンシャルでwriteする
directory=/tmp/
size=1g
#ioengine=libaio

[Random-Read]
rw=randread       # ランダムでreadする
directory=/tmp/
size=1g
#ioengine=libaio

[Random-Write]
rw=randwrite      # ランダムでwriteする
directory=/tmp/
size=1g
#ioengine=libaio
```

素のmacOS上の測定結果:

```
Run status group 0 (all jobs):
   READ: io=2048.0MB, aggrb=49269KB/s, minb=24634KB/s, maxb=510007KB/s, mint=2056msec, maxt=42565msec
  WRITE: io=2048.0MB, aggrb=44262KB/s, minb=22131KB/s, maxb=554508KB/s, mint=1891msec, maxt=47380msec
```

Docker for Macデフォルト:

```
Run status group 0 (all jobs):
   READ: io=2048.0MB, aggrb=17763KB/s, minb=8881KB/s, maxb=375295KB/s, mint=2794msec, maxt=118058msec
  WRITE: io=2048.0MB, aggrb=26915KB/s, minb=13457KB/s, maxb=13607KB/s, mint=77056msec, maxt=77915msec
```

Docker for Mac flush sync無効化:

```
Run status group 0 (all jobs):
   READ: io=2048.0MB, aggrb=19849KB/s, minb=9924KB/s, maxb=340225KB/s, mint=3082msec, maxt=105653msec
  WRITE: io=2048.0MB, aggrb=29665KB/s, minb=14832KB/s, maxb=15376KB/s, mint=68194msec, maxt=70693msec
```

あまり依存関係を増やしたくなかったため避けていましたが、3つ目の構成パターンとして[docker-sync](http://docker-sync.io/)を使うパターンを追加してみる予定です。


# まとめ

`docker-compose.override.yml`や環境変数`COMPOSE_FILE`を使って複数の構成パターンを実現する方法を紹介しましたが、これらに限らず、Dockerの進化は速いため気付くとオプションが追加されていたりします。ぜひDockerの便利機能を活用して、よいDockerライフをお送りください。

この記事は[CrowdWorks Advent Calendar 2016](http://qiita.com/advent-calendar/2016/crowdworks) 3日目の記事でした。明日は@tkoshidaのiOSな記事の予定です:santa_tone1: