mod_proxyのProxyPassReverseの意味がようやく理解できた

気がする!

  1. なぜProxyPassReverseにbalancer://~~ を設定できないのか *1
  2. なぜProxyPassReverseにajp://~~ を設定できないのか
  3. なぜbackendがhttpとajpの場合で、ProxyPassReverseに設定するURLが異なるのか

などなど。今まではmod_proxyする機会がほとんどなく適当にお茶を濁していたので、世間の人から相当遅れているとは思いますが、せっかくなので自分用まとめ。思いついたことをつらつらとメモっているからかなり冗長ですが。

追記 20100907

apache 2.2.12 から、balancer:// のURLにもProxyPassReverseが使えるようにmod_proxyがパワーアップしていました。

*) mod_proxy: Complete ProxyPassReverse to handle balancer URL's. Given;
BalancerMember balancer://alias http://example.com/foo
ProxyPassReverse /bash balancer://alias/bar
backend url http://example.com/foo/bar/that is now translated /bash/that
[William Rowe]

というわけで、記述もスッキリするのでapache 2.2.12はProxyPassReverse balancer://~~ で書いたほうが良いかもしれません。(試してないので何とも言えないけど)

さらに追記

実際に試してみたけど、ProxyPassReverse balancer://~~ がうまく動作するのは、BalancerMemberがhttpのときで、ajp:// だとLocationヘッダが調整できてなかった。3番の項目のとこでも書いたけど、httpとajpプロトコル利用時のレスポンスヘッダの違い関係してるんじゃないかと推測。

## 追記ここまで

自分なりに理解できて納得しているつもりですが、確認に使ったbackendサーバはapache + 簡単なcgi, tomcatのデフォルトのwebappsだけなので間違っているところもあるかもしれませんご指摘いただけると幸いです。

構成

確認で使ったのはvmware環境に作ったapache(reverse proxy)と、backendはapache or tomcat

                      reverse proxy                   backend  server
--------     http      -----------    http or ajp    -------------------
|client|    <======>   | apache  |     <======>      | apache or tomcat|
--------               -----------                   -------------------
  • clientがアクセスするhostはwww.example.com
  • reverse proxyであるapacheがアクセスするbackendのhostはbackend*.example.com
  • backendのapacheはのUseCannonicalName, UseCannonicalPhysicalPortがOffになっているとする

ProxyPassReverse

ProxyPassReverse ディレクティブ
説明: リバースプロキシされたサーバから送られた HTTP 応答ヘッダの URL を調整する
構文: ProxyPassReverse [path] url

apache公式の説明のように、ProxyPassReverseで設定するのはバックエンドサーバから送られてくるLocationヘッダ等のURLで、書き換えしたいURLを設定する。書き換えが行われるのは一致したときなので正しく書かなければいけない。
例えば下2行の設定。backend1のapacheがport80でlistenしていれば、URLとしては実質同じ意味だけど、ProxyPassReverseの設定としては別物(後述).

ProxyPass /app http://backend1.example.com:80/app
ProxyPass /app http://backend1.example.com/app
URLを書き換えしたいときとは

例えばこんなProxyPass設定のとき。

ProxyPass /app http://backend1.example.com:8080/app 

