@kyanny's blog

My thoughts, my life. Views/opinions are my own.

やっとわかった、リバースプロキシの設定の意味

いままでリバースプロキシの設定がよくわかっていなくて、すでに動いているサーバの設定を見よう見まねで使い回してきた。ちゃんと理解しようと思って、マニュアルを読み直したらやっとわかった。設定の方法 (How) がわかったこと以上に、なぜそう書く必要があるかという理由 (Why) を理解できたのが嬉しい。久しぶりに「わかった!」と叫びたくなった。感動を忘れないうちに、思い出せるように、書いておく。

mod_proxy - Apache HTTP サーバ バージョン 2.2 が Apache のプロキシ関連のマニュアル。 mod_proxy を使うことになる。

大事なディレクティブは、 ProxyPass と ProxyPassReverse のふたつ。

ProxyPass

これがリバースプロキシをする上でのほとんどすべてのことをやってくれる。実は見慣れた (コピペし慣れた) 設定ではこのディレクティブが書いてなくて、だから混乱のもとになっていたのだけどそれは後述。

mod_proxy - Apache HTTP サーバ バージョン 2.2

ローカルサーバのアドレスが http://example.com/ であると します。すると、

ProxyPass /mirror/foo/ http://backend.example.com/

と設定すると http://example.com/mirror/foo/bar への リクエストが内部的に http://backend.example.com/bar への プロキシリクエストに変換されることになります。

http://httpd.apache.org/docs/2.2/ja/mod/mod_proxy.html#proxypass

つまり、あるホストの 80 ポートにリバースプロキシサーバを、 8080 ポートにアプリケーションサーバ (mod_perl 組み込みの apache など) を立てた場合、以下のようになる。

ServerName example.com
ProxyPass / http://localhost:8080/

この場合、アプリケーションサーバ側では以下のような設定になっているはずだ。

<Location />
    SetHandler perl-script
    PerlHandler Hello::World
    PerlSetEnv PERL5LIB /path/to/hello/world/lib
</Location>

ProxyPassReverse

mod_proxy - Apache HTTP サーバ バージョン 2.2

このディレクティブは Apache に HTTP リダイレクト応答の Location, Content-Location, URI ヘッダの調整をさせます。これは、Apache がリバースプロキシとして使われている ときに、リバースプロキシを通さないでアクセスすることを防ぐために 重要です。これによりバックエンドサーバの HTTP リダイレクトが リバースプロキシとバックエンドの間で扱われるようになります。

(中略)

例えば、ローカルサーバのアドレスが http://example.com/ だとします。すると

ProxyPass /mirror/foo/ http://backend.example.com/
ProxyPassReverse /mirror/foo/ http://backend.example.com/
ProxyPassReverseCookieDomain backend.example.com public.example.com
ProxyPassReverseCookiePath / /mirror/foo/

という設定をすると、http://example.com/mirror/foo/bar へのローカルリクエストが http://backend.example.com/bar へのプロキシリクエストに内部でリダイレクトされるだけではありません (これは ProxyPass の機能です)。backend.example.com が送るリダイレクトの面倒もみます。http://backend.example.com/barhttp://backend.example.com/quux にリダイレクトされたとき、 Apache は HTTP リダイレクト応答をクライアントに送る前に、 http://example.com/mirror/foo/quux に変更します。 URL を構成するのに使われるホスト名は UseCanonicalName の設定に応じて選択されることに 注意してください。

ProxyPassReverse ディレクティブは 対応する ProxyPass ディレクティブには依存しないため、 mod_rewrite のプロキシ通過機能 (RewriteRule ... [P]) と併せて使用することができます。

mod_proxy - Apache HTTP サーバ バージョン 2.2

このディレクティブは、マニュアルに書いてある通り、バックエンドサーバがクライアントに対してリダイレクト (302 とか) を返すときに Location ヘッダなどをフロントエンドのサーバの URL に書き換えるために存在する。以下のような (先にあげたのと同じ) 設定があったとして、

ServerName example.com
ProxyPass / http://localhost:8080/

フロントエンドサーバに http://example.com/foo という URL でアクセスすると、バックエンドサーバは http://localhost:8080/foo という URL でアクセスされることになる。ここで /foo が /bar へ 302 Found でリダイレクトするレスポンスを返したとすると、上の設定例のままだとクライアントが受け取るレスポンスヘッダの中身はたぶん、以下のようになっている。

HTTP/1.1 302 Found
Date: Wed, 11 Feb 2009 10:02:39 GMT
Server: Apache/1.3.41 (Unix)
Location: http://localhost:8080/bar
Content-Type: text/html; charset=iso-8859-1

Location ヘッダにバックエンドサーバ自身からみた、リダイレクト先の URL が入っていて、フロントエンドサーバはレスポンスに一切手を触れないのでクライアントはそのままのヘッダを受け取ってしまう。このリダイレクションはおそらく失敗するし、まず間違いなく意図された結果にはならないはずだ。また、もしバックエンドのサーバが localhost ではなく app.example.com という ServerName を持っていて、外部から app.example.com:8080 へアクセス可能だったばあい、裏側の URL に対するアクセスを許してしまうとか (アクセス不可能にすべきだけど)、それでなくとも本来知られる必要のない内部的な URL が知られてしまうので、あまり好ましいことではないかもしれない。

そんなわけで、それを解決するのが ProxyPassReverse ディレクティブの役目である、というわけだ。設定をこう変更すればいい。

ServerName example.com
ProxyPass / http://localhost:8080/
ProxyPassReverse / http://localhost:8080/

これで、リダイレクトのレスポンスもおかしなことにならないはずだ。

mod_rewrite の [P] フラグとリバースプロキシ

ProxyPassReverse ディレクティブのマニュアルに、こう書き添えてある。

ProxyPassReverse ディレクティブは 対応する ProxyPass ディレクティブには依存しないため、 mod_rewrite のプロキシ通過機能 (RewriteRule ... [P]) と併せて使用することができます。

http://httpd.apache.org/docs/2.2/ja/mod/mod_proxy.html#proxypassreverse

これは、先ほどから出ている設定を、以下のように書き換えても構わない (同じように動く) ということだ。

ServerName example.com
RewriteEngine On
RewriteRule ^/(.*) http://localhost:8080/$1 [L,P]
ProxyPassReverse / http://localhost:8080/

mod_rewrite の RewriteRule を使って、 [P] (Proxy) フラグでプロキシリクエストをバックエンドサーバに送っている。 L フラグは「リライトはここで終了」という意味で、ここでは深く考える必要はない。おまじない。

最初に書いたように、いままでよくコピペしてお世話になっていた設定は上のようなもので、 ProxyPass ディレクティブがなかった。他の部分で RewriteRule を多用していたので、たまたま RewriteRule の [P] フラグを使ったほうが手軽だったからそういう設定になったのだと思う (書き手の真意は不明)。ただ、もし RewriteRule [P] と ProxyPass が併用できるものならば、 ProxyPass を使ったほうが (というか、不要であっても書いておいたほうが) 混乱が少ないので好ましいのではないか、と思う。マニュアルを読めば書いてあるわけだし、ある程度詳しければ設定を読んですぐ理解できるだろうけど、 ProxyPass と ProxyPassReverse という二種類のディレクティブがあるのに片方しか使われていなくて、しかも動いている、というのはぱっとみただけだととても奇妙だ。

というわけで、これで明日から、はてなダイアリーの記事が消えさえしなければ、リバースプロキシの設定で悩まなくて済むようになった。