複数の rails プロジェクトが共存する開発環境を Docker 化した話を晒してみる

アプリケーションエンジニアの西辻です。
今回のブログでは、弊社のローカル開発環境を Docker 化した話をご紹介したいと思います。 このブログでは、なぜローカル開発環境を Docker 化する考えに至ったのかに始まり、
具体的にどのような方法で Docker 化を進めていったかを振り返りながら書いていきます。
また、Docker 化したことで受けた恩恵などを最後に書いて終わります。

Overview

大きく以下の項目について書いていこうと思います。

  • 自分の仮想化環境への考え方について
  • 今の現場に Docker 開発環境を導入する判断について
  • 実運用している構成例を用いての説明
  • Docker for Mac のファイルI/Oのパフォーマンス改善方法
  • Tips: Docker コンテナに対して binding.pry を利用する
  • ローカル開発環境を Docker 化した恩恵

自分の仮想化環境への考え方について

まず、なぜローカル開発環境を Docker 化したのかを説明したいと思います。
ただ流行っているから Docker 化した訳ではないのと、
こういう話は同じチームメンバーに言語化して伝えた方がいいと思い
この場を借りて自分の仮想環境に対する考え方を共有したいと思います。

前置きが長くなりましたが自分の仮想化環境に対する考え方について書いていきます。

前職で新卒で入った当時は本当に無知で自分のローカル開発環境を整えることに多くの時間を割きました。
支給されていた Windows マシンをセットアップして様々なツールや言語をインストールしていき、
プログラムがうまく動かない時にローカル開発環境に依存した問題に当たることも多かったです。
また、デプロイする先が Linux サーバなのでそことの環境差分による問題もよくあった記憶があります。

その当時からローカル開発環境に対して、クリーンで誰でも再現性のある環境を作って
その中で開発をしていきたいという思いが芽生えました。

開発チームも変わり VirtualBoxLinux デスクトップ環境を作って
それを配布するという経験をしました。
簡単に作って壊せる環境なのとスナップショットという機能で何か問題が起きても
比較的容易にロールバックできるということで Windows 上に VirtualBox で仮想環境を構築するのが
当時のクリーンな開発環境として自分の中で認識されました。
デスクトップ環境だったのは Eclipse などの IDE を利用したかった理由が大きいです。

ただ、デスクトップ環境となると比較的イメージサイズが大きくなりました。
Vagrant などを利用してなるべくイメージに入れる必要のないものを外に逃して作成時にインストールする流れでした。

Vagrant は非常に便利なツールでちょっとしたことを試す際には非常に重宝した記憶があります。
また、中の環境がファイルとして記述できるのにも感動しました。
VirtualBox でやろうとすると構築手順等どうしてもドキュメントの更新が遅れたりして
結局最新のイメージを誰かからもらうみたいな運用になってた気がします。

そんなこんなで Docker というツールには驚かされました。
まず、起動が速い。
docker-compose によるコンテナ同士の連携がやりやすい。
環境を作って壊すことに対して抵抗が全くなくなる。
他の人へ配布が簡単。
ファイルの書き方もシンプルでした、次世代感がありました。

とまあ、Docker への印象は最高の一言でした。
自分がローカル開発をする環境は Docker を置いて他にないと感じました。

今の現場に Docker 開発環境を導入する判断について

前述の通り、新しい環境で開発するならローカル開発環境は Docker がいいなーと思ってました。
結果から言うとだいたい1ヶ月くらいで Web アプリ近辺は全て Docker 化できました。
これはシステム構成がどのようになっているかを把握しているかどうかでかかる時間は大きく変わると思います。
自分の場合だと入社してすぐの取り組みだったので全体のシステム構成の理解をしながらやれたのがよかったですね。

Docker 化するにあたって以下の条件を満たしていたのが大きいです。

  • Docker for Mac が十分実用レベルだと思った
  • 全員 Mac で開発を行っている( Windows でも動作はするはずだけど揃ってた方が楽なのは間違いない)
  • 基本的にシンプルな構成の rails アプリが多かった
  • rails メインなので Ruby Mine をホストマシンから利用してコンテナ内のソース変更で十分だった(Linux Destop環境は不要)
  • メンバーの Docker への抵抗が少ない(ここが一番重要かもしれない)

