複数のテストサーバをリバースプロキシで集約 (1)


各所でテストサーバが立ち上がっていて,これらは一台のリバースプロキシの背後にいます。普通に mod_proxy でやっているのですが,テストサーバを増やすたびに設定ファイルを書き換えて httpd を再起動,としなくちゃいけないのが煩わしい。

で,リバースプロキシのマッピング先を動的に構成したいなと思ったわけです。



以降では下記の例を使います。

アクセス URL リダイレクト先
http://outer1.example.com/path1/ http://inner-a.example.com/path1/
http://outer1.example.com/path2/ http://inner-b.example.com/path2/
http://outer2.example.com/path3/ http://inner-b.example.com/path3/
おことわり

今後のお話ででてくる設定を適用すると,mod_proxy を素で使った場合よりパフォーマンスがだいぶんと落ちます。なのでプロダクション環境にはおすすめできません。あくまでテストサーバ環境ということで。

素直に mod_proxy だけで書いてみる

まずは mod_proxy を使った設定を書いてみます。ProxyPassretry*1 を設定していないのはサンプルということでご容赦を。

LoadModule proxy_module      modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so

NameVirtualHost *

ProxyRequests Off
ProxyPreserveHost On

<VirtualHost *:*>
    ServerName outer1.example.com
    DocumentRoot /var/www/empty

    # http://outer1.example.com/path1/ => http://inner-a.example.com/path1/
    ProxyPass        /path1 http://inner-a.example.com/path1

    # http://outer1.example.com/path2/ => http://inner-b.example.com/path2/
    ProxyPass        /path2 http://inner-b.example.com/path2
</VirtualHost>

<VirtualHost *:*>
    ServerName outer2.example.com
    DocumentRoot /var/www/empty

    # http://outer1.example.com/path3/ => http://inner-b.example.com/path3/
    ProxyPass        /path3 http://inner-b.example.com/path3
</VirtualHost>

普通に書くとだいたいこんな感じになるのではないでしょうか。んで,ドメインが増えるたびに <VirtualHost> を追加し,マッピング先が増えるたびに ProxyPass を追加していく,ということになります。見通しは悪くないのですが,記述する分量はずいぶんと多いですね。

ヘッダの調整について

