最近愛用のMacbookが外観も中身もボロボロになってきて悲しいkyannyです。
ライブドアでは、画像やCSSファイルやjavascriptファイルなどの静的なコンテンツを配信するウェブサーバとウェブアプリケーション本体が稼働するアプリケーションサーバを別々のホストで運用する、いわゆるリバースプロキシ環境を構築しています。今回は、このリバースプロキシ環境の作り方を解説します。
リバースプロキシとは
リバースプロキシとはプロキシサーバの一種で、クライアントに対してファイアウォールの中にあり外部のネットワークから直接アクセスできないサーバへのアクセスを提供する機能です。これ以外にも、複数のバックエンドサーバを用意して負荷分散をしたり、プロキシサーバでキャッシュを併用することでバックエンドサーバへのリクエストを減らしたりと、様々なシーンで活用できるため、大規模なウェブサイトを構築する上で必須の技術となっています。
サーバ構成
まず、今回の説明にあたって例となるサーバ構成をご説明しましょう。
クライアント(一般的なウェブブラウザを想定)から直接リクエストを受け付けるウェブサーバと、ウェブサーバのバックエンドで稼働しているアプリケーションサーバの二台構成とします。ウェブサーバではApacheHTTPServerを起動し、Port 80をListenしているものとします。アプリケーションサーバはなんでも構いませんが、Port 8080をListenしているものとします。ちなみにライブドアでは自社製のオープンソースウェブアプリケーションフレームワークSledgeをmod_perl環境下で稼働させています。
なお、今回の例ではウェブサーバとアプリケーションサーバを同一ホスト(localhost)上で稼働させていますが、実際のサービス運用時には負荷分散やサーバリソースの有効活用を考えて、それぞれ別ホスト上で稼働させることが多いです。
ProxyPassとProxyPassReverse
Apaheでリバースプロキシを構築するためには、ProxyPassディレクティブとProxyPassReverseディレクティブを使います。
<VirtualHost *> ServerName www.example.com DocumentRoot /var/www ProxyPass / http://localhost:8080/ ProxyPassReverse / http://localhost:8080/ </VirtualHost>
ProxyPassディレクティブは、ローカル(ウェブサーバ)のパスをリモート(アプリケーションサーバ)のURLに対応させます。この例では、
http://www.example.com/
というURLへのリクエストは、そのままlocalhostの8080ポートをListenしているアプリケーションサーバにプロキシされます。アプリケーションサーバがリクエストを受けてレスポンスを返すと、ウェブサーバはそのレスポンスをそのままクライアントに返します。ProxyPassディレクティブに「/」を指定してあるので、それより下位のパスへのリクエストも同様にアプリケーションサーバへプロキシされます。
http://www.example.com/foo/bar
へのリクエストは、アプリケーションサーバからみると
http://localhost:8080/foo/bar
へのリクエストと同じです。
ProxyPassReverseディレクティブは、アプリケーションサーバのレスポンスがリダイレクトだった場合に効果を発揮します。
http://www.example.com/redirect_to_foo
というURLにアクセスすると、
http://www.example.com/foo
へステータス302でリダイレクトされるとしましょう。アプリケーションサーバは
Location: http://localhost:8080/foo
というレスポンスヘッダを返すわけです。しかし、クライアントはlocalhost:8080ではなくwww.example.comのコンテンツにアクセスしたいわけですから、このLocationヘッダは都合がよくありません。それを解決するのがProxyPassReverseディレクティブで、Locationヘッダの中に
http://localhost:8080/
が現れるとそれを「/」に置換してくれます。ProxyPassディレクティブと同様に、「/」以下のパスは保持されますので、アプリケーションサーバからの
Location: http://localhost:8080/foo
というレスポンスは、ウェブサーバによって
Location: http://www.example.com/foo
へと書き換えられ、最終的にクライアントは無事に
http://example.com/foo
というURLへたどり着けるというわけです。
リバースプロキシとmod_rewrite
URLを自由自在に書き換えることができるmod_rewriteは、ウェブアプリケーションの構築において必須ともいえるテクニックの一つです。mod_rewriteの提供するRewriteRuleディレクティブは、より柔軟なProxyPassディレクティブとしても利用できます。
<VirtualHost *> ServerName www.example.com DocumentRoot /var/www RewriteEngine On RewriteRule ^/(.*\.(jpg|gif|png|css|js))$ /var/www/static/$1 [L] RewriteRule ^/(.*) http://localhost:8080/$1 [P,L,QSA] ProxyPassReverse / http://localhost:8080/ </VirtualHost>
mod_rewriteは非常に奥が深く、その柔軟性と自由度の高さは「黒魔術」と揶揄されるほどですので、ここではmod_rewriteの詳しい解説には踏み込まず、必要最低限の説明にとどめることにします。
RewriteRuleディレクティブは、第一引数に正規表現によるパターンをとります。URL がパターンにマッチすると、URLを第二引数の文字列に書き換えます。第三引数にフラグが指定されていると、フラグの値に応じた動作をとります(リダイレクトやプロキシリクエストなど)。第一引数のパターン内ではマッチした部分文字列を括弧で囲うことでキャプチャできます。キャプチャされた文字列は第二引数の中で$1,$2のような変数を通して参照できます。正規表現をご存知の方にはお馴染みですね。
それでは、上の例の解説に移りましょう。RewriteEngine Onでmod_rewriteの機能を有効にします。その次のRewriteRuleは、URLが画像、CSSファイルもしくはjavascript ファイルらしきパターンにマッチする場合に実行されます。それらのパターンにマッチした場合、$1にはマッチしたURLのパス部分がキャプチャされます。第二引数は/var/www/static/$1となっているので、このRewriteRuleは
http://www.example.com/profile.jpg
というURLを
http://www.example.com/static/profile.jpg
へと書き換えます。[L]というフラグはLastの略で、それ以上URLの書き換えを行わないという意味です。 [L]フラグを指定しない限りmod_rewriteはURLの書き換えを行ったあとも引き続き次のRewriteRuleディレクティブが現れるたびにパターンマッチとURLの書き換えを行いますので、[L]フラグをつけわすれると意図せず余分なURLの書き換えがされてしまいます。mod_rewriteでありがちな失敗として、フラグをつけわすれてURLが書き換えられすぎてしまいハマることが挙げられますので、注意してください。
その次の行のRewriteRuleのパターンは^/(.*)となっています。これはすべてのURLにマッチするパターンです。
http://www.example.com/foo
というURLは、このパターンにマッチングして
http://localhost:8080/foo
へと書き換えられます。前述の
http://www.example.com/profile.jpg
というURLとは、URL書き換えを行ったRewriteRuleディレクティブが違うことに注目してください。RewriteRuleディレクティブは、上から順番に評価されていきますので、書く順番がとても重要です。mod_rewriteでありがちな失敗として、先頭に^/(.*)のようなパターンを書いてしまって以降のパターンに一切マッチングしなくなりハマることが挙げられますので、注意してください。
[P,L,QSA]は前述の通りRewriteRuleディレクティブの動作を指定するフラグです。 [P]はProxyの略で、第二引数の値で書き換えられたURLにプロキシリクエストします。この[P]フラグは、先ほど説明したProxyPassディレクティブと同様の動作をしますので、[P]フラグつきのRewriteRuleディレクティブを利用すると、ProxyPassディレクティブを利用せずにリバースプロキシ環境を構築できます。[QSA]はQueryStringAppendの略で、マッチング前のURLにクエリストリングがついていた場合、書き換え後のURLにも同じクエリストリングをつけます。mod_rewriteでありがちな失敗として、[QSA]フラグをつけ忘れてアプリケーションサーバ側でクエリストリングの値を取得できなくなりハマることが挙げられますので、注意してください。
RewriteRuleディレクティブの[P]フラグをつかえばProxyPassディレクティブを利用する必要はありませんが、ProxyPassReverseディレクティブは依然として必要です。
RewriteRule ^/(.*) http://localhost:8080/$1 [P,L,QSA]
はProxyPassディレクティブの説明時に挙げた例の
ProxyPass / http://localhost:8080/
と同様ですので、ProxyPassReverseディレクティブも
ProxyPassReverse / http://localhost:8080/
と書きます。 mod_rewrite(のせいではありませんが)でありがちな失敗として、ProxyPassディレクティブとProxyPassReverseディレクティブの違いがわからず「ProxyPassってのを消しても動くならProxyPassReverseってのも消していいんじゃね」と軽い気持ちでProxyPassReverseディレクティブを削ってしまいリダイレクトが正しく動作しなくなってハマることが挙げられますので、注意してください。
さて、RewriteRuleディレクティブでわざわざ画像やCSSファイルやjavascriptファイルの場合だけ、アプリケーションサーバへプロキシさせずに/var/www/staticへとURL を書き換えているのは何故でしょうか?これには大規模なウェブサイトならではの理由があります。サーバ構成の説明をする際に、アプリケーションサーバはSledgeをmod_perl環境下で稼働させていると書きましたが、mod_perlは一プロセスが数十MBほどのメモリを必要とし、そのためプロセス数をあまり多くできません。限りあるアプリケーションサーバの貴重なプロセスを、アプリケーションの実行ではなく静的なファイルへのリクエストに対する応答で消費してしまうのはもったいないことです。静的なファイルの配信はウェブサーバに任せておいて、アプリケーションサーバは動的なコンテンツの生成に注力します。この役割分担をさせるために、mod_rewriteを使って柔軟なリバースプロキシの設定を行うのです。 なお、文中にいくつか「~でハマるので注意せよ」という旨を書き添えましたが、これらの事例はすべて著者が実際にハマって何時間も脂汗を流したものばかりです。mod_rewriteがらみの失敗はちょっとした設定ミスによるものがほとんどであり、冷静に設定を見直せばすぐに解決することが多いのですが、本番運用中のサービスで不具合がでるととても冷静ではいられず泥沼にハマってしまい心臓に悪いので、設定を変更したときは検証用の環境で十分すぎるほどにチェックするようにしましょう。mod_rewriteの設定のデバッグにはRewriteLogディレクティブとRewriteLogLevelディレクティブを利用するのが常道です。出力結果はお世辞にも読みやすくはなく、RewriteLogLevelの値を大きくするとすさまじい勢いでデバッグメッセージが出力されるのでデバッグには苦痛が伴いますが、これくらいしかデバッグする方法がないので我慢するしかありません。grep(1)などを駆使して頑張りましょう。
以上で、mod_rewriteを利用したリバースプロキシ環境の作り方についての説明を終わります。なお、上記の設定例を実際に試してみたい方は、githubからサンプルをダウンロードできますのでご利用ください。
参考文献
mod_proxy
ProxyPass
ProxyPassReverse
mod_rewrite
RewriteRule