LogoSaito Go's Blog
ホームキーワード検索プロフィール
LogoSaito Go's Blog

© 2026 Saito Go's Blog. All rights reserved.

ホームに戻る

Durable ObjectsをWorkersにバインドしてもPreview URLを生成させる方法

CloudflareWorkersDurable Objects
2026/08/09

はじめに

Cloudflare Workersでは、Pull Requestや検証用バージョンごとに個別の動作確認用URLを発行できる Preview URLs 機能が提供されています。しかし、Durable Objects(以下、DO)をWorkerにバインドして直接実装すると、仕様によりPreview URLが自動生成されなくなります。

この制限を回避し、DOによる状態管理を行いながらPreview URLを利用するには、HTTPリクエストを受ける公開WorkerとDOを扱う内部Workerを分離し、両者を Service Binding で接続する構成が有効です。

本記事では、この問題が発生する原因、解決するアーキテクチャ、具体的な実装コード、ローカル開発およびデプロイ手順、注意すべきトレードオフについて解説します。

Preview URLが生成されない理由

Cloudflare Workersの公式ドキュメントには、Preview URLの制限項目として以下のように明記されています。

Preview URLs are not generated for Workers that implement a Durable Object.

公式ドキュメントの制限一覧では、DO制限が将来的に解消されうる旨のニュアンスが含まれていますが、今後変更される可能性があります。

なぜ制限されているのか

Worker本体はステートレスですが、DOは特定のIDや名前に対して単一のインスタンスと永続化ストレージ(SQLite / Key-Value等)を維持するステートフルなコンポーネントです。

もしPreview URL経由でバージョンごとの一時的な検証環境から無制限にDOへアクセスできてしまうと、開発中コードによる本番ストレージの破綻やデータ汚染、バージョン間の状態不整合のリスクが生じます。そのため、Cloudflare側で「DOを実装(バインド)するWorker」に対してPreview URLの発行が一律制限されています。

※これは筆者の推測であり、Cloudflare公式が明言している理由ではありません。公式ドキュメントには制限事実のみが記載されています。

なお、公開Worker側の設定に durable_objects バインディング(外部参照の script_name を含む)が残っているだけでも制限対象と判定される場合があるため、公開WorkerからはDOバインディングを完全に排除する必要があります。

※筆者の検証による挙動であり、公式ドキュメントには明記されていません。

解決するアーキテクチャ

公開WorkerからDOバインディングを除去するため、システムを以下の2つのWorkerに役割分離します。

Browser / Client
      |
      v
public-api Worker  ── Service Binding ──>  state-worker
Preview URL利用可                           Durable Objectをバインド
                                           Preview URL生成不可
                                                  |
                                                  v
                                         Durable Object State
  • public-api(公開Worker): HTTPエンドポイント、ルーティング、認証・認可を担当します。DOを直接バインドせず、Service Bindingのみを定義するため Preview URLが生成されます。
  • state-worker(内部Worker): DOのクラス定義とストレージ操作を担当します。インターネットへ直接公開せず、public-api からの Service Binding 経由でのみ呼び出されます。
  • Service Binding: Worker間を内部ネットワークで直接接続する通信機構です。インターネット経由の公開URLを挟まずに高速かつ安全に通信できます。

実装例

ルームごとにカウンターを保持するシンプルなアプリケーションを例に実装します。

ディレクトリ構成

workers-app/
├── public-api/
│   ├── src/index.ts
│   └── wrangler.jsonc
└── state-worker/
    ├── src/index.ts
    └── wrangler.jsonc

内部Worker (state-worker)

DOクラスを定義し、バインドします。このWorkerにはPreview URLが生成されません。

state-worker/wrangler.jsonc

{
  "name": "state-worker",
  "main": "src/index.ts",
  "compatibility_date": "2026-08-01",
  "durable_objects": {
    "bindings": [
      {
        "name": "ROOM",
        "class_name": "RoomDO"
      }
    ]
  },
  "migrations": [
    {
      "tag": "v1",
      "new_classes": ["RoomDO"]
    }
  ]
}

state-worker/src/index.ts

import { DurableObject } from 'cloudflare:workers';

export interface Env {
  ROOM: DurableObjectNamespace;
}

export class RoomDO extends DurableObject {
  async fetch(request: Request): Promise<Response> {
    const url = new URL(request.url);

    if (request.method === "POST" && url.pathname === "/increment") {
      const current = (await this.ctx.storage.get<number>("count")) ?? 0;
      const count = current + 1;
      await this.ctx.storage.put("count", count);
      return Response.json({ count });
    }

    if (request.method === "GET" && url.pathname === "/count") {
      const count = (await this.ctx.storage.get<number>("count")) ?? 0;
      return Response.json({ count });
    }

    return new Response("Not Found", { status: 404 });
  }
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    const roomId = url.searchParams.get("roomId");

    if (!roomId) {
      return Response.json({ error: "roomId is required" }, { status: 400 });
    }

    const id = env.ROOM.idFromName(roomId);
    const stub = env.ROOM.get(id);
    return stub.fetch(request);
  },
};

公開Worker (public-api)

services に state-worker への Service Binding を指定します。durable_objects の設定は含めません。

public-api/wrangler.jsonc

{
  "name": "public-api",
  "main": "src/index.ts",
  "compatibility_date": "2026-08-01",
  "workers_dev": true,
  "preview_urls": true,
  "services": [
    {
      "binding": "STATE_SERVICE",
      "service": "state-worker"
    }
  ]
}