[http://httpd.apache.org/docs/2.2/mod/mod_proxy.html#proxypassreverse:title=ProxyPassReverse], [http://httpd.apache.org/docs/2.2/mod/mod_proxy.html#proxypassreversecookiedomain:title=ProxyPassReverseCookieDomain], [http://httpd.apache.org/docs/2.2/mod/mod_proxy.html#proxypassreversecookiepath:title=ProxyPassReverseCookiePath] あたりの話です。

なんでこんなのが用意されてるか。

さきほどの例で,[]inner-b.example.com[] にアクセスがあった状況について考えてみます。

GET /path2/hoge
Host: outer1.example.com

のようなリクエストにたいして /path2/foo にリダイレクトする場合。

Host ヘッダを考慮して

HTTP/1.1 303 See Other
Location: http://outer1.example.com/path2/foo

のようにレスポンスを返してくれればよいですが,

HTTP/1.1 303 See Other
Location: http://inner-b.example.com/path2/foo

のように自分のホスト名をドメイン名としてしまう「頑固」なウェブアプリケーションも存在します*2

こんなとき,mod_proxy 側の設定で

<VirtualHost *:*>
    ServerName outer1.example.com
    ProxyPassReverse /path2 http://inner-b.example.com/path2
</VirtualHost>

のようになっていると

HTTP/1.1 303 See Other
Location: http://outer1.example.com/path2/foo

mod_proxy がレスポンスヘッダを適宜書き換えてくれるというわけです。


とはいうものの,レスポンス HTML(のたとえば <a> タグとか)に含まれる (Fully Qualified) URL まで書き換えてくれるわけではありません。逆にいうと,出力される HTML の内容が Host ヘッダによってきちんと変わるウェブアプリケーションであれば,ヘッダも同様にきちんと書き換えてくれるはずでしょう。なので実はいちいち設定しなくてもうまく動くことがほとんどです。


ちなみに HTML の中身まで書き換えてくれる mod_proxy_html というモジュールもあります。普通はウェブアプリ(サーバ)側を書き換えるべきだと思いますが,既存のウェブアプリ環境を書き換えることなく外部に露出したいなどどうしてもということであれば,こういうのを使ってもいいかもしれません。

mod_rewrite とからめて使ってみる

[http://httpd.apache.org/docs/2.2/mod/mod_rewrite.html:title=mod_rewrite] を使うと,リクエスト(ヘッダ)の内容に応じて URI を書き換えたり Proxy 先を選定することが可能になります。これを使った設定は下記のようになります。

LoadModule proxy_module      modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule rewrite_module    modules/mod_rewrite.so

UseCanonicalName Off

ProxyRequests Off
ProxyPreserveHost On

RewriteEngine On

RewriteMap lowercase int:tolower

# http://outer1.example.com/path1/ => http://inner-a.example.com/path1/
RewriteCond ${lowercase:%{SERVER_NAME}} =outer1.example.com
RewriteCond %{REQUEST_URI}              /path1
RewriteRule ^/(.*)$  http://inner-a.example.com/$1 [proxy]

# http://outer1.example.com/path2/ => http://inner-b.example.com/path2/
RewriteCond ${lowercase:%{SERVER_NAME}} =outer1.example.com
RewriteCond %{REQUEST_URI}              /path2
RewriteRule ^/(.*)$  http://inner-b.example.com/$1 [proxy]

# http://outer2.example.com/path3/ => http://inner-b.example.com/path3/
RewriteCond ${lowercase:%{SERVER_NAME}} =outer2.example.com
RewriteRule ^/(.*)$  http://inner-b.example.com/$1 [proxy]

SERVER_NAME とのマッチングで「=〜」というのが見慣れないかもしれません。普通にセオリー通りに正規表現で書くなら,

RewriteCond ${lowercase:%{SERVER_NAME}} ^outer1\.example\.com$

となりますし,PCRE 表記を用いると

RewriteCond ${lowercase:%{SERVER_NAME}} ^\Qouter1.example.com\E$

のようになるでしょう。このような状況で使える全体 as-is マッチング記法「=」というのがあるということです。というかわたしも今回初めて知ったんですけどね。


また

RewriteMap lowercase int:tolower

というのも見慣れないかも。RewriteMap というのは RewriteCondRewriteRule で用いられる値にフィルタリングするためのものです。int:tolower というのは,mod_rewrite に内蔵された tolower というマッパで,このマッパを lowercase という名前で用いる,ということになります。

したがって,

RewriteCond ${lowercase:%{SERVER_NAME}} =outer1.example.com

の部分は,SERVER_NAMEint:lower というフィルタをかけて,outer1.example.com に全部一致したとき,という意味になります。

もちろんこのようなフィルタをかけずに

RewriteCond %{SERVER_NAME} =outer1.example.com

だけでもいいことはいいのですが,URL のドメイン名部分は大文字でやってくるかもしれないので,念のために tolower しています。このへんは実は Dynamically configured mass virtual hosting - Apache HTTP Server Version 2.2 を参考にしました。


ともあれ,これでマッピングルールが増えるたびに,RewriteCond を2〜3行,RewriteRule を1増やせばよい勘定になります。もとの mod_proxy を使う設定に比べると黒魔術的表記になっている気もしますが。

余談: ログについて

このように <VirtualHost> を使わずに複数ドメインを扱うと,異なるドメインへのリクエストログが同一のファイルに書かれてしまいます。同一のファイルにまとまってしまうことはまあいいんですが,ホスト名部分がロストするのでどのリクエストかわからなくなる弊害があります。

LogFormat "%h %l %u %t \"%m http://%{Host}i%U%q\" %s %b" vcommon
CustomLog logs/virtual.log vcommon

のような設定をしてホスト名をログに記載するようにするのがいいかもしれません*3。このような形式のログを既存のログ解析ソフトが受け付けてくれるかわかりませんが。


ここまでの設定では,テストサーバが増減するたびに設定ファイルを書き換えて httpd を再起動しなくてはいけないという状況はかわっていません。続きます。→複数のテストサーバをリバースプロキシで集約 (2) - daily dayflower

*1:ProxyPass には retry=?? を指定しておく - daily dayflower 参照

*2:一部のフレームワークでは設定ファイルでドメイン名を指定するものがあるようです。この場合,内部用に設定しているとリダイレクトの際に内部ドメイン名が露出してしまいます。

*3:SSL についてはまったく無視していますが。