RSSAmplifier

Rick Cogley · Mar 23, 2026

Cloudflare PagesからWorkersへの移行ガイド(2026年版)

0
Sign in to vote or save

Rick Cogley · Rick Cogley

CloudflareはPagesをWorkersに統合しつつある。Pagesが明日なくなるわけではないが、新機能はすべてWorkers側にのみ追加されている。筆者は2026年初頭に全PagesプロジェクトをWorkersへ移行し、その過程でつまずいたポイントをまとめた。

Cloudflare Pagesは廃止されるのか?

正確にはそうではない。RedditやHacker Newsでは「Pagesは非推奨」という声が多いが、実態は少し違う。Workersテックリードの Kenton Varda 氏は「Pages固有の機能をすべて汎用的なWorkers機能に変えていく」と述べている。つまり、製品が廃止されるのではなく、吸収されるのだ。

しかしシグナルは明確だ。新機能はWorkersが先行(もしくはWorkers限定)で提供される。Secrets Store、Workflows、Containers、Dynamic Workers、いずれもWorkers専用だ。Pagesはメンテナンス更新にとどまっている。公式の互換性マトリクスを見ると、WorkersはPagesのすべてをカバーしたうえで、Pages非対応の機能リストが拡大し続けていることがわかる。

筆者は2026年1月から3月にかけて全Pagesプロジェクトを移行した。現時点の機能差は以下の通りだ。

機能 Pages Workers
静的アセット
サーバーサイドレンダリング
Durable Objects 別途Workerが必要 ネイティブ対応
Cronトリガー
Queue Consumer
Email Workers(受信)
画像リサイズバインディング
レート制限
Workers Logs 基本のみ フルオブザーバビリティ
Tail Workers
ソースマップ
段階的デプロイ
リモート開発 --remote
Smart Placement
Secrets Store (複数Worker間で共有)

Durable Objects、スケジュールタスク、本番環境のオブザーバビリティが必要なら、強制移行を待たず今のうちに移行するのが得策だ。

これらの機能が実際に何を可能にするか

上の表は機能名の一覧だが、具体的に何が構築できるのか。要点を解説する。

Durable Objects:ステートフルなエッジコンピューティング

強い整合性を持つステートフルストレージをエッジで提供する。リクエスト間で状態を保持する小さなシングルスレッドサーバーのようなもので、WebSocket接続管理、共同編集エディタ、グローバルに動作するレート制限などに活用できる。

実例として、リアルタイム共同編集機能を構築する場合を考えてみよう。Durable Objectsがなければ中央集権型のWebSocketサーバーが必要になる。Durable Objectsを使えば、各ドキュメントが独自のインスタンスを持ち、接続ユーザーの管理、編集の調整、状態の永続化をすべてエッジで処理できる。

// 各チャットルームが独自のDurable Objectインスタンスを持つ
export class ChatRoom {
  private connections: WebSocket[] = [];

  async fetch(request: Request) {
    const [client, server] = Object.values(new WebSocketPair());
    this.connections.push(server);
    server.accept();

    server.addEventListener('message', (event) => {
      // このルーム内の全接続クライアントにブロードキャスト
      this.connections.forEach((ws) => ws.send(event.data));
    });

    return new Response(null, { status: 101, webSocket: client });
  }
}

Durable Objectsドキュメント

Cronトリガー:インフラ不要のスケジュールタスク

Workerをスケジュールで実行できる。毎分、毎時、毎日、カスタムcron式など。外部スケジューラ不要、常時起動サーバー不要で、自動的にスケールする。

日次ダイジェストメール、キャッシュウォーミング、データ同期、クリーンアップジョブ、動的データからの静的レポート生成などに使える。

{
  "triggers": {
    "crons": [
      "0 0 * * *", // 毎日UTC 0時
      "*/15 * * * *", // 15分ごと
      "0 9 * * 1", // 毎週月曜UTC 9時
    ],
  },
}
export default {
  async scheduled(event: ScheduledEvent, env: Env) {
    // HTTPリクエストへの応答ではなく、スケジュールに従って実行される
    await env.DB.prepare('DELETE FROM sessions WHERE expires_at < ?').bind(Date.now()).run();
  },
};

Cronトリガードキュメント

Queue Consumer:信頼性の高いバックグラウンド処理

Cloudflare Queuesを使うと、リクエスト処理と重い処理を分離できる。Workerは即座にレスポンスを返し、バックグラウンドで非同期処理が行われる。自動リトライとデッドレター処理も対応している。

実例:ユーザーが画像をアップロード → 即座にレスポンス → キューが画像を処理(リサイズ、分析、保存) → タイムアウトの心配なし、ユーザーを待たせない。

// Producer:ワークをキューに入れる
export default {
  async fetch(request: Request, env: Env) {
    const image = await request.arrayBuffer();
    await env.IMAGE_QUEUE.send({ image, userId: 'abc123' });
    return new Response('Processing started', { status: 202 });
  },
};

// Consumer:バックグラウンドで処理
export default {
  async queue(batch: MessageBatch<ImageJob>, env: Env) {
    for (const message of batch.messages) {
      await processImage(message.body);
      message.ack();
    }
  },
};

Queuesドキュメント

Email Workers:プログラマブルな受信メール処理

受信メールをWorkerで受信、パース、ルーティングできる。これはトランザクションメール送信用ではない(送信には Resend、Maileroo、SES などの外部サービスを fetch で呼ぶ)。Email Workersは受信側を担当する。Cloudflareが受信メールをWorkerにルーティングし、処理方法は自由に決められる。

サポートチケットの受信メール解析、メールからタスクへの自動変換、カスタム転送ルール、スパムフィルタリングなどに使える。

export default {
  async email(message: EmailMessage, env: Env) {
    // 受信メールを解析してサポートチケットを作成
    const ticket = {
      from: message.from,
      subject: message.headers.get('subject'),
      body: await new Response(message.raw).text(),
    };
    await env.DB.prepare('INSERT INTO tickets ...').bind(ticket).run();

    // 緊急の場合はチームに転送
    if (ticket.subject.includes('[URGENT]')) {
      await message.forward('team@example.com');
    }
  },
};

