3つとも「間に入る」仕組みだが、目的が違う

リバースプロキシ、ロードバランサ、CDNは、どれもクライアント(ブラウザ)とアプリケーションサーバーの間に入ってリクエストを中継します。違うのは、何のために中継するかです。リバースプロキシはサーバーの手前に立つ受付です。ロードバランサは複数のサーバーにリクエストを振り分けます。CDNは世界各地にコピーを置いて配る仕組みです。

3つの機能は一部が重なります。nginxはリバースプロキシとしてもロードバランサとしても動きます。CloudflareはCDNですが、通信を中継するのでリバースプロキシでもあります。この3つの用語は製品名ではなく役割の名前だ、と考えると整理しやすくなります。

リバースプロキシロードバランサCDN
主な役割リクエストを受けて背後のアプリへ渡す複数のサーバーへ負荷を分ける利用者の近くからキャッシュを返す
置く場所オリジンサーバーの手前(同じVPS内が多い)サーバー群の手前世界各地のエッジ拠点(利用者側)
解決する問題HTTPS化、複数アプリの同居、静的ファイルの配信負荷の集中、1台の停止によるサービス断距離による遅延、オリジンの負荷、転送量
代表例nginx、Apache、Caddynginx、HAProxy、AWS Elastic Load BalancingCloudflare、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 で返るステータスとヘッダーの確認で調べられます。