Git
docker
docker-compose

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

mixiグループ Advent Calendar 2017の9日目は、XFLAGスタジオCREチーム1@upscentがお送りします。

開発環境構築は何をするにしても通らなければいけない道です。
Docker Composeを使うと便利だなあと改めて思ったので、開発環境を構築する上でこの記事が参考になれば幸いです。

Docker Composeを使おうと思った理由

先に私の所属するCREチームの開発フローについて説明します。

CREチームの開発フロー

CRE(Customer Reliability Engineering)チーム1は、XFLAGスタジオが提供する様々なプロダクトにおいて、お客様の信頼を最大化することをミッションとして日々業務を行っています。

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 時にオプションを付け忘れる
ポートを開け忘れたり、ボリュームを付け忘れたりするとつらいです。

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

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

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

とすることにしました。

Docker Composeの導入

以下、アプリケーションフレームワークにPadrino、DBにMySQL、KVSにRedisを利用した環境構築を例に話を進めていきます。

(1) 1コンテナ1プロセスの構成に切り替える

ネックとなるのは、下図左のように1コンテナに複数プロセスを立ち上げる構成に固定されていることです。これを下図右のように1コンテナ1プロセスの構成に切り替えます。

image.png

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

という対応を入れてみます2

(1-a) Padrinoコンテナから別コンテナで起動しているMySQL、Redisへ接続する

Docker Composeにはlinksオプションがあります。
linksオプションを使うと他のコンテナへの接続設定が追加できます。この設定にはエイリアスをつけることができ、エイリアスをホスト名として他のコンテナに接続できるようになります。

今回の例では、Padrinoサーバを立てるコンテナからMySQLサーバを立てるコンテナに対して db_container 、Redisサーバを立てるコンテナに対して kvs_container というエイリアスを設定しておきます(Docker Composeの設定ファイルの書き方docker-compose.ymlの書き方は後述します)。そうすると、アプリケーションコードに書かれているDBサーバ、Redisサーバのホスト名をそれぞれ db_containerkvs_container に置換するだけでPadrinoからそれぞれのサーバに接続できるようになります。

(1-b) ホスト名の変更差分を git add できないようにする

ホスト名を置換してしまうと、Git管理されているファイルであればコミット対象となります。
ですがこの差分はコミットしたくないので、そもそも git add ができないようにGitの設定を下記の通り変更します3

$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
$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をビルドしたイメージで作ったコンテナを利用します。 links オプションも忘れずに設定しておきます。

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
FROM ruby:2.2

ENV APP_ROOT /app
WORKDIR $APP_ROOT

ADD ./replace_host.sh /.

RUN gem install bundler -v '1.12.5'
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にある公式のコンテナが使えます。
インストール用のシェルコマンドを自分で記述する必要がなく、バージョンアップが必要な場面でもDockerイメージを変更することで簡単に対応できます。

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

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

アプリケーションの起動が楽になった
docker-compose up -d で必要なサービスがすべて起動できます。
忘れん坊の方もこれで安心です。

おわりに

開発環境の構築方法は試行錯誤していましたが、今のところこれが一番しっくりきています。
サンプルコードを作ったので実際に試してみたい方はupscent/devenv-composer-sampleをご利用ください。
不便になったらちょこちょこ改善していきたいと思います。


  1. CREについて詳しく知りたい方は mixiグループ Advent Calendar 2017の2日目の CREチームを設立しました! をご覧ください :smile: 

  2. 環境変数等で向き先を変えられるのであればそれが一番です! 

  3. 参考: gitでファイルの一部だけコミットから無視する