Cloudflare Workers におけるマルチサイトアーキテクチャ設計
OSS ライブラリのドキュメントを公開したい、社内ダッシュボードを段階的に統合したい、ポートフォリオサイトの一部だけを別アプリケーションにしたいなど、同一ドメインで複数のサイトやアプリケーションを束ねたくなる場面は、運用 […]
目次
OSS ライブラリのドキュメントを公開したい、社内ダッシュボードを段階的に統合したい、ポートフォリオサイトの一部だけを別アプリケーションにしたいなど、同一ドメインで複数のサイトやアプリケーションを束ねたくなる場面は、運用していると意外と多く発生します。
Cloudflare Workers でこれをやろうとすると、Routes、Service Bindings、マイクロフロントエンド構成と選択肢が並び、それぞれ設計上のトレードオフが異なります。本稿では3つのパターンを整理し、ユースケースに応じて何を選ぶべきかを述べます。
静的ファイルの配信はWorkers Static Assets を起点にする
新規プロジェクトであれば、静的ファイル配信は Workers Static Assets を採用するのが現時点の標準です。Cloudflare Pages はサポートが続いていますが、新機能の投資は Workers 側に集約されると公式に表明されています(Migrate from Pages to Workers)。Pages は「維持はするが、これからは Workers」というポジションだと理解しておくのが正確です。
基本的な構成は以下のとおりです。wrangler.jsonc が現在の Wrangler 推奨形式で、wrangler init 系コマンドはこの形式を自動生成します。
// wrangler.jsonc
{
"name": "my-site",
"main": "src/index.ts",
"compatibility_date": "2026-04-29",
"assets": {
"directory": "./dist/",
"binding": "ASSETS"
}
}
Worker コードからは env.ASSETS.fetch(request) を呼ぶことで、静的ファイルを配信できます。binding を省略すると Worker から ASSETS を参照できなくなる点だけ注意してください(静的ファイルのみを配信する場合は省略してもよい仕様です)。
この前提を踏まえたうえで、複数サイトの統合方法を見ていきます。
パターン 1: Routes による振り分け
最もシンプルかつ高速なのが、Cloudflare の Routes 機能による振り分けです。
構成概要
同一ドメインの異なるパスを、それぞれ独立した Worker にマッピングします。たとえばポートフォリオサイトと OSS ライブラリのドキュメントを同一ドメインで公開する場合、以下のように分割します。
example.com/* → Portfolio Worker
example.com/docs/sdk/* → SDK Documentation Worker
example.com/docs/cli/* → CLI Documentation Worker
Cloudflare の Routes は、複数のパターンが該当した場合に「より具体的な(specific)パターン」を優先します。具体的とは、ワイルドカード(*)が少なく、より長く明示的に書かれているパターンを指します。/docs/sdk/* と /* の両方が候補になる場合、前者が選ばれます(Routes ドキュメント)。「最長一致」と表現される場面もありますが、公式仕様としては specificity ベースだと理解しておくほうが安全です。
実装
各ドキュメントサイトは独立した Worker としてデプロイします。TypeDoc や VitePress などで生成した静的サイトであれば、Worker 本体は最小限のコードで済みます。
// docs-sdk/src/index.ts
export default {
async fetch(request: Request, env: Env): Promise<Response> {
return env.ASSETS.fetch(request);
}
};
親サイトのコードに一切手を加える必要がない点が、このアプローチの最大の利点です。子サイトのデプロイパイプラインは独立して回せて、親サイトに影響を与えません。
適用すべきケース
このパターンは以下を満たす場合に最適です。
- 配信するコンテンツが静的ファイルまたは SPA である
- 親子間でランタイムの連携(認証情報の共有・共通ヘッダー注入など)が不要である
- セッション共有が必要ない
典型的なユースケースは、TypeDoc による API 仕様書、Storybook のコンポーネントカタログ、VitePress による技術ドキュメント、デモ用 SPA などです。
パターン 2: Service Binding によるオーケストレーション
親 Worker から子 Worker を呼び出す Service Binding を使うと、より複雑な要件にも対応できます。
構成概要
親 Worker がリクエストをインターセプトし、Service Binding 経由で子 Worker を呼び出します。
// portfolio/src/index.ts
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
if (url.pathname.startsWith('/docs/sdk/')) {
return env.DOCS_SDK.fetch(request);
}
return env.ASSETS.fetch(request);
}
};
wrangler.jsonc では、子 Worker へのバインディングを宣言します。
{
"name": "portfolio",
"main": "src/index.ts",
"compatibility_date": "2026-04-29",
"assets": {
"directory": "./dist/",
"binding": "ASSETS"
},
"services": [
{
"binding": "DOCS_SDK",
"service": "docs-sdk"
}
]
}
Routes との違い
Service Binding では、親 Worker がリクエストとレスポンスの両方を加工できます。認証情報の注入、共通ヘッダーの付与、レスポンスの後処理といった横断的関心事を親に集約できる点が Routes との決定的な違いです。
一方で、Worker 呼び出しが1段増える分のレイテンシは避けられません。静的ファイルの配信だけが目的であれば、この追加コストを正当化できるケースは限定的で、Routes のほうが適しています。
パターン 3: マイクロフロントエンド構成
Service Binding をさらに発展させると、マイクロフロントエンド(MFE)の原則を適用したアーキテクチャになります。
設計思想
MFE 構成では、各サブアプリケーションが独立したデプロイ単位となり、親サイトが App Shell として機能します。App Shell が担う責務は次の3つです。
リクエストパスに基づいて適切な子 Worker を選択するルーティング決定。認証情報やユーザー設定を子 Worker に伝播させるコンテキスト注入。ヘッダーやフッターといった共通要素をサーバーサイドで組み立てる共通 UI 層。
実装例
App Shell では、MFE レジストリを保持してリクエストをディスパッチします。
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const mfeRegistry: Record<string, keyof Env> = {
'/app/dashboard/': 'DASHBOARD_MFE',
'/app/settings/': 'SETTINGS_MFE',
'/app/analytics/': 'ANALYTICS_MFE',
};
for (const [basePath, binding] of Object.entries(mfeRegistry)) {
if (url.pathname.startsWith(basePath)) {
const mfeContext = {
user: await authenticateUser(request, env),
basePath: basePath,
theme: getUserTheme(request),
};
const mfeRequest = new Request(request, {
headers: {
...Object.fromEntries(request.headers),
'x-mfe-context': JSON.stringify(mfeContext),
},
});
const mfeWorker = env[binding] as Fetcher;
const response = await mfeWorker.fetch(mfeRequest);
return injectCommonLayout(response);
}
}
return env.ASSETS.fetch(request);
}
};
子 Worker 側では、注入されたコンテキストを参照してレンダリングします。
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const contextHeader = request.headers.get('x-mfe-context');
const context = contextHeader ? JSON.parse(contextHeader) : {};
// basePath を考慮したルーティング
const url = new URL(request.url);
const internalPath = url.pathname.replace(context.basePath, '/');
return handleRequest(internalPath, context, env);
}
};
適用すべきケース
MFE 構成は、以下のいずれかが要件として明確にある場合に検討します。
- 複数チームが独立して開発・デプロイを行う体制になっている
- 認証やセッション管理を一元化したい(各 MFE が認証ロジックを個別実装したくない)
- MFE ごとに異なるフレームワーク(Next.js / Remix など)を採用する必要がある
「将来こうなるかも」で導入すると、得られる柔軟性に対して App Shell の保守コストが見合いません。
どのパターンを選ぶか
選択の判断基準を整理します。
まず、配信するコンテンツが完全に静的かを確認します。TypeDoc や Storybook、VitePress でビルドされた静的サイトであれば、Routes による振り分けで十分です。Service Binding を導入する理由はありません。
次に、親子間でランタイム連携が必要かを検討します。認証情報の共有、共通ヘッダーの注入、レスポンス加工といった要件が一つもなければ、ここでも Routes が最適解になります。
動的なサーバーサイド処理があり、かつ複数アプリケーション間で共通の関心事がある場合に、初めて Service Binding を検討します。さらに「複数チームの独立デプロイ」「認証一元化」「フレームワーク多様性」のいずれかが本当に必要であれば、MFE 構成まで進みます。
重要なのは、アーキテクチャの複雑性は常にコストだという認識です。MFE 構成は設計上のエレガンスがありますが、静的サイト配信に対しては明らかに過剰です。パフォーマンス・保守性・チーム構成を見たうえで、要件を満たす最小の構成を選んでください。
まとめ
Cloudflare Workers でマルチサイトを構成する際の選択肢は、Routes による単純な振り分けから MFE 構成まで段階的に存在します。技術的な可能性と実際の要件は分けて考え、ユースケースに応じた最小限の複雑性を選ぶことが、長期的な保守性とパフォーマンスの両立につながります。
静的サイトには Routes、ランタイム連携が必要なら Service Binding、それ以上の組織的要件があれば MFE。この順序で必要性を確認していくと、多くのケースで適切なパターンを選べます。