Email Workersドキュメント

画像リサイズバインディング:オンデマンドの画像変換

事前にバリアントを生成せず、オンザフライで画像を変換する。リサイズ、クロップ、フォーマット変換、最適化をエッジでキャッシュ付きで実行できる。

実例:単一のソース画像からレスポンシブ画像を配信。/image.jpg?w=400 をリクエストすると、400pxのWebPが自動で返される。

export default {
  async fetch(request: Request, env: Env) {
    const url = new URL(request.url);
    const width = url.searchParams.get('w');

    // 取得と変換を1回の操作で実行
    return fetch(url.origin + '/original.jpg', {
      cf: {
        image: {
          width: parseInt(width),
          format: 'webp',
          quality: 85,
        },
      },
    });
  },
};

画像リサイズドキュメント

レート制限:APIの保護

外部サービスなしの組み込みレート制限。IP、APIキー、カスタム識別子ごとにスライディングウィンドウで制限を設定できる。

認証エンドポイントの保護、APIクォータ、コストの高い操作の悪用防止に使える。

export default {
  async fetch(request: Request, env: Env) {
    const { success } = await env.RATE_LIMITER.limit({ key: getClientIP(request) });

    if (!success) {
      return new Response('Rate limit exceeded', { status: 429 });
    }

    return handleRequest(request);
  },
};

レート制限ドキュメント

フルオブザーバビリティ:ログ、トレース、メトリクス

永続ログ、Service Binding間の分散トレース、リアルタイムのログストリーミング、詳細な分析機能を備えたプロダクション品質のデバッグ環境だ。

Pagesは基本的なリクエストログのみだが、Workersは以下を提供する。

  • 永続ログ:リアルタイムだけでなく、ダッシュボードで過去のログを検索可能
  • 分散トレース:Service Bindingで接続された複数Worker間でリクエストを追跡
  • 呼び出しログ:すべての console.log、未キャッチ例外、サブリクエストを確認
  • カスタムメトリクス:システムメトリクスとビジネスメトリクスを併せて追跡

Workersオブザーバビリティドキュメント

Tail Workers:リアルタイムログ処理

Workerのログを別のWorkerにストリーミングし、リアルタイム処理を行う。カスタムアラート、ログ集約、分析パイプラインの構築に使える。

実例:エラーをSlackに送信、ログをSIEMに集約、カスタムダッシュボードの構築。

// Tail Workerが他のWorkerからログを受信
export default {
  async tail(events: TraceItem[]) {
    const errors = events.filter((e) => e.outcome === 'exception');
    if (errors.length > 0) {
      await fetch('https://hooks.slack.com/...', {
        method: 'POST',
        body: JSON.stringify({ text: `${errors.length} errors detected!` }),
      });
    }
  },
};

Tail Workersドキュメント

ソースマップ:本番エラーのデバッグ

デプロイ時にソースマップをアップロードすると、エラースタックトレースでMinify前のファイル名と行番号が表示される。

本番環境が午前3時に壊れたとき、index.js:1:28456 ではなく src/auth/validate.ts:47 と表示されるほうがはるかに助かる。

ソースマップドキュメント

段階的デプロイ:安全なロールアウト

新バージョンを段階的にロールアウトできる。トラフィックの1%を新バージョンに送り、エラーを監視してから徐々に増やす。問題が発生した場合は自動ロールバックされる。

実例:リスクのある変更をユーザーの5%にデプロイし、エラー率を確認してから100%に展開する。

# トラフィックの10%に新バージョンをデプロイ
npx wrangler versions deploy --percentage 10

# メトリクスが問題なければ増やす
npx wrangler versions deploy --percentage 50

# 全展開
npx wrangler versions deploy --percentage 100

段階的デプロイドキュメント

リモート開発:本番バインディングでテスト

wrangler dev --remote を実行すると、ローカル開発中にCloudflareのインフラ上でWorkerが動作する。ローカルエミュレータではなく、実際のD1データベース、KV名前空間、Durable Objectsに対してコードが実行される。

ローカルエミュレーションは優秀だが、実際の本番データでテストしたい場合や、実インフラでのみ発生する問題をデバッグしたい場合に便利だ。

リモート開発ドキュメント

Smart Placement:自動的なレイテンシ最適化

Cloudflareが自動的にWorkerをバックエンドサービス(データベース、API)の近くで実行する。リクエストのほとんどを中央集権型バックエンドとの通信に費やすWorkerに最適だ。

Workerがリクエストごとに us-east-1 のデータベースを呼び出す場合、Smart Placementはユーザーの近くではなくデータベースの近くでWorkerを実行し、データベースコールのラウンドトリップレイテンシを削減する。

{
  "placement": {
    "mode": "smart",
  },
}

Smart Placementドキュメント

Secrets Store:一元的なシークレット管理

複数のWorker間でシークレットを重複なく共有できる。シークレットを1回更新するだけで、使用しているすべてのWorkerに新しい値が反映される。

APIキー、データベース認証情報、署名シークレットなど、アーキテクチャ内の複数Workerで使用するものに適している。

Secrets Storeドキュメント


アーキテクチャ概要

主な構造変更は以下の通りだ。

flowchart TB
    subgraph before["Pagesアーキテクチャ"]
        B1[Git Push] --> B2[Pagesビルド]
        B2 --> B3[Pagesデプロイ]
        B3 --> B4[pages.dev URL]
        B3 --> B5[カスタムドメイン]
        B3 -.-> B6[Durable Objects用<br/>別Worker必要]
    end
    subgraph after["Workersアーキテクチャ"]
        A1[Git Push] --> A2[ビルド]
        A2 --> A3[Workersデプロイ]
        A3 --> A4[workers.dev URL]
        A3 --> A5[カスタムドメイン]
        A3 --> A6[Durable Objects<br/>Cron Triggers<br/>Queues<br/>Email Workers]
        A3 --> A7[Smart Placement]
        A3 --> A8[完全なオブザーバビリティ]
    end
    before -.->|移行| after
    style B6 stroke-dasharray: 5 5
    style A6 fill:#e1f5fe
    style A7 fill:#e1f5fe
    style A8 fill:#e1f5fe