public-api/src/index.ts

export interface Env {
  STATE_SERVICE: Fetcher;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);

    if (!url.pathname.startsWith("/api/rooms/")) {
      return new Response("Not Found", { status: 404 });
    }

    const roomId = url.pathname.split("/")[3];
    if (!roomId) {
      return Response.json({ error: "roomId is required" }, { status: 400 });
    }

    // 内部通信用URLを構築して Service Binding 経由で呼び出し
    const targetPath = url.pathname.endsWith("/increment") ? "/increment" : "/count";
    const internalUrl = new URL(`https://state.internal${targetPath}`);
    internalUrl.searchParams.set("roomId", roomId);

    return env.STATE_SERVICE.fetch(
      new Request(internalUrl, {
        method: request.method,
        headers: { "content-type": "application/json" },
      })
    );
  },
};

ローカル開発環境での確認

Service Binding で接続された複数のWorkerをローカル開発環境で起動するには、wrangler dev コマンドに複数の設定ファイルを渡します。

npx wrangler dev \
  -c public-api/wrangler.jsonc \
  -c state-worker/wrangler.jsonc

先頭に指定した public-api がメインの受信用Worker(デフォルトで http://localhost:8787)として起動し、後ろの state-worker は内部Workerとしてローカルで連携動作します。

動作確認の例:

# カウントアップ
curl -X POST "http://localhost:8787/api/rooms/room-1/increment"

# カウント取得
curl "http://localhost:8787/api/rooms/room-1"

デプロイ手順と Preview URL の使用

デプロイ時は、呼び出し先となる内部Workerを先にデプロイし、その後に公開Workerをデプロイ・アップロードします。

# 1. 内部Workerをデプロイ
npx wrangler deploy -c state-worker/wrangler.jsonc

# 2. 公開Workerのバージョンをアップロード(Preview URLを生成)
npx wrangler versions upload -c public-api/wrangler.jsonc --preview-alias pr-100

public-api はDOを直接バインドしていないため、アップロード結果として pr-100-public-api.<subdomain>.workers.dev のようなPreview URLが正常に返されます。

【重要】データ汚染リスクと環境分離

Preview URLが生成可能になった際、最も注意すべきなのはデータの書き換えリスクです。

もしPreview URL用の public-api が、本番用の state-worker に接続されている場合、検証環境からのアクセスによって本番のDOデータが書き換えられるリスクが生じます。

対策:環境ごとに Service Binding の接続先を切り替える

wrangler.jsonc の env 設定を活用し、検証環境・ステージング環境では専用の state-worker-staging に接続するように設定します。

{
  "name": "public-api",
  "main": "src/index.ts",
  "compatibility_date": "2026-08-01",
  "preview_urls": true,
  "services": [
    {
      "binding": "STATE_SERVICE",
      "service": "state-worker" // 本番環境用
    }
  ],
  "env": {
    "staging": {
      "name": "public-api-staging",
      "services": [
        {
          "binding": "STATE_SERVICE",
          "service": "state-worker-staging" // 検証・ステージング用
        }
      ]
    }
  }
}

デプロイ時も --env staging オプションを活用し、本番環境と検証環境のデータストアを物理的に隔離してください。

直接バインド vs 別Worker分離 のトレードオフ

構成を分離することでメリットが得られる一方、運用上のコストや複雑さが増加します。以下のトレードオフを理解したうえで導入を検討してください。

評価項目 DOを単一Workerに直接バインド 別Worker分離構成(Service Binding)
Preview URL 生成不可(仕様上の制限) 生成可能(public-api側で利用可能)
構成のシンプルさ 単一プロジェクト・1つの設定で完結 2つのWorkerプロジェクトと接続管理が必要
アーキテクチャの関心分離 HTTP処理とDO状態管理が同居 API層(認証・ルーティング)と状態層が明確に分離
通信オーバーヘッド 同一コンテキスト内の直接呼出 Service Binding経由の呼び出し(オーバーヘッドは極めて軽微)
環境・データ分離の管理 1つのWorkerのみ管理 Preview/Staging環境用の内部Worker・DOデータの分離管理が必要
デプロイ・CI/CD順序 1ステップでデプロイ完了 内部Workerを先にデプロイしてから公開Workerをデプロイ

まとめ

  • 制限の原因: Cloudflare Workersでは、Durable Objectを直接実装・バインドするWorkerに対してステート保護のためPreview URLが生成されない。
  • 解決策: HTTP受付を担当する公開Worker (public-api) と、DOを管理する内部Worker (state-worker) に分離し、Service Bindingで連携させる。
  • 注意点: Preview URL経由のリクエストが本番のDOデータを書き換えないよう、ステージング用・検証用の内部Workerを用意して環境を分離する。

参考リンク

  • Preview URLs - Workers - Cloudflare Docs
  • Service bindings - Workers - Cloudflare Docs
  • Configuration - Wrangler · Cloudflare Docs
Saito Go

Saito Go

Blog Owner

大手製造業の現場で働きながら趣味でWeb系エンジニアやってます。

当サイトはアフィリエイト広告を使用しています。

目次

はじめにPreview URLが生成されない理由なぜ制限されているのか解決するアーキテクチャ実装例ディレクトリ構成内部Worker state-worker公開Worker public-apiローカル開発環境での確認デプロイ手順と Preview URL の使用【重要】データ汚染リスクと環境分離対策:環境ごとに Service Binding の接続先を切り替える直接バインド vs 別Worker分離 のトレードオフまとめ参考リンク