このときclientがhttp://www.example.com/app 以下にアクセスすると、reverse proxyのapacheはbackendのサーバにアクセスする。このとき、backendサーバ側でredirectが発生した場合にProxyPassReverseの出番。良くあるのはディレクトリにスラッシュなしでアクセスした場合。他にもアプリ側で特定URLにリダイレクトさせた場合など。

  1. clientがhttp://www.example.com/app/dir にアクセス
  2. リクエストを受け取ったapache(reverse proxy)が、backendのhttp://backend1.example.com:8080/app/dirにアクセス
  3. backendのapacheは301でLocationヘッダにスラッシュを付けたURL(http://backend1.example.com:8080/app/dir/)をreverse proxyのapacheに返す
  4. reverse proxyのapacheはbackendから受け取ったHTTPレスポンスをclientに返す

問題はこのときの3番のHTTPヘッダ(のLocationヘッダ)。reverse proxyであるapacheはbackendのサーバに対してhttp://backend1.example.com:8080/でアクセスしているので、backendから受け取るHTTPヘッダは以下のようなもの。

HTTP/1.1 302 Moved Temporarily
Date: Sat, 05 Dec 2009 03:08:13 GMT
Server: Apache
Location: http://backend1.example.com:8080/app/dir/
Transfer-Encoding: chunked
Content-Type: text/plain

このヘッダがclientに返ってしまうと、clientはLocationヘッダの値であるhttp://backend1.example.com:8080/app/dir/ に直接アクセスしようとしてしまう。

clientがbackendのサーバに直接アクセスするとなぜダメか

色々あるけど。

  • 仕組みがばれてかっこ悪い
  • reverse proxyで静的コンテンツを極力応答したいのに、直接backendのアプリケーションサーバにアクセスされたら負荷が高くなる
  • そもそもbackend1.example.comDNSで名前解決できない or backendがプライベートIPのLANにいる or Firewallで閉じられている等の場合はclientがbackendにアクセスできない
    • ex) /etc/hostsに 10.0.0.10 backend1 を設定
    • ProxyPass /app http://backend1:8080/app と設定している場合など

などの事情があるため、reverse proxyはbackendから上記のようなLocationヘッダが返ってきたときにLocationヘッダを調整する必要がある。これを実際に行うのがProxyPassReverse。調整したいURLとはまさにhttp://backend1.example.com:8080/app なので、apacheの設定は

ProxyPass        /app http://backend1.example.com:8080/app 
ProxyPassReverse /app http://backend1.example.com:8080/app 

こうなる。

なぜProxyPassReverseにbalancer://~~ を設定できないのか、の答え

ここで1番の答え。↑の例のようにProxyPassReverseに設定するのはbackendから返ってくるLocationヘッダ等のURLを調整するためのもの。

なので、ProxyPassとセットにしてProxyPassReverseにbalancer://~~ を書いても意味はない。backendのサーバからbalancer://~~ とかいうLocationヘッダが返ってくるはずはないのだから。

ProxyPass        /app balancer://cluster/app 
ProxyPassReverse /app balancer://cluster/app ## 間違い
ProxyPassReverseのURLはBalancerMember or ProxyPassに設定するURLとは必ずしも一致しない

これも微妙にはまった。実環境ではこんなチグハグな設定はないかもしれないけど。

2つのbackendのapacheがそれぞれportを80/8080でlistenしていたので、BalancerManagerに書くときにportを明示的に書いていた。

  • reverse proxyに設定したbalancer
<Proxy balancer://cluster >
  BalancerMember http://backend1.example.com:80  
  BalancerMember http://backend2.example.com:8080
</Proxy>

肝心のProxyする設定。ここを間違えた。てっきりBalancerMemberと同じ値で良いと思いこんで次の設定にしていたらbackend1にいったときは正常に動かない。

ProxyPass         /app balancer://cluster/app
ProxyPassReverse  /app http://backend1.example.com:80/app    # 1
ProxyPassReverse  /app http://backend2.example.com:8080/app

正解はこっち。

ProxyPass         /app balancer://cluster/app
ProxyPassReverse  /app http://backend1.example.com/app       # 2
ProxyPassReverse  /app http://backend2.example.com:8080/app

なぜか。
reverse proxyがbackend1のapacheにアクセスするときに、http://backend1.example.com:80/とport指定で
アクセスしたとしても、backendのサーバはport番号を付けずに、

Location: http://backend1.example.com/app

でHTTPヘッダを返してくる。ProxyPassReverseが書き換えるのは、このとき返ってくるヘッダのURLなので、#1の設定は間違い。apacheがdefaultの80番でlistenしている場合、URLの意味としては#1も#2も同じだけどProxyPassReverseの設定としては別。

backendがajpの場合

ajpプロトコルの仕様を知らないので経験を元にした推測ですが、ajpで渡す場合は、

ProxyPass /app ajp://localhost:8009/app

と書いたとしてもtomcatに渡されるhostヘッダはlocalhostではなくて、clientが元々アクセスしていたwww.example.comのままっぽい。なので、tomcat側でredirectが発生しても、tomcatからreverse proxyであるapacheに返ってくるLocationヘッダのhostはwww.exmaple.comのままだった。