Workersではすべてが1つのデプロイに集約される。Durable Objectsを使うためだけに別のWorkerをメンテナンスする必要がなくなる。

移行前のアセスメント

作業を始める前に、プロジェクトを監査しよう。

バンドルサイズの確認

Workersには10MBの圧縮サイズ制限がある。バンドルを分析しよう。

# Viteベースのプロジェクト
npx vite-bundle-visualizer

# ソースマップがあるプロジェクト全般
npx source-map-explorer dist/**/*.js

制限を超えている場合は、移行前に最適化が必要だ。

Node.js API互換性

Workersはworkerd上で動作する。CloudflareのオープンソースC++ランタイムで、V8(Chromeと同じJSエンジン)を組み込んでいるが、Node.jsレイヤーは存在しない。ファイルシステム、永続プロセス、生ソケットはない。リクエストごとに独自のアイソレートが1ミリ秒未満で起動し、レスポンス送信後に終了する。これがWorkersを高速にしている理由だが、一部のNode.js APIが利用できない理由でもある。

幸い、compatibility_flags"nodejs_compat"(最近の互換日付では "nodejs_compat_v2")を追加すると、一般的なNode.js APIのポリフィルが有効になる。Buffercryptostreampath などだ。多くのnpmパッケージはこのフラグで動作する。

{
  "compatibility_date": "2026-03-01",
  "compatibility_flags": ["nodejs_compat"]  // Node.js APIポリフィルを有効化
}

互換フラグを有効にしても動作しないもの:

Node.js Workers代替手段
fs モジュール R2/KVからfetch、またはフレームワークのサーバーユーティリティ
process.env fetchハンドラの env パラメータ、またはフレームワークバインディング
child_process 代替なし。Service BindingsまたはQueuesを使用
net / dgram 生ソケット不可。fetch またはWebSocketsを使用

DNS要件

Workersカスタムドメインには、Cloudflareマネージドネームサーバーが必要だ。Pagesと異なり、外部DNSプロバイダは使用できない。作業を進める前に、ドメインのネームサーバーがCloudflareに設定されていることを確認しよう。

移行プロセスの概要

flowchart TB
    subgraph prep["A: 準備"]
        direction TB
        P1[バンドルサイズ確認]
        P2[Node.js API確認]
        P3[環境変数の棚卸し]
    end
    subgraph config["B: 設定"]
        direction TB
        C1[wrangler.jsonc更新]
        C2[フレームワークアダプタ更新]
        C3[.assetsignore作成]
    end
    subgraph test["C: テスト"]
        direction TB
        T1[wrangler devでローカル確認]
        T2[workers.devにデプロイ]
        T3[動作検証]
    end
    subgraph switch["D: ドメイン切替"]
        direction TB
        S1[Pages APIから削除]
        S2[Workers routes APIに追加]
        S3[HTTPS確認]
    end
    subgraph cleanup["E: クリーンアップ"]
        direction TB
        CL1[旧デプロイメント削除]
        CL2[Pagesプロジェクト削除]
        CL3[CI/CD更新]
    end
    prep --> config
    config --> test
    test --> switch
    switch --> cleanup

環境変数のインベントリ

Pagesでは「production」と「preview」の環境が分かれている。すべての変数とシークレットを記録しておこう。Workersで再作成する必要がある。

コア移行手順

設定の変換

最大の変更点は wrangler.jsonc(または wrangler.toml)だ。マッピングは以下の通り。

flowchart LR
    subgraph pages["Pages設定"]
        P1["pages_build_output_dir"]
        P2["暗黙的な404処理"]
        P3["暗黙的なアセット配信"]
    end
    subgraph workers["Workers設定"]
        W1["assets.directory"]
        W2["assets.not_found_handling"]
        W3["assets.binding + main"]
        W4["compatibility_date"]
    end
    P1 -->|変換| W1
    P2 -->|変換| W2
    P3 -->|変換| W3
    pages -->|追加| W4
    style W4 fill:#c8e6c9

Pages設定:

{
  "name": "my-project",
  "pages_build_output_dir": "./dist/client/",
}

Workers設定:

{
  "name": "my-project",
  "compatibility_date": "2026-03-01",
  "compatibility_flags": ["nodejs_compat"],
  "main": "./dist/server/index.js",
  "assets": {
    "directory": "./dist/client/",
    "binding": "ASSETS",
    "not_found_handling": "single-page-application",
  },
}

