tva
← コラム

リバースプロキシの多層化:本番環境の構成

1つのアプリケーションの前に4つのリバースプロキシを置く構成は、一見すると簡素化すべきシステムに見えます。本番のWebインフラを扱う多くのチュートリアルは、Nginx、Caddy、Traefikのいずれか1層で済ませ、それ以上は過剰設計とみなします。しかし実際には、それぞれが異なる問題を解決します。役割分担を理解すれば、冗長に見える構成にも一貫性があると分かります。

当社は複数の顧客向け環境でこの構成を運用しています。エッジにCloudflare、コンテナを認識する入口としてTraefik、HTTPキャッシュにVarnish、アプリケーションのホストにNginxを置き、その先にアプリケーション本体があります。リクエストは次のように流れます。

Cloudflare → Traefik → Varnish → Nginx → Application

以下では、各層の役割、責任の境界、性能への影響を説明します。より単純な構成では生じない追加負荷についても取り上げます。

各層の役割

Cloudflareは公開インターネットのエッジで動作します。DDoSトラフィックを吸収し、WAFルールを適用し、世界各地のPoPでTLSを終端し、利用者の近くに静的ファイルをキャッシュします。一方、コンテナの状況に応じたルーティング判断や、Cookieの確認が必要なアプリケーションレスポンスのキャッシュは得意ではありません。それは別の層が担います。

Traefikは、インフラ内でコンテナを認識する入口です。マルチテナントのDockerスタックを運用する際には、コンテナのラベルを読み取り、それに応じてルーティングできる点が役立ちます。コンテナの再起動や拡張で実態とずれる手動設定ファイルは不要です。内部ドメインやステージング用ドメインのLet's EncryptのTLS証明書を管理し、ヘッダーを削除・書き換え、IP許可リストやBasic認証などのミドルウェアを連続して適用します。Traefikはキャッシュを行わず、高負荷時の圧縮も得意ではありません。役割はルーティングとミドルウェアであり、それを効率的に処理します。

VarnishはHTTPアクセラレーターです。入口とアプリケーションホストの間に置き、読み取りリクエストを吸収します。適切に調整したVarnishは、メモリから毎秒数万件のリクエストを処理し、HTMLページ全体を1ミリ秒未満で返せます。VCL(Varnish Configuration Language)では、キャッシュの対象、期間、条件を、Cloudflareのキャッシュルールよりもはるかに細かく制御できます。認証を伴うアプリケーションでカスタムハッシュを使えばユーザーごとにキャッシュできるレスポンスがある場合(以下の例では行わず、ログイン済みユーザーの通信はキャッシュせずに転送します)や、クエリパラメータによってTTLを変えるページで重要になります。

Nginxはこの構成で、本来の役割であるファイル配信と上流のアプリケーションプロセスへのプロキシを担います。CloudPanelはNginxをマネージドWebサーバーとして使い、各仮想ホストにはPHP-FPM、静的ファイル配信、gzip圧縮を処理する設定が自動生成されます。この設定はCloudPanelが管理するので、直接編集すべきではありません。ここでNginxは判断を行う層ではなく、リクエストを受け、アプリケーションへ転送し、レスポンスを返す役割です。

Traefikの設定:コンテナラベル

この構成では、TraefikのルーティングをすべてDocker Composeのラベルで設定します。Traefikの後ろにVarnishを置く際の要点は、転送先をNginxではなくVarnishコンテナにすることです。ここはよく混乱が起きます。標準的なTraefikの例はアプリケーションへ直接転送する構成であり、その経路にVarnishを組み込む方法は十分に文書化されていません。

services:
  varnish:
    image: varnish:7.5
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.myapp.rule=Host(`www.example.com`)"
      - "traefik.http.routers.myapp.entrypoints=websecure"
      - "traefik.http.routers.myapp.tls=true"
      - "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
      - "traefik.http.services.myapp.loadbalancer.server.port=80"
      - "traefik.http.middlewares.myapp-proto.headers.customrequestheaders.X-Forwarded-Proto=https"
      - "traefik.http.middlewares.myapp-proto.headers.customrequestheaders.X-Forwarded-Port=443"
      - "traefik.http.routers.myapp.middlewares=myapp-proto"
    networks:
      - traefik-public
      - internal

X-Forwarded-Protoヘッダーは重要です。VarnishとNginxはDockerの内部ネットワークで暗号化されていないHTTP通信を行うため、このヘッダーがないと、アプリケーションはすべてのリクエストをHTTPと認識します。その結果、HTTPSを必須とするアプリケーションでリダイレクトループが起こります。Traefikが内部ネットワークに入る前にヘッダーを付け、Varnishがそのまま通し、Nginxがアプリケーションへ渡します。併用するX-Forwarded-Portヘッダーは、ポートも確認するアプリケーションで同種のループを防ぎます。

