HAProxyでHTTPヘッダーを操作する。(レスポンス編)
前回の続き(id:shimula:20101020)で、今回はレスポンスをHAProxyで操作してみます。
rspadd
HTTPレスポンスヘッダーを追加します。
設定例:"X-Resp-Greeding"ヘッダーを追加します。
listen web1 bind :8080 mode http server web1 192.0.2.1:80 check rise 1 fall 3 rspadd X-Resp-Greeding:\ Hello
http://192.0.2.1:8080/にアクセスした時のレスポンスヘッダーは、以下の結果となりました。最後に"X-Resp-Greeding"ヘッダーが追加されていることを確認できました。
HTTP/1.1 304 Not Modified Date: Sat, 13 Nov 2010 11:39:44 GMT Server: Apache/2.2.3 (CentOS) Connection: close Etag: "102a3-d-48da19b900280" X-Resp-Greeding: Hello
rspdel, rspidel
指定した正規表現にマッチするHTTPレスポンスヘッダーを削除します。
rspidelは大文字小文字を無視します。(ignore case)
設定例:"Server"ヘッダーを削除します。
listen web1 bind :8080 mode http server web1 192.0.2.1:80 check rise 1 fall 3 rspidel ^server:
http://192.0.2.1:8080/にアクセスした時のレスポンスヘッダーは、以下の結果となりました。"Server"ヘッダーがないことが確認できました。
HTTP/1.1 304 Not Modified Date: Sat, 13 Nov 2010 12:12:00 GMT Connection: close Etag: "102a3-d-48da19b900280"
rspdeny, rspideny
ステータスライン、レスポンスヘッダーに指定した正規表現がマッチする場合、レスポンスをブロックします。(ステータスコード502を返却します)
ドキュメントでは機密情報流出防止に使うのが目的だそうです。
設定例:excelファイルはダウンロードさせない。
listen web1 bind :8080 mode http server web1 192.0.2.1:80 check rise 1 fall 3 rspideny ^Content-Type:\ .*ms-excel
http://192.0.2.1:8080/test.xlsにアクセスした時に以下のメッセージが表示されました。
502 Bad Gateway The server returned an invalid or incomplete response.
このサンプル設定だとレスポンスヘッダーに"Content-Type"が正しく設定されてる場合にのみに有効なので、ステータスコード304を返却した場合は当然ながらダウンロードできてしまいます。完全に制御するのであれば組み合わせで工夫しないといけないのかもしれません。
rsprep, rspirep
ステータスライン、レスポンスヘッダー内で指定した文字列がマッチする部分を置換します。
rspirep は大文字小文字を無視します。
ドキュメントではLocationヘッダーの書き換えをするのが主な用途と書いてあります。
設定例:リダイレクト先をすべて"http://example.com/"に設定する。
listen web1 bind :8080 mode http server web1 192.0.2.1:80 check rise 1 fall 3 rsprep ^Location:\ .* Location:\ http://example.com/
リダイレクトをさせるようなURLを実行すると"http://example.com/"にリダイレクトします。
HAProxyでHTTPヘッダーを操作する。(リクエスト編)
今回はHAProxyでHTTPヘッダーを操作してみます。主にHTTPリクエストヘッダーの方です。
reqadd
HTTPリクエストのヘッダーを追加します。
設定例:"X-Greeding"ヘッダーを追加します。
backend web1 mode http server web1 172.16.1.129:80 check rise 1 fall 3 option httpchk reqadd X-Greeding:\ Hello\ World
reqaddキーワードでHTTPリクエストヘッダーを追加することができます。
バックスラッシュはその次の文字をエスケープします。バックスラッシュがないとHAPoxyの構文エラーになってしまいます。
以下は、http://172.16.1.129:8080/test.php へブラウザでアクセスした表示結果です。※test.phpは、すべてのヘッダーを出力するプログラムです。
== HTTP headers == Host: 172.16.1.129:8080 User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; ja-JP-mac; rv:1.9.2.8) Gecko/20100722 Firefox/3.6.8 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: ja,en-us;q=0.7,en;q=0.3 Accept-Encoding: gzip,deflate Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7 Keep-Alive: 115 Connection: keep-alive Cache-Control: max-age=0 X-Greeding: Hello World
ちゃんとX-Greedingが追加されていました。
reqdel, reqidel
指定した正規表現にマッチするHTTPリクエストヘッダーを削除します。
reqidelは大文字小文字を無視します。(ignore case)
設定例:ヘッダーUser-Agentを削除します。
backend web1 mode http server web1 172.16.1.129:80 check rise 1 fall 3 option httpchk reqidel ^User-Agent:.*
以下は、http://172.16.1.129:8080/test.php へブラウザでアクセスした表示結果です。
== HTTP headers == Host: 172.16.1.129:8080 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: ja,en-us;q=0.7,en;q=0.3 Accept-Encoding: gzip,deflate Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7 Keep-Alive: 115 Connection: keep-alive Cache-Control: max-age=0
ちゃんとUser-Agentが削除されています。
reqallow, reqiallow, reqdeny, reqideny
指定した文字列がリクエストラインとヘッダーにマッチするものをアクセス許可(reqallow)、不許可(reqdeny)にします。
reqiallow, reqidenyは大文字小文字を無視します。
設定例:publicディレクトリ以下に対してはアクセスを許可する。secretディレクトリ以下に対してはアクセスを許可しない。
backend web1 mode http server web1 172.16.1.129:80 check rise 1 fall 3 option httpchk reqdeny .*/secret/.* reqallow .*/public/.*
secretディレクトリへアクセスしようとすると、以下のメッセージがブラウザに表示されました。
403 Forbidden Request forbidden by administrative rules.
reqrep, reqirep
HTTPヘッダー、リクエストライン内で指定した文字列がマッチする部分を置換します。
reqirep は大文字小文字を無視します。
設定例:User-Agentを"foobar"に変更する。
backend web1 mode http server web1 172.16.1.129:80 check rise 1 fall 3 option httpchk reqrep ^(User-Agent:)(.*) \1foobar
Firefoxでアクセスしてみると、以下のヘッダーが出力されました。
== HTTP headers == Host: 172.16.1.129:8080 User-Agent: foobar Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: ja,en-us;q=0.7,en;q=0.3 Accept-Encoding: gzip,deflate Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7 Keep-Alive: 115 Connection: keep-alive Cache-Control: max-age=0
User-Agentが変更されていました。
reqtarpit, reqitarpit
HTTPヘッダー、リクエストライン内で指定した文字列がマッチする場合ターピットによるスパム対策を施せます。
reqitarpit は大文字小文字を無視します。
設定例:特定のIP(172.16.1.1)はターピットで動作を遅らせる。
backend web1 mode http server web1 172.16.1.129:80 check rise 1 fall 3 option httpchk acl robot src 172.16.1.1 reqtarpit . if robot
172.16.1.1のクライアントでアクセスすると5秒待たされた後に、500エラーが返却されました。
上の例でACLと絡めましたが、その他の例でもACLと一緒に使用することができます。
その他に、reqpass,reqipass というのがあるんだけど、思ったように動作してくれん・・・。
HAProxyのACLとCriteria
HAProxyのACLについて仕事で使う機会があったので、いくつか調べたものを復習としてメモします。(HAProxyはかなり設定可能な項目が多いので、主にCriteriaです。)
※バージョンは、1.4.8で確認しました。
nbsrv
現在稼働中のbackendのサーバー数を返します。
設定例:backend のサーバーの生存台数が2台であれば、それらに振り分ける。
frontend db_front bind :3306 acl is_db_backend nbsrv(db_backend) eq 2 use_backend db_backend if is_db_backend backend db_backend mode tcp balance roundrobin option mysql-check server db1 192.168.0.1:3306 check rise 1 fall 3 server db2 192.168.0.2:3306 check rise 1 fall 3
frontendブロックにて、aclキーワードを使用し、is_db_backendというACLを定義します。
acl is_db_backend nbsrv(db_backend) eq 2
このとき is_db_backendは、db_backendというbackendブロックの生きているサーバー数が"2"であるか?という意味を持ちます。nbsrvは数値を返すので、「eq 2」などとして数値で比較をする必要があります。
その次の行の、
use_backend db_backend if is_db_backend
で、どのbackendサーバーを使用するかという設定になります。※今回は、backendが一つしかないですが、複数定義することが可能となっています。
このときの使用条件で、is_db_backend の値を参照していて、if キーワードで is_db_backend が"2"であるかどうかを判定しています。db_backend のサーバーが全て生きていたら、db_backend のサーバーを使用します。もし、db_backend のサーバーが一つでも落ちていたら、db_backendは使用されません。
また条件を変えて、
nbsrv(db_backend) ge 1
とすれば、1つでも生きていたらという意味になるかと思います。
ちなみに、
acl is_db_backend nbsrv(db_backend) eq 2
use_backend db_backend if is_db_backend
を1行で書くとこうなります。
use_backend db_backend if { nbsrv(db_backend) eq 2 }
"{","}"を使用して記述します。※"{","}"の前後にスペースが必要です。
srv_is_up
指定されたサーバーが生きていたらTRUE、それ以外であればFALSEという結果を返します。
設定例:あるbackendのサーバーが生きていれば、そのサーバーを主系として振り分ける。
frontend db_front bind :3306 acl is_db1_up srv_is_up(db_backend1/db1) acl is_db2_up srv_is_up(db_backend2/db2) use_backend db_backend1 if is_db1_up use_backend db_backend2 if is_db2_up backend db_backend1 mode tcp option mysql-check server db1 192.168.0.1:3306 check rise 1 fall 3 backend db_backend2 mode tcp option mysql-check server db2 192.168.0.2:3306 check rise 1 fall 3
まず、frontend ブロックでの
acl is_db1_up srv_is_up(db_backend1/db1)
acl is_db2_up srv_is_up(db_backend2/db2)
の部分で2つのACLを定義しました。上の方は、db_backend1のdb1というサーバーがあがっているかどうかを定義し、下の方は、db_backend2のdb2というサーバーがあがっているかどうかを定義しています。
サーバーがあがっているかどうかの判定部分は、
srv_is_up(db_backend1/db1)
の部分になります。そして、次の2行で、
use_backend db_backend1 if is_db1_up
use_backend db_backend2 if is_db2_up
どのbackendサーバーを使用するかを定義しています。ここで2つuse_backendというキーワードを使っていて、どのbackendを利用するかという疑問があるかと思いますが、上に記述された方から先に評価をしていき、条件が満たされるものがあればそれを使用していくという動きになっています。
is_db1_upがTRUEであれば、db_backend1のサーバーを使います。もし、db_backendのdb1サーバーが落ちていた場合は、次の行のis_db2_upが評価され、db_backend2が使用されるか判定されます。
評価の方法は、nbsrvの記述部分と同じで、ifキーワードを利用しています。
src
HAProxyに接続してきているIPアドレスを意味する
設定例:特定のIPアドレス(192.168.0.1)からの接続は、サーバーAに振り分け、それ以外はサーバーBに振り分ける
rontend web_front bind :80 acl is_src src 192.168.0.1 use_backend web_a if is_src use_backend web_b unless is_src backend web_a mode http server web1 192.168.0.2:80 check rise 1 fall 1 option httpchk backend web_b mode http server web2 192.168.0.3:80 check rise 1 fall 1 option httpchk
接続してきたIPアドレスが、「192.168.0.1」であるかどうかを判定し、use_backend キーワードで振り分けを実現しています。unless は、if の反対の意味を持ちます。
acl is_src src 192.168.0.1
use_backend web_a if is_src
use_backend web_b unless is_src
src_port
src がIPアドレスに対し、こちらはポートによる設定が可能。
dst
HAProxy側のIPアドレスを意味する。
※使用するケースとして、HAProxyが稼働しているサーバーに複数のIPが割り当てられているときに使用するようなケースだと思ってます。
設定例:IPアドレスAで接続してきたリクエストは、サーバーAへ。IPアドレスBで接続してきたリクエストは、サーバーBへ振り分ける。
rontend web_front bind :80 acl is_dst dst 192.168.0.1 acl is_dst_local dst 127.0.0.1 use_backend web_a if is_dst use_backend web_b if is_dst_local backend web_a mode http server web1 192.168.0.2:80 check rise 1 fall 1 option httpchk backend web_b mode http server web2 192.168.0.3:80 check rise 1 fall 1 option httpchk
この設定の場合、「192.168.0.1」でリクエストした場合は、サーバーAへ振り分けられます。また、ローカルホストから「127.0.0.1」でリクエストした場合は、サーバーBへ振り分けられます。
srcもそうですが、違うネットワーク同士で振り分けを変えたいときに使えるか思います。
dst_port
dst がIPアドレスが対象なのに対し、こちらはポートによる設定が可能。
be_conn
backend で確立された(established)コネクションの接続数が適用されます。
設定例:一定の接続数を超えたら、エラーページにリダイレクトさせる。
backend web1
mode http
server web1 172.16.1.129:80 check rise 1 fall 3
acl is_be_conn be_conn gt 1 # テストのため"1"を設定
option httpchk
redirect location /error.html if is_be_conn
backendセクション内でaclキーワードで"1"を超える接続というACLを定義。超えたときの振る舞いとして、reirectキーワードでACLを評価するように定義しておく。
(サイズ重めの画像をWebサーバー上に配置しておき、それをブラウザで強制リロードをすばやく2度やると、リダイレクトされることを確認しました。)
他にも接続数、単位時間でのセッション作成数といったリクエスト数に応じたCriteriaもいくつかあるようです。
=>"fe_conn", "queue" , "be_sess_rate" , "fe_sess_rate"
ここまでのものは下位のレイヤーの話のものなので、上位のレイヤーになれば高度なことができるようです。
=>HTTPヘッダの内容でACLを定義、等。
ここに書いたことは本当に一部分にすぎないので、もっと有効な使い方があるかと思いますが、HAProxyはかなり高機能でお手軽に負荷分散や可用性を高めることが可能なのでおすすめです。
ネームベースのVirtualHostで複数のポート、複数のドメインを設定する
Apache2.2.x で複数のポートで別のサイトを公開したい場合は、以下の設定で公開することが可能です。
Listen 80 Listen 8080 NameVirtualHost 172.20.30.40:80 NameVirtualHost 172.20.30.40:8080 <VirtualHost 172.20.30.40:80> ServerName www.example.com DocumentRoot /www/domain-80 </VirtualHost> <VirtualHost 172.20.30.40:8080> ServerName www.example.org DocumentRoot /www/otherdomain-8080 </VirtualHost>
ただし、Webブラウザで「http://www.example.com:8080/」を指定すると、www.example.org の方の DocumentRoot が閲覧できてしまいます。
また、「http://www.example.org/」とした場合は、逆に www.example.com の方のVirtualHost の DocumentRoot が閲覧できてしまいます。
これをどうにか制御して、指定したドメインとポートの組み合わせ以外は、アクセスできないようにしたいと思ってこんな感じで設定してみました。
上の VirutalHost の設定に以下の設定を追記。
# deny access <VirtualHost 172.20.30.40:80> ServerName www.example.org <Location "/"> Order allow,deny </Location> </VirtualHost> # deny access <VirtualHost 172.20.30.40:8080> ServerName www.example.org <Location "/"> Order allow,deny </Location> </VirtualHost>
冗長な設定の仕方であまり気に入らないのですが、こうすることで、意図しないドメインとポートの組み合わせでアクセスしてきた場合は、アクセス拒否をすることで防いでいます。
リダイレクトさせてしまってもいいかもしれませんが、どうするかは運用方法によるかと思います。そもそも意図しない組み合わせでアクセスしても問題ないのであれば、この設定はいらない訳です。
そもそも、一つの IP でポートを分けて構築する例ってあんまりないのかもしれないけど。。。