変更点:

  • pages_build_output_dirassets.directory に変わる
  • サーバーエントリポイントを指す main を追加
  • compatibility_date を追加(Workersでは必須。最新機能を使うには6ヶ月以内の日付を設定)
  • Node.js APIを期待するnpmパッケージ用に compatibility_flags: ["nodejs_compat"] を追加
  • 404ハンドリングを明示的に設定(single-page-application または 404-page

静的サイトの設定

サーバーサイドロジックのない純粋な静的サイトの場合:

{
  "name": "my-static-site",
  "compatibility_date": "2026-03-01",
  "assets": {
    "directory": "./dist/",
    "not_found_handling": "404-page",
  },
}

SPA設定

クライアントサイドルーティングのシングルページアプリケーションの場合:

{
  "name": "my-spa",
  "compatibility_date": "2026-03-01",
  "assets": {
    "directory": "./build/",
    "not_found_handling": "single-page-application",
  },
}

アセット除外パターン

Pagesは node_modules.git.DS_Store を自動的に除外していた。Workersでは除外されない。.assetsignore を作成しよう。

node_modules
.git
.DS_Store
.env*
*.map

注意: @sveltejs/adapter-cloudflare を使用するSvelteKitプロジェクトの場合、アダプタがアセットフィルタリングを自動処理する。アダプタの出力ディレクトリ外にカスタム静的ファイルがない限り、.assetsignore は通常不要だ。

ローカル開発

Wranglerコマンドが若干変わる。

# Pages
wrangler pages dev ./dist --port 8788

# Workers
wrangler dev --port 8787

デフォルトポートが 8788 から 8787 に変わる点に注意。チームのスクリプトが旧ポートを想定している場合は、wrangler.jsonc で設定できる。

{
  "dev": {
    "port": 8788, // Pages時代のポートを維持して整合性を保つ
  },
}

フレームワーク別ガイダンス

SvelteKit

Vite 8(Rolldown)へのアップグレードも同時に行う場合は、一度にまとめて対応するのがよい。ビルドパイプラインが変わるため、両方を一気に片付けたほうが楽だ。筆者は1日で8つのSvelteKitサイトをWorkersに移行した。Vite 8側の詳細は8つのSvelteKitサイトをVite 8に移行した話を参照してほしい。

アダプタを更新する。

npm install -D @sveltejs/adapter-cloudflare

svelte.config.js:

import adapter from '@sveltejs/adapter-cloudflare';

export default {
  kit: {
    adapter: adapter({
      routes: {
        include: ['/*'],
        exclude: ['<all>'],
      },
      platformProxy: {
        configPath: './wrangler.jsonc',
        persist: { path: '.wrangler/state/v3' },
      },
    }),
  },
};

wrangler.jsonc:

{
  "name": "my-sveltekit-app",
  "compatibility_date": "2026-03-01",
  "compatibility_flags": ["nodejs_compat"],

  // 重要:SvelteKitアダプタの出力先は .svelte-kit/cloudflare/ であり、dist/ ではない
  "main": ".svelte-kit/cloudflare/_worker.js",
  "assets": {
    "directory": ".svelte-kit/cloudflare",
    "binding": "ASSETS",
  },

  // PRデプロイ用のプレビューURLを有効化
  "workers_dev": true,
  "preview_urls": true,
}

よくある間違い: 多くのガイドでは ./dist/server/./dist/client/ のパスが示されているが、@sveltejs/adapter-cloudflare の出力先は .svelte-kit/cloudflare/ だ。誤ったパスを使うと「Worker not found」エラーになる。

platform.env 経由でバインディングにアクセスする。

// +page.server.ts
export async function load({ platform }) {
  const db = platform?.env?.DB;
  const result = await db?.prepare('SELECT * FROM posts').all();
  return { posts: result?.results ?? [] };
}

Lume、11tyなど(静的サイト)

Lume、11tyなどの静的サイトジェネレータの場合、サーバーサイドロジックは不要だ。アセットのみのWorkerを作成する。

wrangler.jsonc:

{
  "name": "my-11ty-site",
  "compatibility_date": "2026-03-01",
  "assets": {
    "directory": "./_site/",
    "not_found_handling": "404-page",
  },
}

ライブリロード付きのローカル開発では、両方のツールを並行で実行する。

# ターミナル1:11ty watch
npx @11ty/eleventy --watch

# ターミナル2:Wranglerでライブリロード
npx wrangler dev --live-reload

または package.json に便利スクリプトを作成する。

{
  "scripts": {
    "dev": "concurrently \"npx @11ty/eleventy --watch\" \"npx wrangler dev --live-reload\""
  }
}

GatsbyとReact SPA

クライアントサイドルーティングを使用するGatsbyやCreate React Appのビルドの場合:

wrangler.jsonc:

{
  "name": "my-gatsby-site",
  "compatibility_date": "2026-03-01",
  "assets": {
    "directory": "./public/",
    "not_found_handling": "single-page-application",
  },
}

セキュリティヘッダーやカスタムロジックを追加する必要がある場合は、Workerを作成する。

src/index.ts:

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // アセットバインディングに静的ファイルを処理させる
    const response = await env.ASSETS.fetch(request);

    // セキュリティヘッダーを追加
    const headers = new Headers(response.headers);
    headers.set('X-Content-Type-Options', 'nosniff');
    headers.set('X-Frame-Options', 'DENY');
    headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');

    return new Response(response.body, {
      status: response.status,
      headers,
    });
  },
};

次に、Workerを先に実行するよう設定する。

{
  "name": "my-gatsby-site",
  "compatibility_date": "2026-03-01",
  "main": "./src/index.ts",
  "assets": {
    "directory": "./public/",
    "binding": "ASSETS",
    "not_found_handling": "single-page-application",
    "run_worker_first": true,
  },
}

Pages Functions移行

functions/ ディレクトリパターンを使用していた場合:

  1. 関数をコンパイルする:

       npx wrangler pages functions build --outdir ./dist/functions
       
  2. main をコンパイル済み出力に向ける:

       {
         "main": "./dist/functions/index.js",
         "assets": {
           "directory": "./dist/client/",
         },
       }
       

ルーティングに _routes.json を使用していた場合は、run_worker_first に置き換える。

{
  "assets": {
    "run_worker_first": ["/api/*", "/auth/*"],
  },
}

ドメイン移行:最も難しいポイント

ここは多くのガイドが不十分な部分だ。カスタムドメインの移行が単純でない理由は以下の通り。

  1. ドメインがPagesに紐づいている間は、DNS CNAMEを手動で変更できない
  2. ドメインがPagesに紐づいている間は、Workersにドメインを追加できない
  3. Pagesから先に削除するとダウンタイムが発生する

解決策は、APIによるアトミックな切り替えだ。

flowchart TD
    subgraph problem["問題点"]
        PR1[ドメインがPagesに紐付き]
        PR2[Workersに追加不可<br/>使用中エラー]
        PR3[先にPagesから削除?]
        PR4[ダウンタイム発生!]
        PR1 --> PR2
        PR2 --> PR3
        PR3 --> PR4
    end
    subgraph solution["解決策: APIによるアトミック切替"]
        S1[まずworkers.devにデプロイ]
        S2[workers.dev URLで十分にテスト]
        S3[切替スクリプト実行]
        subgraph atomic["約2-5秒"]
            A1[Pages APIからドメイン削除]
            A2[Workersルート追加 ルート]
            A3[Workersルート追加 www]
            A1 --> A2 --> A3
        end
        S4[サイト表示確認]
        S5[Always Use HTTPS有効化]
        S1 --> S2 --> S3 --> atomic --> S4 --> S5
    end
    problem -.->|代わりに| solution
    style PR4 fill:#ffcdd2
    style atomic fill:#c8e6c9

