Randen Pederson

大規模なシステムであれば使っているであろうリバースプロキシ。 セキュリティや稼働率の観点からみて利用することは非常にメリットは高いです。 ただ、社内や周りであまり知見がなく、 「動くからいいや」という理由でApacheをそのままWebサービスの一次受けとして利用されている方も多いと思います。 動くという目的からすれば確かにその通りですが、ただ一枚リバースプロキシを入れるだけで ぐっと運用効率、稼働率も拡張性も上がります。

1. ルーティング処理の簡略化

例えばRESTfulな一般的なAPI構成を作りたいと思った時に以下のようなURL構成になると思います。

http://api.something.com/search/v1/item/list.json?cid=xxxx&gid=xxxxx

もしアプリケーション側のルーティングしか知らなければframework側のrouteのようなファイルで 以下のように設定するケースが多いかと思います。

fuelphpのルーティング設定ファイルであるroute.phpの例とすると

return array(
    'search/v1/item/list'   => 'item/list', //search/v1以下を飛ばす
    'search/v1/item/detail' => 'item/detail',
);

確かに動くといえば動きますが、毎回search/v1を書くのは直感的にどうかと思います。

一歩進んで、.htaccessやhttpd.confでRewriteすると・・・

RewriteEngine On
RewriteBase /
RewriteRule search/v1/(.*) /$1

v2として動かしたいときにはここのファイルを変えるのでしょうか。 Gitのコミットログも少々ノイズが入ってきそうです。

$ git log --oneline
f94f133 refs #3150 search/v2からsearchitem/v2に仕様変更(.htaccess)
d07a2ce refs #3121 search/v1からsearch/v2に変更(.htaccess)

.htaccessなどでURLの書き換えを行ってしまうと、新しい開発者からすると少々面倒です。 「なんだか良くわからないが、うまく動作しない」などと気付き、 よくみてみるとフレームワークの外側の.htaccessでゴリゴリ設定が入っているなど。 そんなプロジェクトの改修作業は極力避けたいものです。

リバースプロキシを入れた場合

nginxを例に設定を行ってみます。

適当に/etc/nginx/conf.d/reverse-proxy.confの名前で以下のファイルを作成します。

proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 600s;
proy_connect_timeout 10s;

そして任意の所に下記のreverse proxy設定を追加します。

server {
    listen *:80;
    server_name api.something.com;

    location /search/v1 {
        rewrite /search/v1/(.*) /$1 break;
        proxy_pass  http://localhost:8080;
    }
}

このようにすると、search/v1となっているものをバックエンドのアプリ側に引き渡すことが出来ます。 アプリ側からすれば、search/1を考えず以下のようなURLでアクセスが来たと考えれば良いだけです。

http://localhost:8080/item/list.json?cid=xxxx&gid=xxxxx
//http://api.something.com/search/v1/item/list.json?cid=xxxx&gid=xxxxx

簡単そうですね。このURL構成であれば、 コントローラ + アクション名の極めてシンプルなフレームワークのルーティング構成になります。 開発者も見たままのURL構成で実装すれば良いので非常に簡単です。

そしてアプリケーション側も細かなルーティングは気にしなくてよくなります。 SEO対策などでURL改変などはよくある事例だと思いますが、末端のスラッシュ以下のパラメータを アプリケーション側で変更することは簡単なのはご存知の通りです。 ただ、URLを一階層固定で下げたいなどの理由であれば、もう一つ上のレイヤーで処理させるべきです。

ワンドメイン配下に複数のサービスが出来る

簡単にサービスをワンドメイン配下に統合も可能です。 APIというドメインの名前の元、色々なサービスをどんどん拡張可能です。

server {
    listen *:80;
    server_name api.something.com;

    #検索用API 
    location /search/v1 {
        rewrite /search/v1/(.*) /$1 break;
        proxy_pass  http://localhost:8080;
    }

    #アイテム用API
    location /item/v1 {
        rewrite /item/v1/(.*) /$1 break;
        proxy_pass  http://localhost:8081;
    }

    #レコメンド用API v1
    location /recommend/v1 {
        rewrite /recommend/v1/(.*) /$1 break;
        proxy_pass  http://localhost:8082;
    }

    #レコメンド用API v2
    location /recommend/v2 {
        rewrite /recommend/v2/(.*) /$1 break;
        proxy_pass  http://localhost:8083;
    }
}