もう1つ注意すべきなのは、TraefikのpassHostHeaderが初期設定でtrueであり、Varnishに元のHostヘッダーが届くことです。これは望ましい動作です。VarnishのVCLはキャッシュキーに、Nginxは仮想ホストの選択に使えます。変更すると両方に誤ったホスト情報が渡り、原因を追いにくい形で、明確なエラーを出さずに失敗します。

Varnish VCL:キャッシュのロジック

以下のVCLは、CloudPanelの背後で動くWordPressやPHPアプリケーション向けの初期構成であり、本番運用の完成されたポリシーではありません。ログイン済みユーザーや冪等でないリクエストのキャッシュ回避、静的ファイルの長期キャッシュ、トラッキングパラメータの正規化、Varnishが更新する間に少し古いページを返す猶予期間といった一般的なケースに対応します。本番運用前には、WooCommerceのカート、決済、アカウントの各ページとwp_woocommerce_session_ Cookieもキャッシュせずに転送してください。このvcl_backend_responseはreturn(deliver)で終わるため、Set-CookieまたはCache-Control: private/no-storeを含むレスポンスをキャッシュ不可にするVarnishの組み込みロジックが実行されません。同等のチェックを追加してください。

vcl 4.1;

backend default {
    .host = "nginx";  # service name in docker-compose
    .port = "80";
    .connect_timeout = 5s;
    .first_byte_timeout = 30s;
    .between_bytes_timeout = 10s;
}

sub vcl_recv {
    # Pass admin and authenticated requests
    if (req.url ~ "^/wp-(admin|login|cron|json)" ||
        req.http.Cookie ~ "wordpress_logged_in_" ||
        req.http.Cookie ~ "woocommerce_items_in_cart") {
        return(pass);
    }

    # Only cache GET and HEAD
    if (req.method != "GET" && req.method != "HEAD") {
        return(pass);
    }

    # Normalise tracking parameters out of the cache key
    if (req.url ~ "[?&](utm_source|utm_medium|utm_campaign|fbclid|gclid)=") {
        set req.url = regsuball(req.url,
            "&(utm_source|utm_medium|utm_campaign|fbclid|gclid)=[^&]+", "");
        set req.url = regsuball(req.url,
            "\?(utm_source|utm_medium|utm_campaign|fbclid|gclid)=[^&]+&", "?");
        set req.url = regsub(req.url,
            "\?(utm_source|utm_medium|utm_campaign|fbclid|gclid)=[^&]+$", "");
    }

    # Remove cookies for static assets — allows caching
    if (req.url ~ "\.(css|js|png|jpg|jpeg|gif|ico|woff2?|svg|webp)(\?.*)?$") {
        unset req.http.Cookie;
    }

    return(hash);
}

sub vcl_backend_response {
    # Cache static assets for 7 days
    if (bereq.url ~ "\.(css|js|png|jpg|jpeg|gif|ico|woff2?|svg|webp)(\?.*)?$") {
        set beresp.ttl = 7d;
        set beresp.grace = 24h;
        unset beresp.http.Set-Cookie;
        return(deliver);
    }

    # Cache HTML pages for 5 minutes with a 60-second grace period
    if (beresp.status == 200 && beresp.http.Content-Type ~ "text/html") {
        set beresp.ttl = 300s;
        set beresp.grace = 60s;
    }

    # Never cache error responses
    if (beresp.status >= 500) {
        set beresp.ttl = 0s;
    }

    return(deliver);
}

sub vcl_deliver {
    # Expose cache status for debugging
    if (obj.hits > 0) {
        set resp.http.X-Cache = "HIT";
        set resp.http.X-Cache-Hits = obj.hits;
    } else {
        set resp.http.X-Cache = "MISS";
    }

    # Remove server identity headers
    unset resp.http.Via;
    unset resp.http.X-Varnish;

    return(deliver);
}

vcl_recvでのトラッキングパラメータの正規化は見落としやすく、怠ると大きな損失につながります。CloudflareでURLの書き換えを設定しない限り、UTM値やfbclidトークンは元のURLに残ったままVarnishへ届きます。正規化しないと、アプリケーションから見れば同じページでも、Facebook広告のクリックごとに別のキャッシュエントリが作られます。キャンペーン期間中にキャッシュが数千の未利用エントリへ分散し、ヒット率を高める効果が完全に失われます。

