Docker(pool)を用いたプレビュー環境を検証する

はじめに

この記事ではmook/poolを使ってdockerでプレビュー環境を作ろうという趣旨のものです。
docker周辺では技術開発が盛んで、利用シーンも多岐にわたるようになりました。
プロダクション環境での利用、開発時の開発者の環境統一、テストの実行などと行った具合です。
今回はdockerを用いたプレビュー環境の可能性を検証します。

私は普段railsのアプリケーションを開発しています。
railsアプリ開発だけに注目しても、ユーザーが触れるwebの側面もありますし、APIの提供もあります。
さらには、rails開発者は開発の拠点が地理的に分散しています。
もちろんrails開発者以外にもiPhone/Android開発、デザイナー、ディレクターとチームには様々なメンバーが集まっています。
docker登場以来、チーム開発でどのように有効利用できるかを模索してきました。
その中でも期待していたのはプレビューの環境です。

チームとして何を問題意識として持っていたのか言うと、例えば複数の新機能開発が同時に進行していた場合の確認です。
もちろんテストコードによって一定の信頼は担保できますが、デザインや画面遷移なども確認したいわけです。
APIの提供となれば、モバイルアプリ開発チームは実際に接続して確認したいという要望もあります。
rails開発者同士のレビューならばgit checkoutしてブランチを切り替えてローカルで確認できます。
モバイルアプリ開発者の場合でも、もちろんgitの操作はできます。
しかしrake db:migrateしてその他rakeタスクを実行して…といったrailsの操作や作法を強要することは避けたいです。
rails/モバイルアプリエンジニア間のハードルに関しては、ローカルでdockerを用いることで、一定の問題解決を図ることが出来ます。
共通知識としてdockerの操作を覚えることで、railsアプリの更新時にはgit checkoutしてdocker build, docker runすれば動作するようにすれば良いのです。
ですがこの方法はエンジニアには通用してもデザイナーや、ましてやディレクター(Windows環境ならなおさら)にこれを要求することは難易度が高いです。
チームにはインテグレーションやステージングと呼ばれるサーバー環境ももちろん構築してあります。
これらの環境はAWS OpsWorksで構築してあるため、ブランチを切り替えてデプロイし直すということは可能です、
しかしリリース用の検証を行うことが主目的であり、ディレクターや複数のステークホルダーが同時に異なる機能を確認したいときには対応が困難です。(ブランチを切り替えてデプロイする毎に確認作業が必要になるため)
このような問題を解決するべく、つまりrails開発者以外でも簡単に機能毎のブランチを切り替えて確認できるような環境の構築が望まれていました。
今回はプレビュー環境を構築する為にmook/poolを使い、これを検証してみます。

poolとは

poolはシンプルな紹介をすればリバースプロキシです。
コア部分はmod_mrubyで開発されており、アクセスに応じて動的にdockerアプリケーションのビルド、起動、そしてリクエストのフォーワードを行ってくれます。

たとえばhttp://master.pool.devならmasterブランチの、http://feauture.pool.devならfeautureブランチのソースを取得してビルドを行ってくれます。
サブドメイン部分にはブランチ名の他にもコミットIDやタグを指定することも可能です。

内部実装については、作者様自身の解説「mod_mrubyとDockerを使ってプレビュー環境を作成するプロキシサーバを作った」があるので参考になります。

セットアップ

ローカルで試すためにはvagrantが必要です。
vagrantのDNSプラグインも必要になるので用意しておきます。

vagrant plugin install vagrant-dns
vagrant dns --install
vagrant dns --start

後はgit clone https://github.com/mookjp/pool.gitして設定を済ませてvagrant upするだけです。
設定は最低限デプロイ対象となるリポジトリを指定しておきます。

s.args << "https://github.com/k-shogo/mini_doc.git"

初回はvagrant boxの用意や、pool自体のビルドも必要なので気長に待ちます。
準備が完了すればhttp://master.pool.devにアクセスすれば、設定したリポジトリからクローンし、リポジトリ内部のDockerfileに応じてビルドを行ってくれます。

公式リポジトリにはAWS EC2へのインストール方法も記載されています。

railsアプリ側の準備