Pagesプロジェクトの検索

まず、APIで既存のPagesプロジェクトを特定する。

#!/bin/bash
# find-pages-project.sh

ACCOUNT_ID="your-account-id"
DOMAIN="yourdomain.com"
API_TOKEN="your-api-token"

# 全Pagesプロジェクトを一覧表示
curl -s "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/pages/projects" \
  -H "Authorization: Bearer ${API_TOKEN}" | \
  jq -r '.result[] | select(.domains[] | contains("'"${DOMAIN}"'")) | .name'

アトミックなドメイン切り替え

このスクリプトはドメインをPagesから削除し、即座にWorkersに追加する(ダウンタイムは2〜5秒)。

#!/bin/bash
# switchover.sh

set -e

ACCOUNT_ID="your-account-id"
ZONE_ID="your-zone-id"
API_TOKEN="your-api-token"
PAGES_PROJECT="old-pages-project"
WORKERS_SCRIPT="new-workers-project"
DOMAIN="yourdomain.com"

echo "Removing domain from Pages..."
curl -s -X DELETE \
  "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/pages/projects/${PAGES_PROJECT}/domains/${DOMAIN}" \
  -H "Authorization: Bearer ${API_TOKEN}"

echo "Adding Workers route for root domain..."
curl -s -X POST \
  "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/workers/routes" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{
    "pattern": "'"${DOMAIN}"'/*",
    "script": "'"${WORKERS_SCRIPT}"'"
  }'

echo "Adding Workers route for www subdomain..."
curl -s -X POST \
  "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/workers/routes" \
  -H "Authorization: Bearer ${API_TOKEN}" \
  -H "Content-Type: application/json" \
  --data '{
    "pattern": "www.'"${DOMAIN}"'/*",
    "script": "'"${WORKERS_SCRIPT}"'"
  }'

echo "Done! Domain switched to Workers."

切り替え後の検証

切り替え後に行うこと:

  1. 即座にテスト: サイトを読み込み、動作を確認する
  2. 両ドメインを確認: yourdomain.comwww.yourdomain.com の両方をテストする
  3. HTTPSを有効化: Cloudflareダッシュボードで「Always Use HTTPS」が有効になっていることを確認する
  4. エラーを監視: Workers分析で5xxエラーがないかチェックする

よくあるドメインの問題

522タイムアウトエラー: 接続先が間違っている。Workersルートはゾーンに直接紐づくため、workers.dev へのCNAMEは設定しないこと。

wwwサブドメインが動作しない: ルートとwwwに別々のルートが必要。上記の切り替えスクリプトは両方を処理する。

API権限エラー: トークンに「Edit Workers Routes」権限が必要。また、開始日が今日以前であることも確認すること(未来の開始日はトークンをブロックする)。

高度な機能

Workersに移行すれば、プラットフォーム全体が利用可能になる。

Service Bindings

ネットワークオーバーヘッドなしで複数のWorkerを接続する。トラフィックはCloudflareのネットワーク内にとどまり、パブリックインターネットを通過しない。

{
  // Service Bindingsはネットワークレベルのセキュリティを提供
  // トラフィックはCloudflareのインフラを離れない
  "services": [
    {
      "binding": "AUTH_SERVICE",
      "service": "auth-worker",
      "entrypoint": "AuthHandler", // オプション:名前付きエクスポートにのみ必要
    },
  ],
}
// メインWorker内
const user = await env.AUTH_SERVICE.validateToken(token);

セキュリティ上の注意: Service Bindingsはネットワークレベルの分離を提供するが、多層防御としてアプリケーションレベルの認証(HMAC署名など)も実装すべきだ。1つのWorkerが侵害された場合、HMACにより他の正当な呼び出し元のなりすましを防止できる。Service Bindingsはプライベートネットワーク、HMACは本人確認と考えるとよい。

Service Bindingsドキュメント

Secrets Store

複数のWorker間でシークレットを共有する。

{
  "secrets_store_secrets": [
    {
      "binding": "API_KEY",
      "secret_name": "shared-api-key",
    },
  ],
}

Secrets Storeドキュメント

Smart Placement

Cloudflareが自動的にWorkerをデータの近くに配置する。

{
  "placement": {
    "mode": "smart",
  },
}

動作を確認するにはレスポンスヘッダーをチェックする。

# Workerがどのcoloで実行されているか確認
curl -sI https://your-worker.example.com/ | grep -iE "cf-ray|cf-placement"

# cf-ray: abc123-NRT    ← NRT = 東京(ユーザー最寄りのcolo、通常動作)
# cf-ray: abc123-IAD    ← IAD = バージニア(Smart PlacementがDB近くに移動)

cf-ray ヘッダーのサフィックスがcoloコードを示す。Smart Placementなしの場合、東京からのリクエストは常にNRTで実行される。有効にすると、D1や外部データベースが us-east-1 にある場合、WorkerがIADで実行されることがある。データベースへのラウンドトリップが減り、Workerがユーザーから遠くても全体的なレスポンスが速くなる。

Smart Placementドキュメント

D1リードレプリケーション

グローバルアプリでD1を使用している場合、リードレプリカを有効にして最寄りのエッジロケーションから読み取りを配信できる。

{
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "my-database",
      "database_id": "xxx",
      // 注意:リードレプリケーションはwrangler.jsoncではなくCloudflareダッシュボードで有効化
    },
  ],
}

重要:Read-After-Write整合性にはD1 Sessionsを使用すること

リードレプリケーションが有効な場合、read-after-write整合性を確保するためにセッションでデータベース接続をラップする必要がある。これをしないと、書き込み直後の読み取りがレプリカから古いデータを返す可能性がある。