同じURLの構成でbackendにproxyする場合

URLを同じ構成でbackendのajpに渡す場合はLocationヘッダの調整は必要ないのでProxyPassReverseもいらないぽい?

  1. clientがhttp://www.example.com/app/dir にアクセス
  2. リクエストを受け取ったreverse proxyが、backendのajp://backend1.example.com:8009/app/dirにアクセス
  3. backendのtomcatは301でLocationヘッダにスラッシュを付けたURLをreverse proxyに返す
  4. reverse proxyはbackendから受け取ったHTTPレスポンスをclientに返す

このときの3のHTTPヘッダは

HTTP/1.1 302 Moved Temporarily
Date: Sat, 05 Dec 2009 04:23:11 GMT
Location: http://www.example.com/app/dir/
Content-Type: text/plain

だったのでヘッダの調整は不要なのかしら。

ProxyPass          /app ajp://backend1.example.com:8009/app
#ProxyPassReverse   /app http://www.example.com/app
## これが不要
URLを変えて渡す場合

このときはProxyPassReverseは必要なはず。例えば /app => backendの/fooに渡す場合

ProxyPass /app  ajp://backend1.example.com:8009/foo
  1. clientがhttp://www.example.com/app/dir にアクセス
  2. リクエストを受け取ったreverse proxyが、backendのajp://backend1.example.com:8009/foo/dirにアクセス
  3. backendのtomcatは301でLocationヘッダにスラッシュを付けたURLをreverse proxyに返す
  4. reverse proxyはbackendから受け取ったHTTPレスポンスをclientに返す

このときの3のHTTPヘッダは

HTTP/1.1 302 Moved Temporarily
Date: Sat, 05 Dec 2009 04:26:25 GMT
Location: http://www.example.com/foo/dir/
Content-Type: text/plain

だった。clientがアクセスした際の頭の/appが/fooに変わってしまっている。httpと違ってFQDNはbackend1にはなっておらず、www.example.comのままなこともポイント。

tomcat側からすると、reverse proxyのapacheajp://backend1.example.com:8009/fooにアクセスするから、"/app"なんてものは全く関知しないところなので、たぶんこうなっている? このままだと、clientが間違ったURLにリダイレクトするので、

ProxyPass          /app  ajp://backend1.example.com:8009/foo
ProxyPassReverse   /app  http://www.example.com/foo

の設定を書いてあげないといけない。

この辺りまで理解できたきたので、冒頭の2番、3番の答えも出る。

なぜProxyPassReverseにajp://~~ を設定できないのか、の答え

何回も書いてるけどProxyPassReverseに書くのはbackendから返ってきたLocationヘッダ等を調整するための設定。client <=>(http) apache <=>(ajp) tomcatと通信しても、Locationヘッダはclientがそもそもアクセスしたhttp://www.exmaple.com/~~ になるのでProxyPassにajp://~~を設定しても意味がない。

なぜbackendがhttpとajpの場合で、ProxyPassReverseに設定するURLが異なるのか、の答え

backendがhttpの場合はProxyPassReverseに設定するのはhttp://backend1.example.com/。それに対して、ajpの場合は、clientがアクセスしたhttp://www.example.com/

で、これは確証がなくて予想なんだけど。ajpプロトコルの場合、Locationヘッダ等に使用されるFQDNapachetomcatajpで接続するときのhost名ではなく、元々clientがアクセスしたヘッダ情報が使われているからでないかと。


httpの場合は、reverse proxyがbackendに接続する場合、

  • client: reverse proxy
  • server: backend apache server

という関係になるので、(UseCannonicalNameの設定に依存するけど) reverse proxyのapacheはProxyPassに設定しているhost名でbackendに接続しにいくので、backendのapacheはLocationヘッダでhttp://backend1.example.com/~~ のURLを返すようになる。その違い。

おまけ

あと、ProxyPassReverseに書くURLはマッチした順に適用されるので、
(こんなproxyの仕方しないかもしれないけど)