Docker 化するにあたり、まずは依存度が少ないものを選びました。
ここはあまり一般化できないかもしれないですが、単独で動作する API などが狙い目ですね。

実運用している構成例を用いての説明

ここからは実際の運用例をご紹介したいと思います。
全体的なディレクトリ構成は以下になります。
今回は api ディレクトリに注目して説明を進めていきます。
api ディレクトリを理解できれば
他のディレクトリも同じ考え方で作成できると思います。
docker-compose.yml を起点にして、各アプリディレクトリ(api, admin, batch)にある
Dockerfile を利用して全体を docker-compose として起動するのが基本の考え方になります。
後述しますが、Docker for Mac は大量のファイルI/Oに対して極度にパフォーマンスが落ちます。
rails precompile などを利用しているとファイルの同期量で一気に遅くなるので
docker-sync というツールを利用して上記問題を解消しています。
docker-sync で利用するファイルが
docker-compose-dev.ymldocker-sync.yml の2ファイルになります。

├── README.md
├── cleanup.sh
├── db
│   ├── Dockerfile
│   ├── init.sql
│   └── my.cnf
├── docker-compose-dev.yml
├── docker-compose.yml
├── docker-sync.yml
├── api
│   ├── Dockerfile <- 各アプリディレクトリに置きます
│   ├── Gemfile
│   ├── Gemfile.lock
│   ├── app
│   ├── bin
│   ├── bitbucket-pipelines.yml 
│   ├── config
│   ├── config.ru
│   ├── coverage
│   ├── lib
│   ├── log
│   ├── public
│   ├── scripts
│   ├── spec
│   ├── tmp
│   └── vendor
├── admin
│   ├── Dockerfile
│   ├── 長いので省略
├── batch
    ├── Dockerfile
    ├── 長いので省略

基本的な考え方は docker-compose.ymlbuild: したいイメージ単位で記載します。
公式のイメージがそのまま使えるものは image: をそのまま指定するだけです、簡単ですね。
以下に実際に利用しているものを簡易化した docker-compose.yml を記載します。

version: '3'
services:
  admin:
    build: admin # admin ディレクトリにある Dockerfile を利用することを明記
    command: bundle exec rails s -p 4000 -b '0.0.0.0'
    volumes:
      - ./admin:/admin # admin ディレクトリをアプリディレクトリとしてマウント
    ports:
      - "4000:4000"
  api:
    build: api
    command: bundle exec rails s -p 3001 -b '0.0.0.0'
    volumes:
      - ./api:/api
    ports:
      - "3001:3001"
    tty: true # binding.pry で利用
    stdin_open: true # binding.pry で利用
    environment:
      REDIS_HOST: redis # 環境変数を設定できます
      DATABASE_HOST: db
      DATABASE_USER: user
      DATABASE_PASSWORD: password
  db:
    build: db # database 作成や my.cnf をカスタマイズするので独自の Dockerfile を利用します
    restart: always
    ports:
      - 3306:3306
    environment:
      MYSQL_ROOT_PASSWORD: dummy
      MYSQL_USER: dummy
      MYSQL_PASSWORD: dummy
  redis:
    image: redis:3.0.7 # 公式イメージが利用できるものはそのまま利用します
    ports:
      - "6379:6379"

api ディレクトリにある Dockerfile は以下のようになります。
特になんの変哲も無いよくある rails アプリ用の Dockerfile になります。
以下のような感じで必要に応じて各アプリディレクトリ(api, batch, admin など)に Dockerfile を設置します。
様々なツールをあらかじめインストールしている必要があるものは
DockerHub のプライベートリポジトリに Automated Build として登録して利用したりしてます。

FROM ruby:2.4.1 # 色んなツールが入ったカスタムされたもの利用したりもしてます
ENV LANG C.UTF-8
ENV TZ Asia/Tokyo

# To chache gems
WORKDIR /tmp
ADD Gemfile Gemfile
ADD Gemfile.lock Gemfile.lock
RUN bundle install -j8

WORKDIR /api
ADD ./ /api

ちなみに db は以下のような Dockerfile になります。 init.sql/docker-entrypoint-initdb.d 以下に
設置しておくと docker-compose build 時に勝手に sql を流してくれるので楽です。

FROM mysql:5.6
COPY my.cnf /etc/mysql/conf.d/
ADD init.sql /docker-entrypoint-initdb.d

