Edited at

CrowdWorksのDocker開発環境

More than 1 year has passed since last update.


はじめに

この記事はCrowdWorks Advent Calendar 2016 3日目の記事です:christmas_tree:

今回はクラウドワークスが運営しているクラウドソーシングサービスであるCrowdWorksのRails開発環境について紹介します。

Qiitaにもたくさん記事が投稿されているとおり、Dockerを開発環境に利用するのはだいぶ一般的になってきたように感じます。クラウドワークスでもローカルでDocker Composeを利用して開発環境を構築できるようにしています。

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


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

  2. ミドルウェアに加えてRailsもDocker化

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

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


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


構成

image

Railsに直接接続するのではなく、Nginxも設定してローカル開発環境で利用しています。

このRailsとNginxを除いたミドルウェア群をDocker化する構成です。

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

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


ポイント


ポート番号定義の分離

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

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

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


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に公開用ポートを定義します。


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.ymldocker-compose.override.ymlを定義したとします。


docker-compose.yml

version: '2'

services:
mysql:
image: mysql
ports:
- "3306:3306"


docker-compose.override.yml

version: '2'

services:
mysql:
ports:
- "13306:3306"

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

$ 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

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


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


構成

image

ミドルウェアに加えて、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ファイルによる上書きが可能となっています。


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オプションで複数渡すことで指定します。

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

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

この環境変数にはComposeファイルを:で区切ることにより、複数渡すことができます。(Windowsの場合は;)

direnvなどを使ってあらかじめこの環境変数に設定しておけば、-fオプションを毎回付与せずに済みます。


.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を使っているメンバーのみが選択しています。

次の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

しかし、fioを使ってベンチマークを取ってみましたが、劇的な改善は見られませんでした。

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


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を使うパターンを追加してみる予定です。


まとめ

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

この記事はCrowdWorks Advent Calendar 2016 3日目の記事でした。明日は@tkoshidaのiOSな記事の予定です:santa_tone1: