Dockerとmrubyで迅速かつ容易にnginxとapacheの柔軟なリバースプロキシ構成を構築する

Docker Hubがアナウンスされて以来、焦ってDockerを触っている@matsumotoryです。

今日は早速mod_mrubyngx_mrubyをdocker buildに対応させました。これによって、Docker環境においてmod_mrubyを組み込んだApache httpdやngx_mrubyを組み込んだnginxを迅速かつ容易に連携させる事ができるようになります。

今日はその一例を紹介したいと思います。

リバースプロキシのnginxの挙動をmrubyで制御する

ngx_mrubyのGitHubレポジトリにはすでにDockerに対応させています。ですので、ngx_mrubyをcloneするとDockerfiledocker/ディレクトリがあり、docker buildするとすぐにngx_mrubyを組み込んだnginxをDockerで起動させることができます。

docker buildする前に、ngx_mrubyディレクトリの中のdocker/conf/nginx.confにnginxの設定、docker/hook/proxy.rbにリバースプロキシの制御をRubyで書いておくことで、起動後のnginxの振る舞いを定義する事ができます。例えば以下のような制御を行いたいとします。

  • nginxの/mruby-proxyにアクセスすると自分自身の/mruby-helloかバックエンドのWebサーバが動いているDockerイメージの/mruby-helloにリバースプロキシする
  • バックエンドはランダムに選択する
  • nginxの設定を変える事なくバックエンドのDockerイメージを1つから3つまでスケールさせることができる

上記の3つの動作を達成するためのngx_mrubyの記述は以下のようになります。

ngx_mruby/docker/conf/nginx.conf

[program lang=’apache’ escaped=’true’]

daemon off;
user daemon;
worker_processes auto;

events {
    worker_connections  1024;
}

# Docker間リンク時の環境変数をnginx内でも引き継ぐ
env PROXY1_PORT_80_TCP_ADDR;
env PROXY1_PORT_80_TCP_PORT;
env PROXY2_PORT_80_TCP_ADDR;
env PROXY2_PORT_80_TCP_PORT;
env PROXY3_PORT_80_TCP_ADDR;
env PROXY3_PORT_80_TCP_PORT;

http {
    include mime.types;
    default_type application/octet-stream;

    sendfile on;
    keepalive_timeout 65;

    server {
        listen 80;
        server_name localhost;

        # ngx_mrubyによりレスポンス処理をインラインコードでnginx.conf内に直接実装する
        location /mruby-hello {
            mruby_content_handler_code 'Nginx.echo "server ip: #{Nginx::Connection.new.local_ip}: hello ngx_mruby world."';
        }

        # リバースプロキシの処理をngx_mrubyを使ってRubyスクリプトで実装する
        location /mruby-proxy {
            mruby_set $backend /usr/local/nginx/hook/proxy.rb;
            proxy_pass http://$backend;
        }
    }
}

[/program]

Dockerでは、コンテナ間のネットワークリンク処理の際に、コンテナ内にリンク先の情報を環境変数として持ちます。その情報をngx_mrubyのRubyスクリプト内でも扱うために、まずはRubyスクリプトを実行するnginxがその環境変数をコンテナから引き継げるようにenv設定をnginx.confに書いておきます。

/mruby-helloのlocation設定では、単にRubyコードを直接インラインで実行するように設定を書き、内容は自身のサーバIPとhello world文字列をレスポンスとして出力するようにします。

/mruby-proxyのlocation設定では、ngx_mrubyによりproxy.rbスクリプトを実行して、その結果を$backend変数に入れるようにします。そのproxy.rbは以下になります。

ngx_mruby/docker/hook/proxy.rb

[program lang=’ruby’ escaped=’true’]

backends = [
  "127.0.0.1:80",
]

# proxy1というalias名のDockerイメージがリンクされていたらbackendsに追加
unless ENV["PROXY1_PORT_80_TCP_ADDR"].nil?
  backends << ENV["PROXY1_PORT_80_TCP_ADDR"] + ":" + ENV["PROXY1_PORT_80_TCP_PORT"]
end

# proxy2というalias名のDockerイメージがリンクされていたらbackendsに追加
unless ENV["PROXY2_PORT_80_TCP_ADDR"].nil?
  backends << ENV["PROXY2_PORT_80_TCP_ADDR"] + ":" + ENV["PROXY2_PORT_80_TCP_PORT"]
end

# proxy3というalias名のDockerイメージがリンクされていたらbackendsに追加
unless ENV["PROXY3_PORT_80_TCP_ADDR"].nil?
  backends << ENV["PROXY3_PORT_80_TCP_ADDR"] + ":" + ENV["PROXY3_PORT_80_TCP_PORT"]
end

uri = '/mruby-hello'

backends[rand(backends.length)] + uri

[/program]

このようにnginxのプロキシ機能をngx_mrubyで実装することで、柔軟なリバースプロキシ構成をRubyで比較的容易に記述することができます。

今回はシンプルな例にしていますが、nginxの設定で書きにくいようなより複雑な条件になってきた場合、Rubyの制御構造や記述力の高さの恩恵をさらに受けられると考えています。

バックエンドのApache httpdの挙動をmrubyで制御する

続いてバックエンドのDockerイメージについて言及します。今回はせっかくなのでバックエンドにmod_mrubyを組み込んだApache httpdを使用したいと思います。

また、ngx_mruby同様、mod_mrubyも既にdocker buildに対応していますので、mod_mrubyをcloneするとDockerfileやdocker/ディレクトリが存在します。これもngx_mrubyと同様にdocker buildする前に、docker/conf/mruby.confにmod_mruby用のApache httpdの設定、docker/hook/以下にRubyスクリプトを書くことでApache httpdをRubyで自由に制御することができます。

今回は、/mruby-helloにアクセスがあると何かしらレスポンスを返せば良いので、以下のように設定を書きます。

mod_mruby/docker/conf/mruby.conf

[program lang=’apache’ escaped=’true’]

<IfModule mod_mruby.c>

  # mod_mrubyでレスポンス処理をインラインコードとして実装する

  <Location /mruby-hello>

    mrubyHandlerMiddleCode ‘Apache.echo “server ip: #{Apache::Connection.new.local_ip}: hello mod_mruby world.”‘

  </Location>

</IfModule>

[/program]

これで、mod_mrubyの機能により/mruby-helloにアクセスがあると自身のサーバIPと共にhello worldメッセージをレスポンスで返します。

また、今回はmod_mrubyを組み込んだApache httpdをバックエンドにしましたが、ngx_mrubyのようなリバースプロキシをmod_mrubyで実装することも可能です。それらに関してはngx_mrubyのWikimod_mrubyのWikiを見て頂くと良いと思います。

Dockerイメージを作成して起動させる

上記のように設定やRubyによる制御を書き終えたら、それぞれのDockerイメージを作ります。今回だとngx_mrubyの場合は、ngx_mrubyディレクトリのDockerfileがある場所で、

[program lang=’bash’ escaped=’true’]

docker build -t mybuild/ngx_mruby .

[/program]

mod_mrubyの場合は、mod_mrubyディレクトリのDockerfileがある階層で、

[program lang=’bash’ escaped=’true’]

docker build -t mybuild/mod_mruby .

[/program]

等としてDockerイメージを作ります。一回目は少し時間がかかりますが、一度作った後はbuildには差分が使われますので、さらにdocker/conf以下やdocker/hook以下のファイルを改修した後の再buildはすぐに終わります。

build後は、バックエンドに必要な数(今回は3つまでスケール可能)のDockerイメージを起動させます。今回のバックエンドはmod_mrubyで制御されたApache httpdなので、上記コマンドで作ったmod_mrubyのDockerイメージを起動させましょう。

[program lang=’bash’ escaped=’true’]

docker run -d mybuild/mod_mruby

[/program]

上記コマンドを3回実行すれば、バックエンドが3個起動します。ngx_mrubyが動的にバックエンドの存在を検出するので、1つ起動でも2つ起動でも問題ありません。

さらに、フロントで構えるngx_mrubyでリバースプロキシを実装したnginxのDockerイメージを、3つのバックエンドとネットワーク的に繋がった状態で起動させます。

また、外部からアクセスできるように、ホストの10080ポートからDockerのnginxのポート80にフォワードするようにします。まずは、既に起動しているバックエンドイメージのNameをdocker psコマンドの一番右側に出力されるカラムから取得します。

[program lang=’bash’ escaped=’true’]

$ docker ps

CONTAINER ID        IMAGE                      COMMAND                CREATED             STATUS              PORTS               NAMES

a9763e437ade        mybuild/mod_mruby:latest   /usr/sbin/apache2 -D   2 minutes ago       Up 2 minutes        80/tcp              clever_euclid

d130d4eb3616        mybuild/mod_mruby:latest   /usr/sbin/apache2 -D   2 minutes ago       Up 2 minutes        80/tcp              elegant_perlman

6c29e1c95678        mybuild/mod_mruby:latest   /usr/sbin/apache2 -D   2 minutes ago       Up 2 minutes        80/tcp              clever_franklin

[/program]

それぞれの起動したDockerイメージのNameは、clever_euclid、elegant_perlman、clever_franklinであることがわかりますね。起動時にNameを予め命名してやっても良いですが、それは後述の例で用いるので今回は命名しません。

そして、これらのNameに対して、それぞれproxy1からproxy3までDockerでいうaliasを指定して、フロントのDockerイメージであるnginxを起動させます。

[program lang=’bash’ escaped=’true’]

docker run -d -p 10080:80 –link clever_euclid:proxy1 –link elegant_perlman:proxy2 –link clever_franklin:proxy3 mybuild/ngx_mruby

[/program]

これでnginxが起動しました。実際にcurlでアクセスすると、

[program lang=’bash’ escaped=’true’]

$ curl http://127.0.0.1:10080/mruby-proxy

server ip: 127.0.0.1: hello ngx_mruby world.

$ curl http://127.0.0.1:10080/mruby-proxy

server ip: 10.1.0.136: hello mod_mruby world.

$ curl http://127.0.0.1:10080/mruby-proxy

server ip: 10.1.0.135: hello mod_mruby world.

$ curl http://127.0.0.1:10080/mruby-proxy

server ip: 127.0.0.1: hello ngx_mruby world.

$ curl http://127.0.0.1:10080/mruby-proxy

server ip: 10.1.0.135: hello mod_mruby world.

$ curl http://127.0.0.1:10080/mruby-proxy

server ip: 10.1.0.135: hello mod_mruby world.

$ curl http://127.0.0.1:10080/mruby-proxy

server ip: 10.1.0.134: hello mod_mruby world.

$ curl http://127.0.0.1:10080/mruby-proxy

server ip: 127.0.0.1: hello ngx_mruby world.

[/program]

このように、nginxの設定を変える事なく稼働させたバックエンドの数を動的に検出して、ランダムでバックエンドにリバースプロキシができている事が確認できます。

いったいどこが迅速かつ容易なのか?

ここまで読んで頂いて、非常に複雑で面倒な印象を感じられたと思います。しかし、ここからがDockerとmod_mruby及びngx_mrubyによるプログラマブルなWebサーバの制御機構の組み合わせの真骨頂になります。

何が言いたいかというと、ここまでの開発が終わってしまえば、そのDockerイメージを予め作っておけば良いということです。

実際、上記の処理はmod_mrubyngx_mrubyのデフォルトのDockerイメージの挙動になりますので、これらは既にDocker HubでGitHubと連携して自動的にイメージが作成・公開されており、実際に使う時はDocker環境がある場所はどこでも、

[program lang=’bash’ escaped=’true’]

docker pull matsumotory/mod-mruby
docker pull matsumotory/ngx-mruby
docker run -d --name name1 matsumotory/mod-mruby
docker run -d --name name2 matsumotory/mod-mruby
docker run -d --name name3 matsumotory/mod-mruby
docker run -d -p 10080:80 --link name1:proxy1 --link name2:proxy2 --link name3:proxy3 matsumotory/ngx-mruby

curl http://127.0.0.1:10080/mruby-proxy

[/program]

とコマンドを実行してやれば、Dockerとmrubyによる動的な制御のおかげで上記のような柔軟なリバースプロキシ構成をすぐに構築・稼働できるのです。差分が存在するような環境ではデプロイも非常に迅速かつ容易に行えるでしょう。

このような構成であっても、Docker環境がある場所であれば「どこでも」構築・稼働することができます。

まとめ

以上のように、一旦mod_mrubyngx_mrubyレポジトリにあるDockerfileとdocker/ディレクトリ以下のファイルを、構築したいリバースプロキシ構成の設定をRubyでプログラマブルに書いてしまえば、後はDockerイメージにすることであっというまにDocker環境がある所はどこでも、迅速かつ容易にnginxとApache httpdの柔軟なリバースプロキシ構成が構築できるようになりました。

それが言いたいがために、細かい実装について説明を行いましたが、実際は上記の「いったいどこが迅速かつ容易なのか?」の章のコマンドだけで上記構成を構築することができます。

今回は非常にシンプルな実装にしているので、まだまだmod_mrubyngx_mrubyとDockerの組み合わせによる旨味が出ていないようにも見えますが、Rubyを使える人はより複雑で動的なバックエンドの検出が必要なリバースプロキシ構成であっても、柔軟にRubyで記述し工夫することで、Dockerでデプロイすることが可能になると思います。

是非皆さんもmod_mrubyngx_mrubyのDockerイメージを参考に、自身の実装をmod_mrubyngx_mrubyのDockerfileやdocker/ディレクトリ以下の設定に実装して、自分だけのDockerイメージをGitHubで公開した上で(基本的にはDockerfileと上記で言うdocker/ディレクトリのファイルだけ公開すれば良い)Docker Hubで自動でイメージ化し、迅速にDocker環境へとリバースプロキシ構成のような仕組みをデプロイしてみては如何でしょうか。

一旦作ってしまえばその利便性にハマる事間違いなしです。