よくあるAPIのバージョンですが、上記のURLのような規則にしていれば、 全く別プロジェクトに対して、APIのエンドポイントを指定することも可能です。 これにより、既存のバージョンのAPIを残せるため、並行稼動なども容易にすることが出来ます。

このリバースプロキシ自体のconfig設定が、API全体の情報も集約する意味あいにもなれるので、 非常に運用効率自体も上がります。

注意点

一つ気をつけるのが、リバースプロキシ側との密接すぎるアプリケーションルーティングを組まないことです。 今度こちらが密結合になってしまうことは本末転倒です。 アプリ側で処理できるルーティングとリバースプロキシ側で処理出来るルーティングの中間地点を狙うべきです。 感覚的になりますが、リバースプロキシ経由でもURLを直感的にアクセスしてページが表示できるし、 アプリケーション単体でも同様にアクセス出来るような状態が好ましいです。

2. リリース切り替えが容易になる

プレリリースしたい場合は簡単で、リバースプロキシ側の向き先を社内のステージング環境に向けるだけでOKです。 これにより、簡単に本番環境のユーザを一時的にプレ本番であるステージング環境に流し込むことが出来ます。

いくらApacheBenchなどを利用して、ベンチマークを取ったとしても、 実際のユーザの挙動というもは似て非なるものです。それを一時的でもプレ本番環境で試して、 すぐに戻せるのはシステム開発において非常に安心感があります。

3. DNSの伝播も気にしなくてよい

ドメインのAレコードの変更設定してもインターネットの仕組み上すぐに反映されません。 ローカルブラウザを含め色々な所でDNSの情報はキャッシュされ、 毎回オリジンのDNSサーバに問い合わせなくて良いような仕組みになっています。

DNSサーバの負荷軽減という意味では良いですが、サービスリリースという意味では大敵です。 全く新規ドメインのサービスであれば、イニシャルアクセスがかかるので問題はありませんが、 同一ドメインのサーバ切り替えるなどという時には問題があります。 DNS側で設定を切り替えても、すぐにユーザに反映されるか分からないからです。

そのため、通常DNSサーバではTTL(time to live)を短めに設定して、すぐにDNS問い合わせをしてもらうように 設定します。ただそれであってもあくまでもTTLを短くしているだけで、 絶対に全ての端末が一瞬で切り替わってくれる保証はありません。

そんな時にリバースプロキシさえあれば、一切問題はありません。

ユーザからすれば、普段と同一のリバースプロキシに向けてアクセスするだけなので、 こちらがリバースプロキシの設定を変更したタイミングで、ユーザに変更を届けることが可能です。 逆に言えば、いざリリースをしてみてトラブルがあった時に即座に別サーバにある前バージョンに戻すことも可能ということです。 リリース後にすぐ差し戻せるのは精神的に安心感があるのは周知の通りです。

4. もしもの障害時に対策が出来る

一枚プロキシが挟まるため、障害時の対応がスムーズです。 例えばページ表示に問題があれば、簡単にペラ1のメンテナンスページに飛ばすことが出来ます。

server {
    listen *:80;
    server_name api.something.com;
    error_page 503 /path/to/maintenance.html;

    location /search/v1 {
        if (-f "/tmp/MAINTENANCE_LOCK") {
          return 503;
        }
        rewrite /search/v1/(.*) /$1 break;
        proxy_pass  http://localhost:8080;
    }

    location /path/to/maintenance.html {
    }
}

tmp以下に任意のファイルをtouchさせすれば簡単に503でユーザアクセスをOFFにすることが出来ます。 もしリバースプロキシが上位に無い場合はApache側などで直接書き換えなければなりません。

もちろんApacheのconf側にこの機能を持たせることも可能ですが、 複数台のサーバがあった時に、その配下のサーバにログインしてファイルをtouchしてロックするのかというと それはあまりにもナンセンスなことは自明です。

そもそも1台であれば、nginx上のupstreamから切り離しノーメンテナンスで行けますし、 全停止であればやはり上流、すなわちnginxで止めたほうがはるかに楽となります。

Docker実践入門――Linuxコンテナ技術の基礎から応用まで (Software Design plus)


(更新 2014/08/26 01:47:00) サンプルソースコードのサーバネームなど一部統一修正いたしました。