poolの環境構築が済んだので、pool上でrailsアプリケーションを動作させてみます。
検証用のリポジトリはk-shogo/mini_docに用意してあります。
railsをdockerで動作させたい場合の構成については、以前に「railsをdockerで動かしたい場合の構成はどうするべきか」にまとめました。
投稿時(2014/07/08)よりも環境の整備が進んでおり、dockerには公式のLANGUAGE STACKSという物が登場しました。
railsのstackも用意されており、onbuildがサポートされたDockerfileが用意されているので、
最小限の用意ならDockerfileにFROM rails:onbuildと書くだけでも良いのですが、
細かくコントロールしたかったので公式のrubyイメージをFROMにして始めます。

一つ目のポイントとして、DBについてはどうするか。
プロダクション環境ではmysqlなりpostgresqlやAmazon RDSを使うのですが、プレビュー環境において複数コンテナの起動や、外部のDBに接続することは面倒が増えるだけです。
データベースを共用するよりもむしろコンテナ毎に独立している方が都合が良いのでsqliteを用いることにします。
bundlerによって依存するgemを管理するrailsでは、Gemfileにgroupを記述することで対処します。

例えばプレビューだけsqliteで、その他でMySQLを使い場合のGemfileです。
このようにした場合、ローカルでの開発時にはbundle install --without previewしておけばプレビュー用のgemは開発環境にはインストールされません。

group :preview do
  gem 'sqlite3'
end

group :development, :test, :production do
  gem 'mysql2'
end

railsでは環境自体も複数定義しておくことが可能なので、今回はプレビュー用の環境定義ファイルを用意します。
独自の環境定義はconfig/environments/以下にpreview.rb作成するだけです。
DBはプロダクションとは異なる構成にしましたが、その他はプロダクションと近くしたいところです。
eager_loadを有効化し、アセットもプリコンパイルして動作を確認しましょう。
ただ、アセットファイルの配信についてはDBと同じく、フロントに立てるnginx等を別に用意したくないので、railsサーバー自身が静的ファイルを配信出来るようにserve_static_filesの有効化を行っておきます。

Rails.application.configure do
  config.cache_classes = true
  # eager_load を有効化
  config.eager_load = true
  config.consider_all_requests_local       = false
  config.action_controller.perform_caching = true
  # 静的ファイルを直接配信する
  config.serve_static_files = true
  config.assets.js_compressor = :uglifier
  config.assets.compile = false
  config.assets.digest = true
  config.log_level = :debug
  config.i18n.fallbacks = true
  config.active_support.deprecation = :notify
  config.log_formatter = ::Logger::Formatter.new
  config.active_record.dump_schema_after_migration = false
end

Dockerfileは以下のようにしています。
公式LANGUAGE STACKSのrubyをベースに、sqliteとアセットのコンパイルに必要なnodeをインストールしています。
先にGemfile, Gemfile.lockをコピーしているのは、Gemfileに変更が無い場合にbundle installをスキップして高速化を図る工夫です。
Dockerfileの中でのbundle installでは、開発やプロダクション固有のgemをインストールしないようにしてあります。
poolではコンテナと80番ポートを接続するので、EXPOSE 80を指定してあります。

FROM ruby:2.2.0