// 間違い:書き込み後に古いデータを読む可能性あり
const db = env.DB;
await db.prepare('INSERT INTO posts (title) VALUES (?)').bind('New Post').run();
const posts = await db.prepare('SELECT * FROM posts').all(); // insertが反映されていないかも!

// 正解:整合性のためにセッションを使用
const db = env.DB.withSession();
await db.prepare('INSERT INTO posts (title) VALUES (?)').bind('New Post').run();
const posts = await db.prepare('SELECT * FROM posts').all(); // insertが確実に含まれる

複数データベースの場合はヘルパーを作成する。

interface D1SessionEnv {
  DB_MAIN: D1Database;
  DB_CLIENT?: D1Database;
}

function wrapWithSessions(env: D1SessionEnv) {
  return {
    DB_MAIN: env.DB_MAIN.withSession(),
    DB_CLIENT: env.DB_CLIENT?.withSession(),
  };
}

// リクエストハンドラ内
const dbs = wrapWithSessions(env);

D1リードレプリケーションドキュメント D1 Sessionsドキュメント

オブザーバビリティ

デバッグとモニタリングのための包括的なログ記録とトレースを有効化する。

{
  "observability": {
    "enabled": true,
    "head_sampling_rate": 1, // 1 = 100%サンプリング、高トラフィック時は削減
    "logs": {
      "enabled": true,
      "head_sampling_rate": 1,
      "persist": true, // 後から分析するためにログを保存
      "invocation_logs": true, // 各リクエストをログ記録
    },
    "traces": {
      "enabled": true,
      "persist": true,
      "head_sampling_rate": 1, // デバッグ用にすべてのトレースをキャプチャ
    },
  },
}

この設定で以下が提供される。

  • 呼び出しログ:タイミングとステータス付きですべてのリクエストを確認
  • 永続ログ:Cloudflareダッシュボードで過去のログを検索
  • トレース:Service Binding間の複雑なフローをデバッグするための分散トレース

高トラフィックの本番環境では、コスト管理のために head_sampling_rate を下げる(例:10%なら 0.1)。

Workersオブザーバビリティドキュメント Workers Logsドキュメント Workers Tracesドキュメント

CI/CDセットアップ

苦い経験から学んだ教訓

Pagesから移行したとき、明らかな選択肢はビルドとデプロイのパイプライン全体をGitHub Actionsで再現することだった。そうした結果、pushのたびにプロジェクトがビルドされ、テストが実行され、wrangler deploy でデプロイされた。2週間ほどは順調だったが、GitHub Actionsの月間利用枠をすべて使い切ってしまった。

問題はこうだ。PagesはCloudflareのインフラでビルドを無料で処理していた。GitHub Actionsは分単位で課金され、複数プロジェクトでpushごとに npm ci && npm run build && wrangler deploy を実行するとすぐに積み上がる。

解決策:実際のビルドとデプロイには Cloudflareの Workers Builds(GitHubリポジトリに接続された組み込みCI)を使い、GitHub Actionsはリント、セキュリティスキャン、型チェックなどの軽量ジョブにのみ使う。ビルドはCloudflare側で行われ、プランに含まれている。

それでもGitHub Actionsでデプロイしたい場合

動作するが、利用枠に注意。パイプラインは以下の通り。

flowchart LR
    subgraph trigger["トリガー"]
        PR[Pull Request]
        Push[mainにPush]
    end
    subgraph build["ビルド"]
        Install[npm ci]
        Build[npm run build]
        Test[npm test]
    end
    subgraph deploy["デプロイ"]
        Preview[wrangler versions upload]
        Prod[wrangler deploy]
    end
    subgraph notify["通知"]
        Comment[PRにURLコメント]
        Slack[Slack通知]
    end
    PR --> Install --> Build --> Test --> Preview --> Comment
    Push --> Install --> Build --> Test --> Prod --> Slack

完全なGitHub Actionsワークフローは以下の通り。

# .github/workflows/deploy.yml
name: Deploy to Workers

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci
      - run: npm run build

      - name: Deploy to Cloudflare Workers
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}
          accountId: ${{ secrets.CF_ACCOUNT_ID }}

必要なAPIトークン権限

トークンに必要な権限:

  • Workers Scripts: Edit :Workersのデプロイ
  • Workers Routes: Edit :カスタムドメインの管理
  • Account Settings: Read :アカウントリソースへのアクセス

Secrets Storeを使用する場合:

  • Workers Secrets Store: Edit(Readだけでは不足!)

プレビュー環境

Pagesの優れた機能の1つは、すべてのブランチに対する自動プレビューデプロイだった。Workersでは3つのアプローチでこれを再現できる。

アプローチ 仕組み 分離レベル 複雑さ 適したケース
組み込みプレビューURL wrangler versions upload → デプロイごとにユニークURL デプロイ単位 ほとんどのプロジェクト
環境ベース wrangler deploy --env preview → ステージングサブドメイン 単一プレビュー環境 ステージングワークフロー
ブランチWorker ブランチごとに別Worker、マージ後に削除 ブランチ単位 大規模チーム

ほとんどのプロジェクトでは、オプション1から始めるのがよい。

オプション1:組み込みプレビューURL(最もシンプル)

Cloudflareネイティブのプレビュー URL機能を有効化する。

wrangler.jsonc:

{
  "name": "my-app",
  "compatibility_date": "2026-03-01",
  "preview_urls": true,
  "main": "./dist/server/index.js",
  "assets": {
    "directory": "./dist/client/",
  },
}

バージョン管理されたデプロイを使用する。

# 新バージョンをアップロード(本番に影響しない)
npx wrangler versions upload

# そのバージョンのプレビューURLを取得
# 出力:https://abc123.my-app.workers.dev

CIでは、プルリクエスト用にプレビューバージョンをデプロイする。

# .github/workflows/preview.yml
name: Preview Deployment

on:
  pull_request:
    branches: [main]

