• 71
    いいね
  • 0
    コメント

はじめに

この記事は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: