mod_mrubyとDockerを使ってプレビュー環境を作成するプロキシサーバを作った


この記事は [mod_mruby ngx_mruby Advent Calendar 2014](http://qiita.com/advent-calendar/2014/mod-ngx-mruby) 8日目の記事です。

---

mod_mrubyを使って、プレビュー環境をDockerコンテナで作成するプロキシサーバ「pool」を作りました。

[mookjp/pool](https://github.com/mookjp/pool)

主に私と、[mod_mruby ngx_mruby Advent Calendar 2014](http://qiita.com/advent-calendar/2014/mod-ngx-mruby)の12日目にエントリーもしている[ainoya](http://qiita.com/ainoya)と2人で開発しています。


## poolについて

### 概要

`http://<ブランチ名 or GitのコミットID>.pool.dev/`のようなURLへアクセスすることにより、このコミットIDの状態のアプリケーションをプレビューすることができます。
URLへのアクセスがトリガーとなるので、プレビュー用の環境作成を開発者以外が実行することも可能です。
デザイナーやディレクターでも、見たい状態のコミットIDを含むURLををクリックするだけで参照することができます。

このプロキシサーバでは、以下のような処理を行っています。

* Apacheをプロキシサーバとし、mod_mrubyのフックでURLのサブドメイン部分に含まれるコミットIDを取得
* サーバ内部のビルドサービスで、取得したコミットIDの状態のアプリケーションのDockerコンテナを作成
	* コミットIDとコンテナIDのひもづけを内部で行う
* コンテナが作成完了していれば、コンテナのポートへフォワード

プレビュー対象となるアプリケーションは、リポジトリのルートに`Dockerfile`を配置し、Dockerコンテナとして起動できるようにしておく必要があります。

### 構成

以下が構成図です。
プロキシとなるpool自体もDockerコンテナとして起動します。Dockerのsockファイルをホスト側と共有することによって、poolのコンテナからアプリケーションのコンテナを作成することができています。

![pool-architecture-japanese.png](https://qiita-image-store.s3.amazonaws.com/0/5882/f6d4c542-9385-9242-c3a7-c251a2607c05.png "pool-architecture-japanese.png")


### 使い方

masterブランチの状態のアプリケーションのプレビューをしたい場合はWebブラウザを開いて`http://master.pool.dev/`へアクセスします。
アクセスを受けた時点で対象のコミットのコンテナがまだ作られていない場合は、コンテナのビルドログを流す画面が表示され、ビルドが完了したら自動でアプリケーションの画面へ遷移します。

以下のGIFは、コンテナがない場合に表示されるビルドログを出力する画面をアニメーションにしているものです。

<img src="https://cloud.githubusercontent.com/assets/1519309/4186488/54415ec4-3761-11e4-9b13-41e09653945f.gif">

ビルドが完了した段階で、アプリケーションの画面へ自動的に遷移しています。

### メリット

production/staging/qaなど決まったサーバを用意しておき、Gitリポジトリにpushするたびに特定のブランチを対象にビルド・デプロイする…というのがよくあるWebアプリケーションのCI環境の構成かと思います。
このプロキシサーバは、とりあえず用意しておけばURLにアクセスするだけである特定のコミット時の状態の環境をつくれます。
自前でAWSなどを使ってCI環境を構築せずとも、URLにアクセスするだけでプレビュー環境をつくれるのはリソースの少ないチームで利点があるのでは、と考えています。


## mod_mrubyで実装したフックについて

mod_mrubyで実装したフックのコードは以下のようになっています。
mrubyで簡潔に記述できるため、フック全体は100行くらいで小さなスクリプトになっています。

[pool/hook.rb at master · mookjp/pool](https://github.com/mookjp/pool/blob/master/docker/pool/handlers/hook.rb)


```rb
# hook.rb
#
# ...省略...

# GitのコミットIDとコンテナのIDをひもづけるIDファイルを作成
# コンテナ作成後はこのファイル内にコミットIDとコンテナIDのセットを書き込む
File.new(ID_FILE, "w") unless FileTest.exist?(ID_FILE)

# サブドメインからコミットIDを取得
hin = Apache::Headers_in.new
target = hin["Host"].split(".")[0]

# リポジトリがまだcloneされていない場合はclone
unless FileTest.exist?(APP_REPO_DIR)
  repository_url = File.open(REPOSITORY_CONF).gets.chomp
  `git clone #{repository_url} #{APP_REPO_DIR}`
  return Apache::return(Apache::HTTP_BAD_REQUEST) \
    unless FileTest.exist?(APP_REPO_DIR)
end

# サーバ内部のAPIサーバを経由してコミットIDを取得
# ※設計見直しのため、gitとdockerへのアクセスはフック内部からではなく、別サービスを経由する実装に修正中
target_commit_id = `curl http://0.0.0.0:9000/resolve_git_commit/#{target}`.chomp
Apache.errlogger Apache::APLOG_NOTICE, \
  "target: #{target}, target_commit_id: #{target_commit_id}"
# コミットIDが正しく取得できない場合はBAD REQUESTを返す
Apache::return(Apache::HTTP_BAD_REQUEST) unless `echo $?`.chomp == '0'

# コミットIDと紐づくコンテナIDがあればコンテナへフォワード
# なければビルド画面へ遷移させる
container_id = get_container_id(target_commit_id)
if container_id == nil
  return_build_screen
else
  forward_to_container(container_id)
end
```

※ 初期バージョンを書いた時、できそうなことはすべてこのフックに詰めてしまいましたが、今は設計を見なおして少しずつ修正を入れています。そのため、現時点では一貫性がない書き方になっています…が、そのうちスッキリする予定です。


## 使い方（Vagrant編）

### セットアップ

[poolのリポジトリ](https://github.com/mookjp/pool)にはVagrantfileが含まれています。Vagrantをインストールすれば、すぐにローカル環境で試してみることができます。

ローカル環境でのDNS設定をするために、vagrant-dnsプラグインを使用しています。プラグインをインストールし、DNS設定を有効にしてください。

```sh
vagrant plugin install vagrant-dns
vagrant dns --install
vagrant dns --start
```

### 設定

Vagrantfile中で、以下を設定することができます。

* アプリケーションのリポジトリURL
	* https://github.com/mookjp/pool/blob/fae14049a6619f39fe9513b48713bb86940e91d5/Vagrantfile#L28
* 内部で動かすコンテナ数の最大値
	* https://github.com/mookjp/pool/blob/fae14049a6619f39fe9513b48713bb86940e91d5/Vagrantfile#L30

デフォルトでは[mookjp/flaskapp](https://github.com/mookjp/flaskapp)という[Flask](http://flask.pocoo.org/)のアプリケーションのリポジトリをプレビュー対象としています。
このリポジトリのルートには[Dockerfile](https://github.com/mookjp/flaskapp/blob/master/Dockerfile)が配置してあり、[ファイル中でアプリケーション起動のコマンドを設定](https://github.com/mookjp/flaskapp/blob/master/Dockerfile#L22)しています。 

### 起動

設定ができたら、サーバを起動します。

```sh
vagrant up
```

おもむろにWebブラウザを起動し、`http://master.pool.dev/`へアクセスしてみてください。
コンテナのビルド画面が表示され、ビルド完了後に`Hello World!`というメッセージが表示される画面（[アプリ側での記述はこちらを参照](https://github.com/mookjp/flaskapp/blob/master/app.py#L4-L8)）へ遷移します。

## 使い方（汎用編）

### 必要条件

導入するマシンは、以下のように設定されている必要があります。

1. Dockerがインストールされている
2. ドメインネームでアクセスできる環境にある
	* 現在コミットIDは先頭のサブドメインを見ることで取得しているため、`XXX.example.com`のようにドメインネームを使ってサーバにアクセスできるようにしておいてください
	* DNSを用意できない場合は、クライアントマシンのhostsを書き換えて上記のようなドメインネームでアクセスできるようにしておいてください
3. `80`と`8080`ポートが利用できる環境にある
	* プロキシとなるApacheは`80`、コンテナのビルドログを送受信するためのWebsockerサーバは`8080`を使います

### インストール

pool本体はDockerコンテナとなっています。
poolリポジトリ内には起動用のシェルスクリプト、`scripts/init_host_server`が含まれています。このシェルスクリプトを実行して、poolコンテナを起動します。

```sh
git clone https://github.com/mookjp/pool.git
cd pool

# `scripts/init_host_server`で、poolコンテナのビルドと起動を行います。
# init_host_server にはオプションで以下の3つの引数を与えられます。
#
# 1つ目には、プレビュー対象アプリケーションのリポジトリのURLを与えます。
# 例）https://github.com/mookjp/flaskapp.git
#
# 2つ目には、プレビュー対象アプリケーションのコンテナを最大何個作成するかを指定します。
# ここで指定した数以上コンテナが立ち上がっていた場合は、自動的に内部で古いコンテナからkillします。
# 例）10
#
# 3つ目には、このサーバのドメインネームを指定します。
# この引数で与えられた文字列は、アプリケーションのコンテナを起動する時にコミットIDと合わせて$POOL_HOSTNAMEという環境変数に設定されます。
# アプリケーションのコンテナ内でドメイン名を使う設定がある場合に、Dockerfile内で利用することができます。
# 例）example.com
#    ※ 起動するアプリケーションコンテナの$POOL_HOSTNAME環境変数には<コミットID>.example.comが入ります
./scripts/init_host_server ${プレビュー対象アプリケーションのリポジトリ} ${起動コンテナ最大数} ${ベースのドメイン}
```

### アクセス

おもむろにWebブラウザを起動し、`http://<コミットID or ブランチ名>.<ホスト名>/`へアクセスしてみてください。
マシンに`exmaple.com`という名前でアクセスできる場合は、URLは`http://<コミットID or ブランチ名>.example.com`となります。

## mod_mrubyを使ってみて

### やりたいことが簡単にできる

このツールを作る以前はApacheのフック？なにそれ？状態でしたが、やりたかったことが当初予想していたよりも簡単に実現できました。
C言語を書くことに慣れていない私のような人間には、mod_mrubyを使ってmrubyの簡潔な記述でフックを書けるというのは便利です。
もちろんApacheについての深い知識を身につけていくことは大切ですが（実際、作っている途中でApache本体のフック機能についても調べました）、導入としてmod_mrubyという選択肢はありではないでしょうか。

私はそもそもmruby以前にRubyすら書いたことがありませんでした。ですが、作者のブログやリポジトリのドキュメントやWikiにサンプルが複数あったので、それらを参考にして書いていったら思ったより簡単にやりたいことが実現できました。
Rubyの経験者ならもっと導入のハードルは低くなりそうです。

### 作者からPull Requestもらえた！

余談になってしまうのですが、このツールをGithubにおいておいたところ、mod_mrubyの作者である[matsumotory](http://qiita.com/matsumotory)さんからPull Requestをいただき、さらにはブログ記事（[人間とウェブの未来 - Gitのコミット単位で動的にDockerイメージをデプロイするプロキシサーバpool](http://blog.matsumoto-r.jp/?p=4541)）まで書いていただくという事件がありました。それまで、OSSを自分が利用することはあっても、自分の書いたコードに対してフィードバックをもらうなんて（しかも自分が使っているOSSの作者から！）想像したこともなかったので、すごくうれしかったです。

そういう点でも、mod_mrubyは私にとってすごく印象的なOSSとなっています。
こんなところで、という感じですが[matsumotory](http://qiita.com/matsumotory)さん、ありがとうございます！

## おわりに

ちょっと変わった使い方かもしれませんが、mod_mrubyの1つの利用例として見ていただけたらと思います。

次は、[matsumotory](http://qiita.com/matsumotory)さんの「mod_mrubyインストール後の初め方」です！！ワイワイ！！