jobs:
  preview:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci
      - run: npm run build

      - name: Deploy Preview Version
        id: deploy
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}
          accountId: ${{ secrets.CF_ACCOUNT_ID }}
          command: versions upload

      - name: Comment Preview URL
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: 'Preview deployed! Check it out: ${{ steps.deploy.outputs.deployment-url }}'
            })

オプション2:環境ベースのプレビュー

専用のプレビュー環境を独自のサブドメインで作成する。

wrangler.jsonc:

{
  "name": "my-app",
  "compatibility_date": "2026-03-01",
  "compatibility_flags": ["nodejs_compat"],
  "main": "./dist/server/index.js",
  "assets": {
    "directory": "./dist/client/",
  },

  // プレビューURLの動作に必要
  "workers_dev": true,
  "preview_urls": true,

  "env": {
    "preview": {
      "name": "my-app-preview",
      "vars": {
        "ENVIRONMENT": "preview",
      },
      // プレビューはworkers.dev URLを自動使用
    },
    "production": {
      "routes": [
        { "pattern": "yourdomain.com", "zone_name": "yourdomain.com" },
        { "pattern": "www.yourdomain.com", "zone_name": "yourdomain.com" },
      ],
      "vars": {
        "ENVIRONMENT": "production",
      },
    },
  },
}

異なる環境にデプロイする。

# プレビューにデプロイ
npx wrangler deploy --env preview

# 本番にデプロイ
npx wrangler deploy --env production

CIワークフロー:

- name: Deploy
  uses: cloudflare/wrangler-action@v3
  with:
    apiToken: ${{ secrets.CF_API_TOKEN }}
    command: deploy --env ${{ github.ref == 'refs/heads/main' && 'production' || 'preview' }}

オプション3:動的ブランチWorker

フィーチャーブランチごとに分離された環境が必要なチーム向け:

# .github/workflows/branch-preview.yml
name: Branch Preview

on:
  push:
    branches-ignore: [main]
  delete:

jobs:
  deploy:
    if: github.event_name == 'push'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'

      - run: npm ci && npm run build

      - name: Deploy Branch Worker
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}
          # ブランチ名をWorkerの命名用にサニタイズ
          command: deploy --name my-app-${{ github.ref_name | replace('/', '-') }}

  cleanup:
    if: github.event_name == 'delete'
    runs-on: ubuntu-latest
    steps:
      - name: Delete Branch Worker
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}
          command: delete --name my-app-${{ github.event.ref | replace('/', '-') }}

トラブルシューティング

デプロイの失敗:

flowchart TD
    DF{デプロイ失敗: エラー種別?}
    DF -->|サイズ超過| SL[バンドルが大きすぎる]
    DF -->|Error 10021| AE1[Secrets StoreにEdit権限必要]
    DF -->|Error 10000| AE2[Workers Routes Edit権限必要]
    DF -->|DNS競合| DNS[既存DNSレコードを先に削除]
    DF -->|wrangler not found| WNF[npx wrangler deployを使用]
    style SL fill:#fff3e0
    style AE1 fill:#ffcdd2
    style AE2 fill:#ffcdd2
    style DNS fill:#fff3e0

ランタイムエラー:

flowchart TD
    RF{ランタイムエラー: 症状?}
    RF -->|fs/path/processエラー| NODE[nodejs_compatフラグ未設定]
    RF -->|522タイムアウト| T522[CNAMEが間違い: Workersルートを使用]
    RF -->|ルートで404| R404[not_found_handling設定を確認]
    RF -->|環境変数undefined| ENV[静的/動的アクセスの違い]
    RF -->|認証失敗| AUTH[新Workerでシークレット再設定]
    RF -->|SQLITE_CONSTRAINT| FK[外部キー: 親レコード不在]
    style NODE fill:#e3f2fd
    style T522 fill:#ffcdd2
    style AUTH fill:#ffcdd2
    style FK fill:#fff3e0

バンドルサイズが10MBを超える

症状: サイズ制限エラーでデプロイが失敗する。

まず試すべきこと: Vite 8にアップグレードする。RolldownバンドラはVite 7のRollupよりも大幅に小さい出力を生成する。コード変更なしで10〜30%のバンドルサイズ削減が見られた。詳細は8つのSvelteKitサイトをVite 8に移行した話を参照。

それでも超える場合:

  1. 積極的にツリーシェイク: 未使用のインポートを削除
  2. ダイナミックインポート: すべてのリクエストに不要なコードを分割
  3. クライアントへ移動: サーバーサイドレンダリング不要の大きなライブラリ
  4. 外部サービス化: KV、R2、外部APIにオフロード
// Before:大きなインポートが常にロード
import { heavyLibrary } from 'heavy-library';

// After:必要時にダイナミックインポート
const heavyLibrary = await import('heavy-library');

Node.js APIエラー

症状: fspathprocess が見つからないランタイムエラー。

対処法:

fs 操作の場合:

// fs.readFileSyncの代わりに
const response = await env.ASSETS.fetch(new Request('file.json'));
const data = await response.json();

process.env の場合:

// SvelteKit
import { env } from '$env/dynamic/private';
const apiKey = env.API_KEY;

// 素のWorkers
export default {
  fetch(request, env) {
    const apiKey = env.API_KEY;
  },
};

認可エラー

エラー10021(Secrets Store): トークンに「Read」はあるが「Edit」権限が必要。

エラー10000(Workers Routes): トークンに「Edit Workers Routes」権限が必要。

トークンがまったく動作しない: トークンの開始日が未来に設定されていないか確認する。

移行後のシークレット同期

症状: 移行後に他のWorkerや外部サービスへの認証が失敗する。

原因: wrangler secret put で設定したシークレットはWorker固有だ。PagesからWorkersに移行すると新しいWorkerが作成され、シークレットは自動的に引き継がれない。

対処法:

  1. 新しいWorkerですべてのシークレットを再設定する:

       wrangler secret put MY_SECRET
       
  2. 別のサービスとHMAC認証を使用している場合は、シークレットの形式を確認する。

    • 生のシークレットを署名キーとして使うサービスもある
    • シークレットのハッシュ(SHA256など)を署名キーとして使うサービスもある
    • 受信側サービスのドキュメントまたはコードを確認すること
  3. Service Bindings認証の場合は、呼び出し側と受信側の両方のWorkerに一致するシークレットが設定されていることを確認する。

