Cloudflareを入り口としたマルチテナントPaaSの設計

KamuiDashのマルチテナントPaaSを支えるエッジ設計。Cloudflareでドメイン解決、動的リクエストの中継、オリジン保護を担い、Worker・KV・Tunnelに処理を分けています。

KamuiDashでは、テナントごとのアプリケーションと独自ドメインを、ひとつのCloudflareエッジで受けています。
この記事では、利用者のリクエストが入口に届いてから、テナントアプリの応答として返るまでを順にたどります。

公開入口からテナントアプリまで

すべてのHTTPリクエストは、まずCloudflare Workerに届きます。WorkerはHostから対象アプリを解決し、動的アプリならCloudflare Tunnelを通じて、クラスタ内の内部ルーターとテナントアプリへ渡します。

入口のWorkerがドメイン解決と経路選択を持ち、実行が必要な動的リクエストだけをクラスタまで送ります。

エッジから配信先まで
利用者ブラウザ / APIクライアント
CLOUDFLARE EDGE
Workersドメイン解決・経路判定
エッジ応答一部のコンテンツはここで完結
Tunnel確立済みの非公開経路
PRIVATE ORIGIN
内部ルータールーティング文脈を検証し転送
テナントアプリ動的コンテンツを応答
同じ公開入口で、動的なリクエストだけをTunnel経由で非公開オリジンへ渡す概念図です。

1. WorkerでHostを解決する

Workerは受信したHostを正規化し、KVにあるドメインと内部アプリの対応表を引きます。独自ドメインもプラットフォームのサブドメインも、この時点では同じように「どのアプリを指すか」を解決する入力です。対応表にない外部ドメインは、クラスタへ流さずWorkerで404を返します。

アプリが特定できたら、Workerは公開種別を確認します。エッジで応答を完結させない場合は、動的アプリの実行先を引いてTunnelへ渡します。Workerが読むKVは、用途ごとに分けています。

Workerが参照するルーティング情報
ドメイン対応      Host            → 内部アプリ名
実行先レジストリ  内部アプリ名    → 動的アプリの実行先

ドメイン対応と動的アプリの実行先は、更新頻度も反映許容時間も異なるため、同じレコードにはしていません。

リクエストの分岐
  1. 01ホスト名を解決独自ドメインを内部アプリへ対応付ける
  2. 02公開種別を確認動的リクエストだけをクラスタへ送る
  3. 03動的な実行先を解決アプリの配置先からTunnel経路を選ぶ
  4. 04失敗を境界で正規化タイムアウト・上流エラー・追跡IDを一貫させる
Hostを起点にアプリを解決し、動的リクエストだけをTunnelへ渡します。

2. 動的リクエストはTunnelを通って内部ルーターへ進む

エッジで応答を完結させない場合、Workerはアプリと実行先の対応表をKVから読み、該当するTunnelの経路を選びます。そこで解決したアプリ識別子と実行先の文脈をリクエストへ付けてTunnelへ送ります。クライアントから届いた同名の情報は、そのまま使わずWorkerの解決結果で置き換えています。

転送時には、受信したリクエストヘッダーを基に新しい上流リクエストを組み立てます。その際にHostとルーティング文脈をWorkerの解決結果で明示的に設定し、リクエストIDも付与します。テナントの選択を、クライアントが送った任意のヘッダー値に委ねないためです。

Tunnelコネクタは外向きにCloudflareと接続しており、クラスタ側にインターネットから到達できる受信口を持ちません。Tunnelの先にある内部ルーターがWorkerから渡された文脈を受け取り、対象のテナントアプリへ転送します。

Workerから上流へのfetchはAbortControllerで時間を区切っています。接続不能・タイムアウト・上流のサーバーエラーは、入口でゲートウェイエラーに変換します。応答に含まれる内部向けのルーティング情報は削除し、アプリがCache-Controlを返さない動的応答には共有キャッシュしない既定値を設定します。WebSocketのアップグレードは、通常のHTTP応答と別にそのまま中継します。

動的リクエストの信頼境界
PUBLICクライアントHost / Path / Request headers
受信
EDGEWorkerHost正規化・KV参照・上流リクエストを再構成
解決済みの文脈
PRIVATE PATHTunnel外向きに確立された接続
コネクタのみ
CLUSTER内部ルーター対象テナントへ転送
クライアント入力をそのままルーティングに使わず、Workerで解決した文脈をTunnel経由で内部ルーターへ渡します。

3. Tunnelの先でも入口を限定する

内部ルーターはWorkerが付与したルーティング情報を使うため、Tunnelを通ったリクエストだけを受け取る構成にしています。KamuiDashでは、内部ルーターへのIngressをTunnelコネクタのPodに限定するNetworkPolicyを適用しています。

このポリシーでは、Tunnelコネクタだけを送信元として許可しています。これにより、同じクラスタ内の別のワークロードが内部ルーターへ直接到達し、Workerが解決するはずの文脈を偽装する経路を閉じています。公開入口でのHost解決と、クラスタ内での受信元制限を、同じリクエスト経路の両端に置いています。

4. ドメイン登録とデプロイで変わるもの

独自ドメインを追加するときは、Cloudflareのカスタムドメイン機能で所有確認と証明書発行を通した後に、ドメインと内部アプリの対応を登録します。削除時には、その対応とドメイン側の状態を順に解除します。

動的アプリの移動では、実行先レジストリの対応が変わります。ドメイン対応と実行先は更新頻度が異なるため、別々にキャッシュ時間を設定しています。

KVは書き込みによってすべてのエッジキャッシュが即時に消えるストアではなく、未登録という結果も短時間残ります。登録や移動の処理では、DNS・証明書・KV反映までを含めて状態を追います。WorkerはリクエストIDとエッジ側の追跡情報を構造化ログに残し、認証情報やCookieは記録しません。Tunnel、内部ルーター、テナントアプリまでを、同じリクエストIDで追跡します。

設定変更が配信経路へ反映されるまで
独自ドメインの追加
所有確認・証明書ドメイン対応を更新KVキャッシュの反映Workerが新しいHostを解決
動的アプリの移動
移動先を準備実行先レジストリを更新KVキャッシュの反映Workerが新しい経路を選択
登録・移動の完了は、KVへの書き込みだけではなく、エッジで新しい対応が解決されるところまで含めて扱います。

この経路でCloudflareに任せていること

独自ドメインでは、カスタムホスト名の所有確認と証明書の発行・更新をCloudflareに任せています。KamuiDash側で持つのは、そのドメインをどのテナントアプリへ結び付けるかという対応情報です。ドメインごとに証明書を発行し、公開入口を追加する処理をアプリケーション基盤で個別に持たずに済んでいます。

Workerはすべての公開リクエストが通る共通の実行地点です。Hostの解決、上流リクエストの組み立て、エラー応答の正規化、リクエストIDの付与をここでまとめています。テナントアプリごとに同じ入口処理を実装する必要がありません。

Tunnelでは、クラスタ側のコネクタが外向きに接続を張ります。そのため、テナントアプリや内部ルーターにインターネットから直接到達するための公開受信口を持たせずに、Workerからクラスタへリクエストを渡せています。受信元を限定するNetworkPolicyはKamuiDash側で持ち、Cloudflareからクラスタへ入る経路はTunnelに集約しています。

まとめ

KamuiDashの公開経路では、次の三つをCloudflareの入口に集約しています。

Cloudflareを入り口に置くことで、テナントごとに公開経路を持たせず、Worker、Tunnel、内部ルーターの役割を分けています。

KamuiDashの設計思想や、GitHubからデプロイする基本的な流れについてはKamuiDashを使う理由で説明しています。