RUN apt-get update && apt-get install -y nodejs sqlite3 --no-install-recommends && rm -rf /var/lib/apt/lists/*

RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

COPY Gemfile /usr/src/app/
COPY Gemfile.lock /usr/src/app/
RUN bundle install -j4 --without development test production

ADD . /usr/src/app

ENV RAILS_ENV preview
RUN bundle exec rake db:create && \
    bundle exec rake db:migrate && \
    bundle exec rake db:seed_fu && \
    bundle exec rake assets:precompile

EXPOSE 80
CMD ["rails", "server", "-b", "0.0.0.0", "-p", "80"]

プレビュー用途なので、ある程度確認用のデータが欲しくなります。
railsにはシードデータを記述するdb/seeds.rbがデフォルトで用意されていますが、何度もrake db:seedすると重複して登録されてしまったりするため、今回はmbleigh/seed-fuを用いてデータの投入を行っています。
このseed-fuを利用すると、データの投入や環境毎の定義を分割することを容易にしてくれます。

.dockerignoreもちゃんと用意しておきましょう。
ローカルでビルドするときなど、余分なファイルをADDしなくなるので、時間の短縮やビルド時のキャッシュに役立ちます。
例えばログファイルがADD対象になっている場合、アプリケーションのコードに変更が無くても、development.logに変化があるだけでキャッシュが使われなくなってしまいます。

### Rails ###
public/assets/*

## Environment normalisation:
.bundle
vendor/bundle
.git

## db
db/*.sqlite3

## log
log
tmp

## OSX
.DS_Store

poolを使ってみてのまとめと今後の課題

poolのセットアップと、railsアプリ側の準備さえしてしまえば、ブラウザからのアクセスによってコンテナのビルドから行ってくれるのはお手軽です。
開発環境をローカルに持っていないディレクターには例えば「http://design_fix.pool.devにアクセスしれもらえば新しいデザインを直接確認できますよ」と教えさえすれば良いのです。
(pool側で初回ビルドの場合時間がかかるので、一度自分でアクセスしてビルドしてから教えるのが良し)
複数の変更作業が同時に走っていても、プレビューで対応できるのは無駄な待ち時間が発生しなくてグッドです。

次に課題について。
第一には「やっぱりDBはMySQLで確認したい!」とか「redis, memcache, elasticsearch, solrをrailsアプリから利用したいんだけど」「バックグランドで動作するワーカーも動作させたい(resqueとかsidekiqとか)」の場合にどうするかについて。
「poolをfigを使って複数コンテナ動作するように拡張する」というのも一つの選択肢でアリだと考えました。
ですが個人的にはとりあえず単一コンテナで動作できるようにDockerfileを用意する事がpool wayなのかなと思っています。
今回はプロダクション環境で使いたいわけでは無く、プレビューの環境なので、単一コンテナで完全に分離できた方が都合が良いからです。
Dockerfileの記述は増えますが、プロダクション環境で無い限り1コンテナ1プロセスを前提とした運用は面倒を増やしかねません。
複数プロセスの起動についてはrailsの場合はddollar/foremanで対応が楽かな。(検証アプリに導入したら追記します)

第二にはDockerfileビルド時の挙動について。
今回のDockerfileの例では、Gemfile及びGemfile.lockをアプリケーション本体のコードより先にADDすることにより、bundle installの結果をキャッシュする手法が組み込まれています。
しかし、Gemfileの中身に変更が無かったとしても、タイムスタンプが変更された場合、dockerはタイムスタンプを含めたメタデータで管理しているためにビルドしたキャッシュが効きません。
これはpool利用時にのみ発生する物ではありません。Gemfileに対してtouchしてみるとビルドのキャッシュが使用されないことを簡単に確認できます。
Jenkinsでdockerをビルドする場合などもこの問題に当たるので、そのような場合はGemfileGemfile.locktouch -tでタイムスタンプを常に一定にすることで回避するテクニックが存在します。
GemfileGemfile.lockのタイムスタンプを常に同じ値にしたとしても、内容が更新されている場合はメタデータが変わっているので問題はありません。
ADDをなるべくキャッシュさせたい場合にtouchを使うのですが、poolではその選択肢をとることが出来ません。
これには「リポジトリにbefore_build.shがあれば実行する」などの拡張をpoolに施すと効率化を図れるかもしれません。

第三にはリポジトリの取り扱いとアクセスコントロールについて。
プレビュー環境の閲覧の制限について、つまりユーザーからpoolへのアクセスはAWSならセキュリティグループで対応することが出来ます。
しかし、poolからアプリのリポジトリへのアクセスはどうでしょうか。
アプリのリポジトリはいつもGitHubで公開できるわけではありません。
Bitbucketを使っていたり、プライベートなgitサーバーを構築している所も多いでしょう。
アプリ本体のリポジトリだけでなく、Gemfile内部に独自のgemを指定している場合もあります。
この場合のssh鍵や認証ユーザーの取り扱いに関してはさらなる検証が必要だと感じました。実際にpoolをAWS EC2等に設置して検証して見たいです。

課題と感じるところはある物の、poolの「アドレスによって動的にコンテナのビルド、起動を行う」というアイデアは強力です。
チーム開発において有効活用できる場面は多いと思います。