ほぼすべてのWebサービスの前面にあるもの
ようこそ
ブラウザに example.com と入力しても、実際にアプリケーションを実行しているマシンに直接到達することはほとんどありません。到達するのは、リクエストを実行マシンに転送するマシンです。その転送を行うマシンには名前があります:逆プロキシです。
このレッスンでは、逆プロキシが何をするのか、なぜほぼすべてのパブリックなWebサービスがその背後に隠れているのか、そしてエッジレイヤーが同時に処理する3つの役割について学びます。
終了時には、以下のことを理解できるようになっています:
- クライアント、プロキシ、オリジンの違い
- なぜオリジンの前にプロキシを置くことで、保護・スケーリング・誰も気づかずに部品を交換できるのか
- リバースプロキシが同時に処理する3つの役割:オリジンの隠蔽、TLSの終端、レプリカへの負荷分散
- リクエストがブラウザからプロキシ、アップストリームを経て戻るまでのホップごとの流れ
学習終了時には、プロキシの配置、エッジにおける関心の分離、そしてステートレスなプロキシ層が安価に増殖できる一方で、単一のオリジンはそうではない理由について、自信を持って推論できるようになります。
フォワードプロキシとリバースプロキシ
プロキシの2つの方向性
両方のプロキシは2つの当事者の間に立ち、トラフィックを中継します。違いはどちらの当事者を代表しているかです。
フォワードプロキシ: クライアントのグループの前に立ちます。クライアントはプロキシの存在を知っており、外部サーバーはクライアントのアドレスではなくプロキシのアドレスを見ます。企業の出口プロキシ、コンテンツフィルタ、およびSOCKSプロキシはこのパターンに該当します。
リバースプロキシ: サーバーのグループの前に立ちます。外部の世界(クライアント)はプロキシのアドレスと対話し、クライアントは背後に実際のサーバーが隠れていることを知りません。ほぼすべてのパブリックなWebサービスがこれを使用しています。
語呂合わせ: フォワードプロキシはサーバーからクライアントを隠します。リバースプロキシはクライアントからサーバーを隠します。
向きがなぜ重要か? 役割、障害モード、セキュリティ境界はすべて異なります。フォワードプロキシはユーザーが誰と通信しているかを気にし、リバースプロキシは誰がサーバーに接続しているかを気にします。
クライアントが両方の種類を同時に通してトラフィックを送信する場合、経路は以下のようになります: client -> forward_proxy -> internet -> reverse_proxy -> origin。
なぜこれほど多くの処理がエッジで行われるのか
エッジレイヤーがその価値を証明する
リバースプロキシは同時に3つの役割を担っています。そのうち1つだけでもこのレイヤーの存在を正当化できますが、同じアドレスでこれら3つを同時に処理する点が、なぜほぼすべての本番環境のWebアーキテクチャがフロントエンドで同じ形状に見えるのかを説明しています。
役割1:オリジンを隠す。 プロキシはパブリックIPで応答します。バックエンドは、インターネットから到達できないプライベートIPに配置されます。攻撃者がオリジンに直接アクセスしようとする場合、まずプロキシを侵害する必要があります。
役割2: TLSの終端。 プロキシはexample.comの証明書を持ち、受信したHTTPSリクエストを復号化します。バックエンドは、信頼できるネットワークセグメント上でプロキシに対してプレーンHTTP(またはよりシンプルな内部TLS)で通信します。証明書のローテーション、更新、および暗号化ポリシーはすべて1つの場所に集約されます。
役割3: ロードの分散。 プロキシは各リクエストをどのバックエンドが処理するかを選択します。1つのプロキシの背後にあるバックエンドはプールを形成し、プロキシは戦略(ラウンドロビン、最少接続数、ヘッダーのハッシュなど)に基づいてリクエストごとに1つを選択します。容量を追加するには、プールにバックエンドを追加するだけでよく、すべてのクライアントに新しいアドレスを通知する必要はありません。
各役割は小さなプログラムです。これらが組み合わさることで、なぜ「Pythonアプリの前にCaddyを置く」というような単純なレイヤーが、Pythonアプリ自体よりも多くの設計上の重みを持つのかを説明できます。
3つの役割すべてを考慮した設計
あなたのチームは、単一のVM上のポート8000でリッスンしている単一のPythonプロセスで小さなAPIを運用しています。トラフィックが増加し、1台のVMでは処理しきれなくなっており、セキュリティレビューでは、そのVMがパブリックIPを持ち、独自のTLS証明書を実行していることが指摘されました。
前面にリバースプロキシを配置することにしたとします。レイアウトをスケッチしてください:DNSはどこを指すか、証明書はどこに存在するか、ロードはどのようにバックエンドに到達するか、そして元のVMは何が変わるか。
誰も気づかずにコンポーネントを差し替える
間接性が自由をもたらす
古いコンピュータ科学の格言がある。「すべての問題は、間接性のレイヤーを1つ追加することで解決できる(間接性のレイヤーが多すぎるという問題を除いて)」。リバースプロキシは、分散システムにおいて最も有用な間接性の一つである。
これによって得られるもの:
- バックエンドの交換可能性。 アプリケーションをPythonからGoに移行する?データセンターを別の場所に移行する?ダウンタイムゼロで新しいバージョンをリリースする?これらすべてが、安定したパブリックアドレスの背後で実現される。ユーザーにとっては何も変わらない。
- 独立したスケーリング。 プロキシ層は、TLSレイヤーでの帯域幅とCPUに基づいてスケーリングされる。バックエンド層は、アプリケーションの処理量に基づいてスケーリングされる。異なるマシン上で動作しているため、それぞれが独自の軸で拡張される。
- 障害の封じ込め。 バックエンドでの不良なデプロイでも、パブリックアドレスがダウンすることはない。プロキシは稼働し続けたままで、修正を適用するかロールバックすればよい。バックエンドが回復すれば、世界は再接続される。
- 横断的な関心事の一元管理。 レート制限、ジオブロッキング、リクエストログ、ヘッダー書き換え、キャッシュ、レスポンス圧縮:これらはすべてプロキシ上で処理される。バックエンドのコードはアプリケーションに集中できる。
プロキシがない場合、これらの問題のすべてがアプリケーションプロセス内に存在しなければならない。プロキシがある場合、これらは1つのチームが管理する1つのレイヤーに存在する。
コスト:運用すべきレイヤーが1つ増えること。成熟したチームは、プロキシ層自体がステートレスであり水平スケーリングに対応しているため、このコストを受け入れる。1つのプロキシを2つに置き換える際に調整は不要である。
プロキシ経由のブルー/グリーンデプロイ
あなたのチームは、リバースプロキシの背後にある3台のバックエンドVM(ブループール)でAPIのバージョン1を運用しています。何か問題が発生した場合、30秒以内にロールバックできる形でバージョン2をデプロイしたいと考えています。
バージョン2を実行する3台の新しいバックエンドVM(グリーンプール)を、ブループールと並列で起動しますが、まだこれらへのトラフィックをルーティングしません。
ブラウザからバックエンドへ、そして戻ってくるまで
リクエストをエンドツーエンドで追跡する
バックエンドプール前面に配置されたリバースプロキシを介して、https://api.example.com/users/42 への単一のHTTPS GETリクエストを追跡します。
ホップ1: DNS解決。 ブラウザがリゾルバに api.example.com の解決を要求します。リゾルバはプロキシの公開IPアドレス(例: 203.0.113.10)を返します。ブラウザは 203.0.113.10:443 に対してTCP接続を開きます。
ホップ2: TLSハンドシェイク。 プロキシは api.example.com 用の証明書を提示します。ブラウザは証明書を検証し、両者はセッションキーに合意し、暗号化チャネルが確立されます。
ホップ3: TLS内のHTTPリクエスト。 ブラウザは GET /users/42 HTTP/1.1\nHost: api.example.com\n... を送信します。プロキシはリクエストのバイト列を復号化します。
ホップ4: バックエンドの選択。 プロキシは api.example.com のアップストリームプールを参照し、ロードバランシング戦略に基づいて1つのバックエンド(例: 10.0.0.21:8000)を選択します。
ホップ5: アップストリームリクエスト。 プロキシは 10.0.0.21:8000 に対してプレーンHTTP接続を開く(または再利用)し、リクエストを転送します。プロキシは途中でヘッダーを書き換える場合があります:X-Forwarded-For: <client-ip> を追加し、Host: を正しく設定し、Connection のようなホップバイホップヘッダーを削除します。
ホップ6: バックエンド処理。 バックエンドアプリケーションはリクエストを読み取り、データベースをクエリし、JSONレスポンスを構築します。
ホップ7: アップストリームレスポンス。 バックエンドはレスポンスをプレーンHTTPとしてプロキシに返します。
ホップ8: エッジレスポンス。 プロキシはレスポンスを書き換えたり圧縮したりし、TLSセッションを通じて暗号化してブラウザに送信します。
ホップ9: 接続のライフサイクル。 TLSセッションは通常、次のリクエストのために開いたままになります(HTTP/2は1つの接続で多数のリクエストを多重化します)。プロキシからバックエンドへの接続は、再利用のためにプールされることがよくあります。
すべてのパブリックなWebサービスは、この形状の何らかの変形版に従っています。各ホップを理解することで、レイテンシがどこで蓄積するか、ログをどこに配置すべきか、そして障害がどこに隠れる可能性があるかを推論できます。
時間はどこに消えたのか?
ユーザーからAPIが遅いという苦情が届いた。測定したところ、リクエストの往復時間(end to end)は850 msだった。バックエンドのサーバーログを見ると、アプリケーションがリクエストを処理した時間は40 msだった。プロキシのログでは、プロキシ側の処理(TLSハンドシェイク+ルーティング+レスポンス書き込み)に50 msかかっていた。
新しいサービス用の最小限のエッジを設計する
統合
フォワードプロキシとリバースプロキシの違い、リバースプロキシが同時に処理する3つの役割、オリジンを隠蔽することが変更時にどのように利益をもたらすか、そしてリクエストがエッジをホップごとにどのように流れるかを学習しました。
では、それを応用しましょう。
小規模なチームが、notes.example.com という新しいサービスをリリースする計画を立てています。ユーザーは個人ノートの読み書きを行います。チームはリリース時に2台のバックエンドVMを運用し、今後1年以内に10台までスケールすることを想定しています。彼らはユーザー向けにHTTPS、新バージョンの段階的なロールアウト、そしてバックエンドIPの公開回避を希望しています。
このコースの次のステップ
このコースの次のステップ
このレッスンでは、エッジレイヤーの構造を確立しました。このコースの残り4つのレッスンはその上に構築されます:
- ステートレスな水平スケーリング: プロキシティア(およびその背後のバックエンド)がなぜ安価に複製可能であるか、およびサージ時のレプリカ数を決定するための計算式。
- イングレス/エグレスの分離: 受信トラフィックと送信トラフィックの両方を処理する単一のプロキシボックスが、なぜ驚くべき形で最終的に失敗するのか、そしてレイヤーをどのように分離するか。
- 障害モードと影響範囲(ブラスト・レディウス):単一の設定変更がどのようにして障害に波及するか、および再発を防ぐための非責任意向のアクションアイテムの書き方。
- オブザーバビリティとキャパシティ:ユーザーが気づく前に問題を検知するために、エッジで何を測定すべきか。
各レッスンは独立しています。これらを組み合わせることで、ウェブスケールのフリート(サーバー群)に対する実用的なメンタルモデルが得られます。
関連レッスン:geometry_of_proxies_and_origins では、本レッスンの内容を有向グラフとして再構築し、グラフ理論がリクエストパスについて何を教えてくれるかを探究します。
よくできました。次へ進みましょう。