複数のテストサーバをリバースプロキシで集約 (2)
複数のテストサーバをリバースプロキシで集約 (1) - daily dayflower の続きです。
前回 mod_proxy と mod_rewrite を組み合わせてリバースプロキシ環境を構成しました。が,そのままだとプロキシ先のサーバが増減するたびに設定ファイルを書き換えて httpd を再起動しなくてはなりません。
再起動することなく動的にマッピング先を変えるにはどうすればよいのか。何らかの手段でマッピング情報を「外在化」させる必要があります。
動的にマッピング先を変える
前回 [http://httpd.apache.org/docs/2.2/mod/mod_rewrite.html#rewritemap:title=RewriteMap]
というのを持ち出してきました。mod_rewrite に内蔵された内部フィルタ(int:tolower
)を用いましたが,実はファイル(プレーンテキストファイル,DBM ファイル)やプログラムを値フィルタとして用いることもできます。
当初このファイルによる値フィルタを使えないかなと思ったんですが,いい方法が思いつきませんでした*1。なので,プログラムによる値フィルタを使ってみます。
RewriteMap lowercase int:tolower RewriteMap proxymapper prg:/var/www/proxymapper.pl RewriteLock /tmp/mapper.lock RewriteCond ${lowercase:%{SERVER_NAME}}%{REQUEST_URI} ^(.*)$ RewriteCond ${proxymapper:%1} ^(.+)$ RewriteRule ^/ %1 [proxy]
という設定です。
まず
RewriteCond ${lowercase:%{SERVER_NAME}}%{REQUEST_URI} ^(.*)$
というところで SERVER_NAME
(を小文字化したもの)と REQUEST_URI
を結合しています。それを ^(.*)$
でマッチングしているので,この RewriteCond
は必ず成功します。そして括弧でくくっているので,マッチング対象が %1
という変数に入ります。
次に
RewriteCond ${proxymapper:%1} ^(.+)$
というところで,proxymapper
というフィルタに先ほどのマッチング結果を渡しています。このフィルタの出力を ^(.+)$
でマッチングしているので,この RewriteCond
もほぼ必ず成功します。
最後に
RewriteRule ^/ %1 [proxy]
というところでルールの適用を行います。すべての URL を先ほどのマッチング結果=proxymapper
によるフィルタリング結果に置換します。そして [proxy]
フラグによって mod_proxy
に処理を移譲します。
proxymapper
というのは
RewriteMap proxymapper prg:/var/www/proxymapper.pl
で指定したマッパです。先頭に prg:
とつけると,指定されたファイルをプログラムとみなしてマッピングを行います。
ちなみにこのプログラムは Apache の起動時,もっというと設定時に起動されます。つまり fork()
する前なので 1 インスタンスのみ起動されます。この 1 インスタンスのプログラムを各リクエストで共有するのでフィルタリングの入出力を直列化するために
RewriteLock /tmp/mapper.lock
のようにロック用ファイルが必要となります*2。
proxymapper.pl
はたとえば下記のようなスクリプトです。
#!/usr/bin/perl use strict; use warnings; $| = 1; while (<STDIN>) { chomp; my ($server, $path) = split '/', $_, 2; if (0) {} elsif ($server eq 'outer1.example.com') { if (0) {} elsif ($path =~ m{ \A \Q/path1\E }xms) { print "http://inner-a.example.com${path}", "\n"; next; } elsif ($path =~ m{ \A \Q/path2\E }xms) { print "http://inner-b.example.com${path}", "\n"; next; } } elsif ($server eq 'outer2.example.com') { if (0) {} elsif ($path =~ m{ \A \Q/path3\E }xms) { print "http://inner-b.example.com${path}", "\n"; next; } } print "NULL", "\n"; }
標準入力から一行読み込んで,マッピング結果を一行出力する,というのを延々と繰り返すプログラムです。これが prg:
タイプの RewriteMap
プログラムのインタフェースです。NULL
を返すとマッピング失敗とみなされます。
RewriteCond ${lowercase:%{SERVER_NAME}}%{REQUEST_URI} ^(.*)$
にて SERVER_NAME
と REQUEST_URI
を結合していたので,split()
によって分割して該当するマッピング先を決定しています。それだけのスクリプトです。
フィルタリングプログラムが落ちたらどうしよう
かくして「マッピング情報」を「外在化」させることに成功しました。しかし,このマッピング用スクリプトは Perl で書いており,(まあないとは思いますが)いつか落ちてしまうかもしれません。とても不安です。
これに対処するため,フィルタリングプログラムをできるだけ「軽く」書いて,実際のマッピング作業をさらに外在化させてみましょう。どういうことかというと,
prg:
で指定されるフィルタリングプログラムは UNIX ドメインソケットに接続してフィルタリング情報を投げる- UNIX ドメインソケットで待ち受けして実フィルタリング作業を行うプログラムを立ち上げる
のような2層構造にするということです。これで後者が落ちたとしても後者のみ再度立ち上げればよいことになります。
前者のコード例は 502 Bad Gateway におきました。ソケットプログラミングするのは久しぶりなのでいろいろチョンボがあるかもしれません。
後者の待ち受けサーバ側ですが,たとえば下記のようなコードになります。
#!/usr/bin/perl use strict; use warnings; our $SOCKET_PATH = '/tmp/proxymapper'; our $CONFIG = <<'END_YAML'; --- outer1.example.com: /path1: http://inner-a.example.com/path1 /path2: http://inner-b.example.com/path2 /: NULL outer2.example.com: /path3: http://inner-b.example.com/path3 /: NULL END_YAML use IO::Socket::UNIX; -e $SOCKET_PATH && unlink $SOCKET_PATH; my $listener = IO::Socket::UNIX->new( Type => SOCK_STREAM, Local => $SOCKET_PATH, Listen => SOMAXCONN, ) or die $!; $SIG{PIPE} = sub { print {*STDERR} "SIGPIPE\n"; }; while (1) { my $conn = $listener->accept() or die $!; $conn->autoflush(1); print {*STDERR} "connection accepted\n"; while (1) { my $line = $conn->getline(); last if ! $line; print {*STDERR} "received: ", $line; my $result = 'NULL'; ######################################## chomp $line; my ($server, $path) = split '/', $line, 2; if (defined $server && defined $path) { $path = '/' . $path; use YAML; #my $mapping = YAML::LoadFile('proxy.yaml'); my $mapping = YAML::Load($CONFIG); if (exists $mapping->{$server}) { my $len = 0; foreach my $key (%{$mapping->{$server}}) { next if $len > length $key; if (substr($path, 0, length $key) eq $key) { $result = $mapping->{$server}->{$key}; $result .= substr $path, length $key; $len = length $key; } } } } ######################################## $conn->print($result, "\n"); print {*STDERR} "sent: ", $result, "\n"; } print {*STDERR} "connection closed\n"; $conn->close(); }
ここまで自力で書く必要もない気がしますが,IO::Socket
でゴリゴリ書いています。つーても多重化してないのでここがすごく律速になってしまうでしょう。あくまで参考ということで。
ここではマッピング情報をコード内部に埋め込んでいますが,外部ファイルから読み込むようにすれば,晴れて httpd の再起動を行う必要のないリバースプロキシ環境ができあがったことになります。
とはいえ,まだまどろっこしいですよね。ということで続きます。→複数のテストサーバをリバースプロキシで集約 (3) - daily dayflower