Docker for Mac のファイルI/Oのパフォーマンス改善方法

前述しましたが、Docker for Mac は大量のファイルI/Oに弱いです。
API など View を持たないものはそのままでも問題ないのですが、
View を持つ admin, front などは rails precompile の影響もありブラウザレンダリングが極端に遅くなります。
上記問題を解決するために docker-sync というツールを利用します。

https://github.com/EugenMayer/docker-sync

docer-sync はシンプルな設定ファイルを設置するだけで
ファイルI/Oの問題を解決してくれます。
公式ドキュメントも充実しているので細かいオプションなどは
上記の github を参照するのがいいでしょう。
必要なファイルは docker-compose-dev.yml, docker-sync.yml の2ファイルだけです。
docker-compose-dev.yml の例は以下になります。
sync させたいディレクトリパスを service に記載します。

version: '3'

services:
  admin:
    volumes:
      - admin-sync:/admin:nocopy
  api:
    volumes:
      - api-sync:/api:nocopy
  front:
    volumes:
      - front-sync:/front:nocopy

volumes:
  admin-sync:
    external: true
  api-sync:
    external: true
  front-sync:
    external: true

docker-sync.yml の例は以下になります。
sync から除外したいディレクトリを指定したりできます。
syncs 以下で命名している名前と docker-compose-dev.yml 内の名前は揃える必要があります。

version: '2'
syncs:
  admin-sync:
    src: './admin'
    sync_excludes: [ '.git', '.idea', 'tmp', 'log' ]
    sync_excludes_type: 'Name'
  api-sync:
    src: './api'
    sync_excludes: [ '.git', '.idea', 'tmp', 'log' ]
    sync_excludes_type: 'Name'
  front-sync:
    src: './front'
    sync_excludes: [ '.git', '.idea', 'node_modules', 'tmp', 'log' ]
    sync_excludes_type: 'Name'

ここまで揃うと docker-sync に内包されているコマンドの
docker-sync-stack startdocker-compose.yml を起点に置いた
各アプリが起動します。
docker-sync-stack start コマンドは docker-compose upsync がついてる感じです。

ファイルを更新すると Sync Log が流れます。

   Sync Log:  UNISON 2.48.4 started propagating changes at 19:15:01.22 on 23 Oct 2017
   Sync Log:  [BGN] Updating file app/controllers/application_controller.rb from /host_sync to /app_sync
   Sync Log:  [END] Updating file app/controllers/application_controller.rb
   Sync Log:  UNISON 2.48.4 finished propagating changes at 19:15:01.22 on 23 Oct 2017
   Sync Log:  Synchronization complete at 19:15:01  (1 item transferred, 0 skipped, 0 failed)

docker-sync が優れているところは
docker-compose-dev.ymldocer-sync に必要な設定を上書きしていることです。
つまり、いつか Docker for Mac のファイルI/O問題が解決したら docker-compose-dev.yml を消すだけで
従来の docker-compose up での起動に切り替えができます。

Tips: Docker コンテナに対して binding.pry を利用する

binding.pry を利用方法をチームメンバーが見つけてくれたので設定方法を書いて置きます。
以下のように設定しておくと docker attach で対象のコンテナにて binding.pry が利用できます。

version: '3'
services:
  admin:
    tty: true # binding.pry で利用
    stdin_open: true # binding.pry で利用

ローカル開発環境を Docker 化した恩恵

長々と書きましたが、最後にローカル開発環境を Docker 化した恩恵を書いてまとめにしようと思います。
個人的には Docker という仮想化技術は今後ますます様々なプラットフォームで利用されていくと思います。
現に前回のブログで書いた Bitbucket Pipelines なども Docker 化していることでだいぶ楽に導入できました。
また、現在は ECS で一部運用しているサービスもあり、それらも Docker の知識があることで
すんなり入れた気がします。
別エントリにはなると思いますが、全てを ECS 化する話も社内では上がっており、
土台としてローカル環境が Docker 化できているのは
導入にあたっての障壁を一段減らせていると思います。
今回のブログが皆さんのローカル開発環境改善の手助けになれば幸いです。


Housmartでは不動産業界を変えるカウルを支えるエンジニアを募集しています。
今話題のReTech!業界を変えるカウルを支えるエンジニアをWanted!