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

Preview URLs allow you to preview new versions of your project without deploying it to production.

Cloudflare Docs
Preview URLs

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を挟まずに高速かつ安全に通信できます。

Service bindings

Facilitate Worker-to-Worker communication.

Cloudflare Docs
Service bindings

実装例

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

ディレクトリ構成

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分離 のトレードオフまとめ参考リンク