ちょっと話題の記事

mod_proxy再入門 – ProxyPassとProxyPassReverse

2014.04.25

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。望月です。
思い返すこと数年前、私が始めてLinuxサーバ構築に関わってやったお仕事は、「Apache + Tomcatの構成を作る」でした。その時にApacheに以下のような設定をしました。

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

それ以降Apacheをフロントに置いて別のアプリケーションサーバーへProxyを行う機会は何度となくあり、その度に魔法のようにProxyPassとProxyPassReverseを書いてきましたが、自分で理解しきっていない設定をするのは止めよう、と思い立ったので、改めてmod_proxyについてお勉強してみました。今日はその勉強過程をまとめます。

ProxyPass

まず一行目のProxyPassです。上の設定の場合はhttp://server.example.com/appのリクエストをApacheが受け付けた場合にリクエストをhttp://backend.example.com:8000/へと転送します。

動作を検証するために、実際にProxyを立ててみました。以下の設定をApacheに行っています。

ProxyPass /app http://backend.example.com:8000/

検証のために簡単なアプリケーションを書きます。Sinatraがインストールされたマシンで、以下のコードを書いて下さい。

require 'sinatra'

# access
get '/index' do
  logger.info(env)
  "Application Response"
end

# redirect source
get '/redirect' do
  logger.info(env)
  redirect to('/dest')
end

# redirect destination
get '/dest' do
  logger.info(env)
  "Redirected"
end

リクエストのたびに、RackのHTTPに関する情報が詰まっているenvをログに出力しておきます。このアプリケーションを8000番ポートで待ち受けるように起動します。

$ ruby app.rb -p 8000

では、クライアントからアクセスしてみましょう。ブラウザでもいいのですが、今回はヘッダを中心に確認したいのでcurlコマンドで確認します。まずはクライアントから見た情報を確認してみます。

$ curl -i http://54.199.247.157/app/index
HTTP/1.1 200 OK
Date: Wed, 16 Apr 2014 10:20:30 GMT
Server: thin 1.6.2 codename Doc Brown
Content-Type: text/html;charset=utf-8
Content-Length: 20
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN

Application Response                                                                                           $

問題なくコンテンツが返ってきました。では、サーバの情報を見ておきましょう。

[ec2-user@ip-10-0-0-181 app]$ ruby app.rb -p 8000
== Sinatra/1.4.4 has taken the stage on 8000 for development with backup from Thin
Thin web server (v1.6.2 codename Doc Brown)
Maximum connections set to 1024
Listening on localhost:8000, CTRL+C to stop
I, [2014-04-16T10:24:56.927759 #10595]  INFO -- : HTTP_HOST : localhost:8000
I, [2014-04-16T10:24:56.927991 #10595]  INFO -- : REQUEST_PATH : /index
59.146.77.152 - - [16/Apr/2014 10:24:56] "GET /index HTTP/1.1" 200 20 0.0032

アプリケーションサーバから見ると、アクセスURLのホスト部分がlocalhost:8000となっており、リクエストパスは/app/indexではなく、/indexになっているのがわかります。アプリケーションサーバの前にいるApacheがリクエストを受け付けた時に、/app 以下のパスのみをアプリケーションサーバに転送しているのがわかると思います。

ProxyPassReverse

ProxyPassは比較的わかりやすいと思うのですが、問題はProxyPassReverseです。私はこれを記載する意味をいまいち理解できていませんでした。このディレクティブが効果を発信するのは、アプリケーションサーバがリダイレクトを発行した時です。
まずリダイレクトのしくみを整理しておきます。リダイレクトさせる際には、HTTPサーバが300番台のステータスコードと共にHTTPのLocationヘッダにリダイレクト先のURLを記載してクライアントに戻します。クライアントはHTTPレスポンスを受け取った際にHTTPステータスコードを読み、300番台だった場合はLocationヘッダにあるURLに対して再度リクエストを送信します。つまり、Locationヘッダには「クライアントからアクセスできるURL」を記述する必要があります。例えば127.0.0.1しかリッスンしていない場合やプライベートIPアドレスのアプリケーションサーバにはクライアントはアクセスができません。

では、Apacheの設定をProxyPassのみ記載した場合にリダイレクトが発生すると、どうなるのでしょうか。/redirectにアクセスした後、/destへとアクセスが発生するというのが想定される挙動です。curlにリダイレクトに追従するオプションである-Lオプションをつけて実施してみます。

$ curl -i -L http://server.example.com/app/redirect
HTTP/1.1 302 Moved Temporarily
Date: Wed, 16 Apr 2014 10:43:21 GMT
Server: thin 1.6.2 codename Doc Brown
Content-Type: text/html;charset=utf-8
Location: http://server.example.com/dest
Content-Length: 0
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN

HTTP/1.1 404 Not Found
Date: Wed, 16 Apr 2014 10:43:21 GMT
Server: Apache/2.4.9 (Amazon) PHP/5.5.10
Content-Length: 202
Content-Type: text/html; charset=iso-8859-1

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL /dest was not found on this server.</p>
</body></html>

まず、/redirectにアクセスした際に302リダイレクトが発生しています。そのレスポンスヘッダの中にLocationヘッダがあるのが確認できると思います。ですが、Locationヘッダの内容がhttp://server.example.com/destになっています。クライアントからのアクセスはhttp://server.example.com/app/destとならなければならないのですが...リダイレクトに従った結果、404の結果が帰ってきました。

アプリケーションサーバが、/appというURLを認識していないのが原因です。アプリケーションサーバApacheからアクセスされた時にはhttp://<サーバ名>/redirectのアクセスだと思っているので、それをそのまま信じてLocationヘッダにhttp://<サーバ名>/destというURLを返します。ApacheのProxyPassReverseはこのHTTPヘッダのLocationを確認し、ProxyPassReverseで設定した値に書き換えます。

公式ドキュメントには、ProxyPassReverseの説明として以下のように記載されています。

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

今回の例では、Apacheが/appというURLプレフィクスを付記することでリダイレクトが正常に働くようにしています。

ProxyPassReverseを設定して、先ほど失敗したリダイレクトを試してみましょう。

curl -i -L http://server.example.com/app/redirect
HTTP/1.1 302 Moved Temporarily
Date: Fri, 25 Apr 2014 01:12:48 GMT
Server: thin 1.6.2 codename Doc Brown
Content-Type: text/html;charset=utf-8
Location: http://server.example.com/app/dest
Content-Length: 0
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Connection: keep-alive

HTTP/1.1 200 OK
Date: Fri, 25 Apr 2014 01:12:49 GMT
Server: thin 1.6.2 codename Doc Brown
Content-Type: text/html;charset=utf-8
Content-Length: 10
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Connection: keep-alive

Redirected

Locationヘッダに/appが付記されており、リダイレクトに成功しているのが確認できました。

余談ですが、例えば以下の様な場合では、ProxyPassReverseは記載しなくても動作します。リダイレクト時にLocationヘッダを書き換える必要がないからですね。

ProxyPass / http://backend.example.com:8080/ # 全てのリクエストはバックエンドに飛ばす
ProxyPassReverse / http://backend.example.com:8080/ # ヘッダを書き換える必要が無いため、ProxyPassReverseは不要。

まとめ

mod_proxyの基本であるProxyPassとProxyPassReverseについて一度調べてみました。ProxyPassはアクセスをプロキシさせる時に利用され、ProxyPassReverseはヘッダ情報の書き換えに利用されます。
まだ自分で理解できていないことも多いのでもう少し調べてみようと思います。