3つとも「間に入る」仕組みだが、目的が違う
リバースプロキシ、ロードバランサ、CDNは、どれもクライアント(ブラウザ)とアプリケーションサーバーの間に入ってリクエストを中継します。違うのは、何のために中継するかです。リバースプロキシはサーバーの手前に立つ受付です。ロードバランサは複数のサーバーにリクエストを振り分けます。CDNは世界各地にコピーを置いて配る仕組みです。
3つの機能は一部が重なります。nginxはリバースプロキシとしてもロードバランサとしても動きます。CloudflareはCDNですが、通信を中継するのでリバースプロキシでもあります。この3つの用語は製品名ではなく役割の名前だ、と考えると整理しやすくなります。
| リバースプロキシ | ロードバランサ | CDN | |
|---|---|---|---|
| 主な役割 | リクエストを受けて背後のアプリへ渡す | 複数のサーバーへ負荷を分ける | 利用者の近くからキャッシュを返す |
| 置く場所 | オリジンサーバーの手前(同じVPS内が多い) | サーバー群の手前 | 世界各地のエッジ拠点(利用者側) |
| 解決する問題 | HTTPS化、複数アプリの同居、静的ファイルの配信 | 負荷の集中、1台の停止によるサービス断 | 距離による遅延、オリジンの負荷、転送量 |
| 代表例 | nginx、Apache、Caddy | nginx、HAProxy、AWS Elastic Load Balancing | Cloudflare、Amazon CloudFront、Fastly |
リバースプロキシ:サーバーの手前に立つ受付
リバースプロキシはサーバー側に置きます。外部からのリクエストをアプリの代わりに受け取り、背後のアプリへ渡します。一般的なプロキシ(フォワードプロキシ)は、社内ネットワークの利用者などクライアント側の代理として外部へ接続します。これとは逆にサーバー側の代理をするので「リバース」と呼びます。
個人開発でよく見るのは、VPS上でNode.jsやPythonのアプリを3000番や8000番のポートで動かし、その前にnginxを置く構成です。
ブラウザ
│ https://app.example.com (443番ポート)
▼
[VPS]
nginx(リバースプロキシ・TLS終端)
├─ app.example.com → 127.0.0.1:3000(Node.jsアプリ)
└─ blog.example.com → 127.0.0.1:8080(別のアプリ)この構成でnginxが受け持つのは、HTTPS化、ドメインやパスごとの振り分け、画像やCSSなど静的ファイルの配信です。HTTPS化では、TLS終端(暗号化された通信をここで復号すること)をnginxが行います。そのためアプリは証明書を扱う必要がなく、ローカルのHTTPだけを処理すれば済みます。最小限の設定は次のとおりです。
server {
listen 443 ssl;
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
}
}ロードバランサ:複数のサーバーへ負荷を分ける
ロードバランサは、同じ役割のサーバーを複数台並べてリクエストを分散させます。目的は2つあります。1台では処理しきれないアクセスをさばくことと、1台が止まっても残りのサーバーでサービスを続けること(冗長化)です。多くのロードバランサはヘルスチェック(定期的な死活確認)を行い、応答しないサーバーを振り分け先から外します。
ブラウザ │ ▼ ロードバランサ ├─→ アプリサーバーA ├─→ アプリサーバーB └─✕ アプリサーバーC(応答がないため振り分け先から除外)
nginxでも、upstream ブロックに複数のサーバーを書けば負荷分散ができます。オープンソース版のnginxは、接続に失敗したサーバーを一定時間外す受動的な方式で障害を検知します。
upstream app_servers {
server 10.0.0.11:3000;
server 10.0.0.12:3000;
}
server {
listen 443 ssl;
server_name app.example.com;
# 証明書の設定は省略
location / {
proxy_pass http://app_servers;
}
}クラウドでは、AWSのElastic Load BalancingやGoogle Cloud Load Balancingのようなマネージドサービスを使うのが一般的です。ロードバランサには2つの種類があります。HTTPの中身(URLやヘッダー)を見て振り分けるL7ロードバランサは、リバースプロキシの一種でもあります。TCPの接続単位で振り分けるL4ロードバランサは、HTTPの内容を見ません。サーバーが1台だけの段階では、ロードバランサは必要ありません。
CDN:利用者の近くにコピーを置く配信網
CDN(Content Delivery Network)は、世界各地のエッジサーバーにコンテンツのコピー(キャッシュ)を置き、利用者に近い拠点から返す仕組みです。エッジサーバーとは、利用者に近い場所にある中継サーバーのことです。これに対し、元のコンテンツを持つサーバーをオリジンと呼びます。
利用者(東京) → Cloudflareのエッジ(東京) ┐
利用者(ロンドン) → Cloudflareのエッジ(ロンドン) ┤ キャッシュがないときだけ中継
▼
[VPS] nginx(リバースプロキシ) → アプリCDNで解決できるのは、物理的な距離による遅延とオリジンの負荷です。画像やCSS、JavaScriptのように誰にでも同じ内容を返すファイルは、エッジから返されるのでオリジンに届きません。Cloudflareでは、DNSレコードのプロキシを有効(オレンジの雲)にすると、通信がCloudflareを経由します。オリジンのIPアドレスが外から見えにくくなるので、DDoS攻撃(大量の通信でサーバーを停止させる攻撃)への対策にもなります。
置く場所にも違いがあります。リバースプロキシとロードバランサはオリジンの近くに置きます。CDNは利用者の近く、つまりオリジンから見てネットワーク上の遠い側に置きます。
区別がつかないとつまずく場面
CDNを入れたら管理画面や更新内容が反映されない
原因はCDNのキャッシュです。CDNはオリジンの応答を一定時間保存して返すため、サイトを更新してもエッジに古いコピーが残ることがあります。Cloudflareは標準の設定では、画像・CSS・JSなどを拡張子で判断してキャッシュし、HTMLはキャッシュしません。しかし、表示を速くしようとキャッシュルールで「すべてキャッシュする」ように設定すると問題が起きます。ログイン後の管理画面やフォームの送信結果まで保存され、古い画面や別の利用者向けの画面が返ることがあります。
対策として、/wp-admin や /admin のような管理用のパスとログイン中のリクエストは、キャッシュの対象から外します。Cloudflareでは、キャッシュルールでバイパス(キャッシュしない)を指定します。CSSやJSの変更が反映されないときは、キャッシュをパージ(削除)します。あるいは、style.css?v=2 のようにバージョンを付けて、別のファイルとして扱わせます。オリジンが返す Cache-Control ヘッダーの値も確認してください。
アクセス元IPがすべて同じになる
間に中継役が入ると、背後のサーバーから見た接続元は中継役になります。nginxの後ろにあるアプリのログには127.0.0.1が記録されます。Cloudflare経由の場合、nginxのログにはCloudflareのIPアドレスが記録されます。この仕組みを知らないと、アクセスログが分析に使えません。IPアドレス単位でアクセスを制限したときに、全員をまとめて遮断してしまうこともあります。
中継役は、本来の接続元のIPアドレスをHTTPヘッダーに入れて渡すことができます。nginxからアプリへ渡すには、次のように設定します。アプリ側でもこれらのヘッダーを信頼する設定が必要です(Expressの trust proxy など)。
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}nginxの前にCloudflareがある場合は、nginxのrealipモジュールを使います。Cloudflareが付けるCF-Connecting-IPヘッダーの値を、本来の接続元として扱う設定です。
set_real_ip_from 173.245.48.0/20; # Cloudflareが公開するIP範囲をすべて列挙する real_ip_header CF-Connecting-IP;
set_real_ip_from には、Cloudflareが公式に公開しているIP範囲をすべて書きます。IP範囲を限定せずにヘッダーを信頼すると、誰でもヘッダーを書き換えて接続元IPを偽装できてしまいます。
HTTPSにしたらリダイレクトが止まらない
CloudflareのSSL/TLS暗号化モードが「フレキシブル」の場合、ブラウザからCloudflareまではHTTPS、Cloudflareからオリジンまでは暗号化されないHTTPで通信します。この状態で、オリジンのnginxに「HTTPで来たアクセスはHTTPSへリダイレクトする」設定があると、リダイレクトが無限に繰り返されます。Cloudflareはリダイレクトされても再びHTTPでオリジンにアクセスするためです。ブラウザには「リダイレクトが繰り返し行われました」と表示されます。オリジンに証明書を入れ、モードを「フル(厳密)」にすると解消します。
ロードバランサの後ろでログインが切れる
セッション情報を各サーバーのメモリに保存していると、ログインが切れます。次のリクエストが別のサーバーに振り分けられた時点で、ログイン状態が失われるためです。セッションの保存先をRedisやデータベースなど全サーバー共通の場所に移すと解決します。もう一つの方法は、同じ利用者を同じサーバーへ送り続けるスティッキーセッションを使うことです。
自分の構成に何が必要かを判断する基準
VPS 1台でアプリを動かすなら、nginxをリバースプロキシとして置くところから始めます。HTTPS化と複数サービスの同居は、これで解決します。アクセスが1台の性能を超えたときや、停止が許されないサービスになったときに、サーバーを増やしてロードバランサを検討します。海外からの利用者が多い場合、画像や動画の転送量が大きい場合、攻撃への備えが必要な場合は、CDNが有効です。
中継役を1つ追加したら、その直後に次の3点を確認します。1つ目は、アクセスログに本来の接続元IPが記録されているかです。2つ目は、管理画面やログイン後のページがキャッシュされていないかです。3つ目は、HTTPからHTTPSへのリダイレクトが1回で終わるかです。この3点は、ブラウザでの確認と curl -I https://app.example.com で返るステータスとヘッダーの確認で調べられます。