静的リダイレクトの制限

Pagesでは _redirects で2000件の静的リダイレクトが許可されていた。Workersにはこのファイルがない。

対処法:

  1. Workerでのパターンベースのリダイレクト:

       const redirects = new Map([
         ['/old-path', '/new-path'],
         ['/another-old', '/another-new'],
       ]);
    
       export default {
         fetch(request, env) {
           const url = new URL(request.url);
           const redirect = redirects.get(url.pathname);
           if (redirect) {
             return Response.redirect(new URL(redirect, url.origin), 301);
           }
           return env.ASSETS.fetch(request);
         },
       };
       
  2. Cloudflare Bulk Redirects: 大量のリダイレクトリストには、Cloudflareダッシュボードの Bulk Redirects を使用する。

DNSコンフリクト

症状: 「DNS record already exists」エラーでデプロイが失敗する。

対処法: デプロイ前にCloudflareダッシュボードで競合するDNSレコードを手動で削除する。

D1外部キー制約エラー

症状: ランタイムで FOREIGN KEY constraint failed: SQLITE_CONSTRAINT エラーが発生する。

原因: D1はデフォルトで外部キー制約を強制する(一部のSQLite設定とは異なる)。これにより、以前は黙って無視されていた参照整合性の問題が検出される。

対処法:

  1. 親レコードが先に存在することを確認:

       // 間違い:親より先に子を作成
       await db.prepare('INSERT INTO posts (user_id, title) VALUES (?, ?)').bind(userId, title).run();
    
       // 正解:親の存在を確認するか、正しい順序で作成
       const user = await db.prepare('SELECT id FROM users WHERE id = ?').bind(userId).first();
       if (!user) throw new Error('User not found');
       await db.prepare('INSERT INTO posts (user_id, title) VALUES (?, ?)').bind(userId, title).run();
       
  2. オプショナルな外部キーにはNULLを使用:

       // FK列がnull許容で有効な参照がない場合
       await db
         .prepare('INSERT INTO posts (user_id, title) VALUES (?, ?)')
         .bind(null, title) // undefinedや無効なIDではなくnullを渡す
         .run();
       
  3. バッチ操作の順序を正しくする:

       // バッチでは、親のinsertを子のinsertより前にする
       await db.batch([
         db.prepare('INSERT INTO users (id, name) VALUES (?, ?)').bind(userId, name),
         db.prepare('INSERT INTO posts (user_id, title) VALUES (?, ?)').bind(userId, title),
       ]);
       

CIで wrangler コマンドが見つからない

症状: CIが「wrangler: command not found」で失敗する。

対処法: パッケージマネージャのexecコマンドを使用する。

# npm
npx wrangler deploy

# pnpm
pnpm exec wrangler deploy

# yarn
yarn wrangler deploy

大量デプロイがあるPagesプロジェクトの削除

Cloudflareダッシュボードでは500件以上のデプロイがあるPagesプロジェクトを削除できない。

対処法: APIでデプロイをバッチ削除する。

#!/bin/bash
# delete-deployments.sh

ACCOUNT_ID="your-account-id"
PROJECT_NAME="your-pages-project"
API_TOKEN="your-api-token"
LIMIT=100  # 1回の実行で削除するデプロイ数

deployments=$(curl -s \
  "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/pages/projects/${PROJECT_NAME}/deployments?per_page=${LIMIT}" \
  -H "Authorization: Bearer ${API_TOKEN}" | jq -r '.result[].id')

for id in $deployments; do
  echo "Deleting deployment: $id"
  curl -s -X DELETE \
    "https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/pages/projects/${PROJECT_NAME}/deployments/${id}" \
    -H "Authorization: Bearer ${API_TOKEN}"
  sleep 0.3  # レート制限対策
done

echo "Deleted up to ${LIMIT} deployments. Run again if more remain."

ダッシュボードから削除できるまでデプロイ数が十分に減るまで、このスクリプトを繰り返し実行する。

移行後チェックリスト

移行後、すべてが動作することを確認する。

  • すべてのカスタムドメインでサイトが正しく読み込まれる
  • ルートドメインとwwwサブドメインの両方が動作する
  • HTTPSが強制されている(「Always Use HTTPS」有効)
  • すべての環境変数が設定されている
  • シークレットにアクセスできる(wrangler secret put で再設定)
  • データベースバインディング(D1、KV、R2)が動作する
  • リードレプリケーション使用時にD1 Sessionsが有効
  • APIルートが正しく機能する
  • Service Bindings認証が成功する(該当する場合)
  • 非本番ブランチのプレビューデプロイが動作する
  • CI/CDパイプラインが正常にデプロイする
  • オブザーバビリティ/ログが有効(ログ + トレース)
  • スケジュールトリガー(cron)が正しく起動する
  • Smart Placementがアクティブ(必要な場合)

現状(2026年3月時点)

2026年3月時点で、Workersは静的アセット、SSR、カスタムドメインにおいてPagesと完全な機能パリティを持つ。Secrets Store、Workflows、Containers、Durable ObjectsはWorkers専用のままだ。Cloudflareは強制移行の期限を発表していないが、差は広がり続けている。主要なプラットフォーム機能はすべてWorkers向けに先行リリースされる。

ドメインの切り替えが最も難しいポイントだ。上記のアトミックAPIアプローチは動作するが、2〜5秒のダウンタイムは想定しておくこと。それ以外はすべて設定変更だ。

新規プロジェクトの場合は、Pagesをスキップして最初からWorkersにデプロイしよう。既存のPagesプロジェクトがある場合は、プロセスをコントロールできるうちに自分のスケジュールで移行するのがよい。


参考資料:

移行と入門

フレームワークアダプタ

Workers機能

オブザーバビリティとデバッグ

デプロイとCI/CD

Read the original on cogley.jp

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.