ProxyPass         /baz balancer://cluster
ProxyPass         /foo balancer://cluster/foo

この場合は、/bazについてのProxyPassReverseを後に書く必要がある。

  • 間違い
ProxyPassReverse  /baz http://backend1.example.com:8080    
ProxyPassReverse  /foo http://backend1.example.com:8080/foo
  • 正解
ProxyPassReverse  /foo http://backend1.example.com:8080/foo
ProxyPassReverse  /baz http://backend1.example.com:8080             # こっちを後に

/bazの設定を先に書いてしまうと、/fooへのリクエストに対してのLocationヘッダも調整されてしまって、http://backend1.example.comの部分がhttp://www.example.com/bazに書き換えられる。結果としてhttp://www.example.com/baz/foo になってしまう。

設定まとめ

上記のことをまとめると、たぶんこんな感じになる。

backendがhttp

基本的にProxyPassと同じものを書けば良い。

  • 1台の場合
ProxyPass         /app http://backend1.example.com:8080/app 
ProxyPassReverse  /app http://backend1.example.com:8080/app
  • 複数台でbalancerしてるとき
<Proxy balancer://cluster/ >
  BalancerMember http://backend1.example.com:8080
  BalancerMember http://backend2.example.com:8080
</Proxy>

ProxyPass         /app blancer://cluster/app
ProxyPassReverse  /app http://backend1.example.com:8080/app
ProxyPassReverse  /app http://backend2.example.com:8080/app
backendがajp

URLを変換してproxyしている場合は設定が必要。URLをそっくりそのまま渡している場合はProxyPassReverseはいらない。とはいえ書いても問題はないと思うので、書いてしまってても良いかも。

  • 1台の場合: URLをそのまま渡している
ProxyPass         /app ajp://backend1.example.com:8009/app
ProxyPassReverse  /app http://www.example.com/app
## ↑いらない
  • 1台の場合: URLを変更している
ProxyPass         /app ajp://backend1.example.com:8009/foo/app
ProxyPassReverse  /app http://www.example.com/foo/app
  • 複数台でbalancer: URLをそのまま渡している
<Proxy balancer://cluster >
  BalancerMember ajp://backend1.example.com:8009
  BalancerMember ajp://backend2.example.com:8009
</Proxy>
ProxyPass         /app blancer://cluster/app
ProxyPassReverse  /app http://www.example.com/app
## ↑いらない
  • 複数台でbalancer: URLを変更している
<Proxy balancer://cluster >
  BalancerMember ajp://backend1.example.com:8009
  BalancerMember ajp://backend2.example.com:8009
</Proxy>
ProxyPass         /app blancer://cluster/foo/app
ProxyPassReverse  /app http://www.example.com/foo/app

追記

ProxyPassReverseCookiePathとCookieDomain

ProxyPassReverseはLocationヘッダを調整する。それと同様に、アプリによってはCookie(Set-Cookieヘッダ)のpath、domainも調整しないといけない。仕組みはProxyPassReverseと同じ。

clientがアクセスしているURLのpathとbackendのアプリが返すSet-Cookieのpathは必ずしも一致しない。置換する必要がある場合に、reverse proxyしているapacheに設定する。

ProxyPass        /foo http://backend.example.com/bar
ProxyPassReverse /foo http://backend.example.com/bar

clientが http://www.example.com/foo/hoge.cgiにアクセスするとreverse proxyはhttp://backend.example.com/bar/hoge.cgiにいく。このcgi

Set-Cookie: id=100; path=/bar

のようなcookieヘッダを返す場合、このままではpathが変わってcookieが動かないのでProxyPassReverseCookiePathを使う。

### 20140824 修正 コメントで指摘いただいたので修正しました!

ProxyPassReverseCookiePath  /bar /foo

↓間違ってこう書いてしまっていました。
ProxyPassReverseCookiePath /foo /bar


また、同様にSet-Cookie内のdomainも置換する場合は、ProxyPassReverseCookieDomainで。この設定は他と違って、reverse proxy先のdomainを先に書くので注意。

ProxyPassReverseCookieDomain backend.example.com www.example.com

*1:apache 2.2.12以前