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 でポートを分けて構築する例ってあんまりないのかもしれないけど。。。