vcl_backend_responseのgrace設定も、詳しく理解すべき仕組みです。キャッシュのTTLが切れると、通常、次のリクエストはNginxが新しいレスポンスを返すまで待ちます。ニュースレター配信やSNSでの紹介でアクセスが急増すると、期限切れのエントリに複数のリクエストが同時に届きます。猶予期間がなくても、キャッシュ可能なオブジェクトについてはVarnishがリクエストをまとめ、Nginxへの取得を1件に抑えます。ただし、ほかのリクエストはその取得が完了するまで待ちます。60秒の猶予期間があれば、1件のバックグラウンド取得がキャッシュを更新する間、Varnishはそれらのリクエストに古いオブジェクトを即座に返します。結果として、期限切れの瞬間にNginxが処理するのは数百件ではなく1件となり、訪問者も待たずに済みます。

性能への影響

この構成の性能上の効果は一様ではありません。優れた効果が得られる箇所も、測定できる追加負荷が生じる箇所もあります。

効果が高い場面:Varnishのメモリからキャッシュ可能なページを返す場合、Traefikの層での応答は2ms未満です。PHPで生成に400msかかるWordPressのホームページも、TTLが切れるまでは後続のリクエストに1〜2msで返せます。スループットは小幅な改善ではなく、桁違いに向上します。Nginxの静的ファイル配信は元々高速で、Cloudflareもエッジでキャッシュするため、本番環境ではNginxがそれを処理する機会はほとんどありません。

追加負荷が生じる場面:キャッシュミス時には、1ホップではなく4ホップを経由します。ログイン済みユーザーのセッション、POST送信、WooCommerceのカートページなど、キャッシュできないPHPリクエストは、PHP-FPMに届く前にTraefik、Varnish、Nginxを通過します。同一ホスト上のDockerネットワークでは、1ホップごとに約0.1〜0.3msの遅延が加わります。PHPの応答が50msなら許容範囲ですが、5ms未満で返すべきAPIエンドポイントでは適しません。その場合は、別のTraefikルータールールでAPIのパスをアプリケーションコンテナへ直接転送し、Varnishを完全に迂回するのが適切です。

メモリ容量は多くの設定値より重要:VarnishはオブジェクトストアをRAMに確保します。公式Dockerイメージの初期設定は100MBです。固有のURLが多いサイトではすぐに埋まり、キャッシュからの追い出しが増え、TTLの設定にかかわらずヒット率が低いままになります。デプロイ後、性能改善のために最初に試すべきなのは割り当ての増加です。起動フラグ-s malloc,2Gか、Dockerの環境変数VARNISH_SIZE=2Gを使い、varnishstat -1 | grep hitで結果を監視します。

SSL終端:公開TLS接続はCloudflareで終端されます。CloudflareのSSLモードを「Full (strict)」にすると、Cloudflareは上記のルーターのLet’s Encrypt証明書を使い、HTTPSでTraefikに接続します。Traefikの背後では、TraefikとVarnish間、VarnishとNginx間はDockerの内部ブリッジネットワーク上で暗号化されていないHTTP通信を行います。これは意図した設計です。すべてのコンテナを管理する単一ホストでは、内部のコンテナ通信にTLSを追加するとCPU負荷が増える一方、セキュリティ上の利点は限定的です。ブリッジ上の通信はホストの外に出ず、TLSを追加してもコンテナやホストの侵害からは保護できません。脅威モデル上、コンテナ間の暗号化が必要なら、別のアーキテクチャとして検討すべきです。

この構成が適する場面

これは汎用的な推奨構成ではありません。キャッシュ要件やCloudPanelの制約がない単一のコンテナ化アプリケーションなら、Traefikから直接転送するほうが単純で適切です。4層構成が複雑さに見合うのは、Nginxが必須のCloudPanel管理環境、ページ全体のキャッシュでスループットが桁違いに向上するCMSを使ったサイト、あるいはTraefikが既に12個のコンテナのルーティングを担う複数サービスのデプロイ環境です。最後のケースでは、そのうち1つにVarnishを追加するのは新システムの導入ではなく、小さな追加コストです。

責任を分けることで、複雑さを長期にわたって管理できます。Traefikの設定はDocker Composeのラベルに、VarnishのロジックはComposeスタックと一緒にバージョン管理するVCLファイルに置きます。Nginxの設定はCloudPanelが管理します。互いの担当領域には踏み込みません。想定外の動作が起きても、調べる経路は明確です。X-CacheヘッダーはVarnishが応答したかを示し、Traefikのアクセスログはルーター、バックエンドサービス、上流から返されたステータスコードとクライアントに送信したステータスコードを、NginxのエラーログはPHP-FPMの報告を記録します。各層から観測できる情報が得られ、気づかないまま層をまたいで障害が重なることを防げます。

冗長に見えるものは、実際にはエッジでの保護、ルーティング、キャッシュ、アプリケーション配信の明確な境界です。4つの役割は独立して拡張でき、障害も独立して発生します。この境界には、設定箇所が増える負担に見合う価値があります。

関連記事

関連記事