ブログへ戻る

Cloudflare PagesからWorkersへ、停止なしで移行する

React SPAやViteアプリをCloudflare PagesからWorkersへ移行する手順。wrangler設定、SPAルーティング、Pages Functions移行、環境変数、カスタムドメインの切り替え順序を解説する。

React SPAやViteで構築したアプリをCloudflare Pagesで配信している。これをWorkersへ移行したい——Durable Objectsを使いたい、段階的デプロイが必要、Tail Workersでリクエストログを取りたい、あるいは複数プロジェクトをWorkers一本に集約したい、といった理由から。

設定の変更量は多くないが、順序を間違えると本番が止まる。「Workerを作って、ドメインを向け直して、Pagesを消す」という直接切り替えはダウンタイムを生む。安全な手順は、workers.devサブドメインでWorkerを構築・検証してから、本番トラフィックが使うドメインを動かすことだ。

この記事では全体の手順を扱う。wrangler設定、SPAルーティング、Pages Functions移行、環境変数、ドメイン切り替え、ロールバックまでをまとめる。

PagesからWorkersへの移行が意味をなす場面

Pagesはシンプルなデプロイ先だ。リポジトリを接続して、ビルドコマンドを設定すれば動く。Workersには設定が増える。次のいずれかが必要なときに移行する価値がある。

  • Durable ObjectsやWorkersバインディングをPagesからはネイティブに使えない
  • 段階的デプロイでリリース前に一部トラフィックへ変更を流したい
  • Tail WorkersやLogpushでPagesより詳細なリクエストログが必要
  • ルートパス以外のルーティング — Workersはexample.com/api/*形式のルートを設定できる
  • リモート開発wrangler dev --remoteで本番バインディングに対してローカル開発したい
  • 本番エラーのSource Maps対応

該当しないなら、Pagesのままで構わない。

PagesとWorkersで何が変わるか

核心はプロジェクトの記述方法だ。

| | Cloudflare Pages | Cloudflare Workers | |---|---|---| | 設定ファイル | wrangler.tomlpages_build_output_dir | wrangler.jsonc / .tomlassets.directory | | 開発コマンド | wrangler pages dev(ポート8788) | wrangler dev(ポート8787) | | デプロイコマンド | wrangler pages deploy | wrangler deploy | | SPAフォールバック | _redirects/* /index.html 200とすれば自動 | not_found_handling: "single-page-application"を明示的に設定 | | APIルーティング | functions/ディレクトリのPages Functions | mainフィールドで指定するWorkerエントリーポイント | | プレビューURL | ブランチごとに自動生成 | Workers Buildsで設定が必要 | | Cloudflare外のカスタムドメイン | 対応(CNAME検証で追加可能) | 非対応 — ドメインがCloudflare DNSで管理されていることが必須 |

最後の行が最初に確認すべき最重要項目だ。

まずカスタムドメインの状態を確認する

WorkersのカスタムドメインはCloudflareが管理するネームサーバーのドメインのみ対応している。Pagesにはこの制約がなく、外部レジストラのドメインをCNAME検証で追加できる。

移行を始める前にCloudflareダッシュボードを開き、対象ドメインがCloudflareゾーン配下でオレンジ雲(プロキシ有効)のDNSレコードとして設定されているか確認する。ドメインが外部レジストラで管理されCloudflareのネームサーバーに向いていなければ、ネームサーバーを移管してから作業を進める必要がある。

ネームサーバーの移管ができない場合、Workersへの移行はそのドメインでは行えない。

wrangler.jsonc設定を作る

プロジェクトルートにwrangler.jsoncを作成する。既存のPages設定はまだ削除しない。

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

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "your-app-name",
  "compatibility_date": "2026-08-01",
  "assets": {
    "directory": "./dist",
    "not_found_handling": "single-page-application"
  }
}

Workerエントリーポイント(APIルートやミドルウェア)がある場合:

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "your-app-name",
  "compatibility_date": "2026-08-01",
  "main": "./dist-worker/index.js",
  "assets": {
    "directory": "./dist-client",
    "not_found_handling": "single-page-application"
  }
}

nameフィールドはCloudflareダッシュボードで作成するWorker名と一致させる必要がある。不一致だとWorkers Buildsが失敗する。

静的アセットとSPAルーティングを設定する

SPAの404問題

Pagesでよく見るパターンは、出力ディレクトリに置いた_redirectsファイルに次の一行を書くことだ:

/* /index.html 200

これにより、静的ファイルとして存在しないパスへのリクエストに対しindex.html200で返す。React Routerなどのクライアントサイドルーターはこれを前提にしている。

Workersではこの挙動は自動ではない。設定なしだとhttps://example.com/dashboardのような直接URLが404になる。しかもアプリ独自の404ページではなく、Cloudflareのエラーページがそのままブラウザへ返される。最初のwrangler deploy直後にもっともよく踏むのがこの症状だ。not_found_handling"single-page-application"にすることで同等の挙動になる:

"assets": {
  "directory": "./dist",
  "not_found_handling": "single-page-application"
}

この設定で、静的ファイルとして存在しないリクエストはすべてindex.html200で返すようになる。

APIルートとrun_worker_first

デフォルトではWorkerはアセット配信の後に動く。/api/usersのようなAPIルートをWorkerで処理するには、対象パスでWorkerコードを先に実行する設定が必要だ:

"assets": {
  "directory": "./dist-client",
  "not_found_handling": "single-page-application",
  "run_worker_first": ["/api/*"]
}

run_worker_firstに配列でglobパターンを渡すと、そのパスではアセット照合より先にWorkerが呼ばれる。ブール値のtrueにすると全リクエストがWorkerを通るため、静的ファイルへのリクエストにも不要なWorkerの呼び出しが発生する。APIパスだけを対象にする方が明確だ。

_redirectsとリダイレクトルール

_redirectsファイルにSPAフォールバック以外のリダイレクトルール(例:/old-path /new-path 301)が含まれている場合、移行前にWorkers静的アセットのルーティングドキュメントでそれらのルールが対応しているか確認する。SPAフォールバックパターン(/* /index.html 200)は上述のnot_found_handlingで置き換える。それ以外のリダイレクトルールは、後述のworkers.dev URL検証ステップで各ルールの動作を確認すること。

_headersファイル

PagesプロジェクトでHTTPレスポンスヘッダーを_headersファイルで管理していた場合、同じファイルがWorkers静的アセットでも動作する。assets.directoryで指定したディレクトリに置けばよい。構文は変わらない:

/assets/*
  Cache-Control: public, max-age=31536000, immutable

/*
  X-Frame-Options: DENY
  X-Content-Type-Options: nosniff

注意点がある。_headersファイルのヘッダーは静的アセットのレスポンスにのみ適用される。run_worker_firstでWorkerが生成したレスポンスには適用されない。Workerが生成するレスポンスにヘッダーが必要な場合はWorkerコード内で直接設定する。

Pages FunctionsをWorkerエントリーポイントへ移行する

Pages Functionsを使っていた場合、移行経路は二つある。

方法1:Functionsディレクトリをコンパイルする

functions/ディレクトリにPages Functions形式で書かれた関数がある場合、一つのWorkerエントリーポイントへまとめてコンパイルできる:

npx wrangler pages functions build --outdir=./dist-worker/

これで./dist-worker/index.jsが生成される。wrangler.jsoncmainにそのパスを設定する:

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

このコンパイルステップをwrangler deployの前に実行するようビルドスクリプトへ追加する。

方法2:HonoやWorkerハンドラーとして書き直す

Pages Functionsがシンプルな数本のAPIエンドポイントしかない場合、標準的なWorkersハンドラーとして書き直す方がすっきりする場合が多い:

import { Hono } from "hono";

const app = new Hono();

app.get("/api/health", (c) => c.json({ ok: true }));

export default app;

KVやD1などのバインディングを使う場合はnpx wrangler typesで型定義ファイルを生成し、new Hono<{ Bindings: Env }>()とする。

この構成ではrun_worker_firstでAPIパスをカバーし、その他は静的アセットに委ねる。

Advanced Mode(_worker.js)

Pages Advanced Modeで出力ディレクトリに_worker.jsを置くパターンを使っていた場合、そのファイルをアセットディレクトリの外へ移動するか、.assetsignoreに追加して静的ファイルとして配信されないようにする:

_worker.js

そしてmainフィールドでそのパスを指定する:

{
  "main": "./dist/_worker.js",
  "assets": {
    "directory": "./dist",
    "not_found_handling": "single-page-application"
  }
}

環境変数とバインディングを移す

WorkersはPagesプロジェクトの環境変数を引き継がない。再設定が必要だ。

本番環境変数: Cloudflareダッシュボードでワーカーを開き、Settings → Variables and Secrets から追加する。Pagesの Settings → Environment Variables に定義していた変数を同様に設定する。

プレビュー環境変数: Workers Buildsは本番とプレビューで環境を分けて管理している。プレビュー用の変数も同じ場所で設定する。Workersではビルド時に使う変数(ビルドコマンドが参照する)とランタイム変数(Workerコードが参照する)は別管理になっている。ビルド時に見えていた変数が自動でランタイムに引き継がれることはない。

KV・D1・R2などのバインディング: wrangler.jsoncに追記し、ダッシュボードでも設定する。バインディング名はWorkerコードが参照している名前と一致させること。この手順は通常のWorkersプロジェクトと同じだ。

本番を変更する前にworkers.dev URLでテストする

カスタムドメインに触れる前に、まずworkers.devサブドメインへデプロイして検証する。これがダウンタイムを回避する核心部分だ。Pagesプロジェクトが本番トラフィックを処理し続けている間に、Workerの動作を確認できる。

npx wrangler deploy

これでyour-app-name.your-account.workers.devへデプロイされる。次の項目を確認してから先へ進む:

  1. ルートURLを開く。ページが表示されることを確認。
  2. 深いURLを直接開く(例:/dashboard/settings)。404ではなくindex.html200で返ることを確認。
  3. APIルートへのリクエストが静的アセット層ではなくWorkerに届いていることを確認。
  4. 静的ファイルのレスポンスヘッダーにCache-Controlが含まれていることを確認。
  5. ドメインに依存する認証フローやサードパーティ連携を確認する。workers.devドメインでは本番ドメインと異なるため失敗するものがあるが、カスタムドメインを付けた後は正常に動く。問題点として記録しておく。

何か問題があれば、カスタムドメインを変更する前にここで修正してから再デプロイする。

カスタムドメインを停止なしで切り替える

Workerの検証が済めばドメイン切り替えは安全にできる。PagesもWorkersもCloudflareのプロキシの裏にあるため、ドメインがCloudflare DNSで管理されていれば切り替えはほぼ即時だ。外部DNSプロバイダーでの伝播待ちは発生しない。

手順は次のとおり:

ステップ1 — Pagesからカスタムドメインをはずす CloudflareダッシュボードでPagesプロジェクトを開き、Custom Domains からドメインの割り当てを削除する。Pagesはそのドメインへのリクエストへの応答をすぐに停止する。

ステップ2 — Workerにカスタムドメインをつける Workerを開き、Settings → Domains & Routes → Add Custom Domain でドメインを追加する。Cloudflareはトラフィックを数秒以内にWorkerへ向ける。

ステップ3 — カスタムドメインでWorkerが動いているか確認する カスタムドメインでサイトを開く。ページが表示されること、およびドメインを使う認証フローが正常に動作することを確認する。

ステップ4 — Pagesプロジェクトはまだ削除しない ロールバック先として残しておく。Workerの安定が確認できるまでPagesを削除すると退路がなくなる。

ステップ1とステップ2の間に、数秒のウィンドウが生じる。この期間にそのドメインは未割り当て状態になる。これが回避不能な唯一のギャップだ。最小化するには、Pagesからドメインを外す前にWorkerのダッシュボードタブを開いて手順を準備しておくとよい。ゼロダウンタイムを厳密に要求する環境では、アクセスの少ない時間帯に切り替えを行う。

CI/CDパイプラインを更新する

PagesのCIではwrangler pages deployコマンドを使っていた。WorkersでのCI/CDはwrangler deployになる。

Workers Builds(推奨): CloudflareダッシュボードでWorkerを開き、Settings → Builds からリポジトリを接続する。Workers Buildsはビルドコマンドを実行し、成功したらwrangler deployを呼ぶ。設定項目:

  • ビルドコマンド(例:npm run build
  • ビルド出力ディレクトリ(wrangler deploywrangler.jsoncを参照するため厳密には不要)
  • Workers Builds接続後はwrangler.jsoncのWorker名がダッシュボードの名前と一致していることを再確認する

Workers Buildsを有効にしたらPagesの自動デプロイを無効にする。両方が同じpushに反応するのを防ぐ。

GitHub Actions: GitHub Actionsのワークフローで動かしている場合は、wrangler pages deploywrangler deployに変更し、--project-nameフラグを削除する:

- name: Deploy
  run: npx wrangler deploy
  env:
    CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

プレビュー環境はWorkerダッシュボードでBranch deploymentsを有効にすると、mainブランチ以外のブランチにも自動的にプレビューURLが作られる。

ロールバック手順

ドメイン切り替え後にWorkerで問題が起きた場合:

  1. WorkerのSettings → Domains & Routes でカスタムドメインの割り当てを削除する。
  2. PagesプロジェクトのCustom Domainsでカスタムドメインを再追加する。

Pagesがトラフィックを再び受ける。残しておいたPagesプロジェクトがロールバック先になる。

Workerコード自体のロールバック(ドメインを巻き戻さない場合)には、Workers BuildsのVersion Historyから以前のバージョンを昇格させるか、wrangler rollbackを使う。

移行チェックリスト

Pagesを削除する前にこのリストを確認する。

設定

  • [ ] wrangler.jsoncnamecompatibility_dateassets.directoryを設定済み
  • [ ] SPAであればnot_found_handling: "single-page-application"を設定済み
  • [ ] Workerエントリーポイントがある場合はrun_worker_firstでAPIパスを指定済み
  • [ ] _headersファイルをアセットディレクトリに配置し動作確認済み
  • [ ] 必要に応じて.assetsignoreを作成済み

Functions移行

  • [ ] Pages FunctionsをWorkerエントリーポイントにコンパイルまたは書き直し済み
  • [ ] wrangler.jsoncmainフィールドが正しい出力ファイルを指定している
  • [ ] workers.devでAPIルートが正常に応答する

環境変数

  • [ ] 本番環境変数をWorker設定で再設定済み
  • [ ] プレビュー環境変数を再設定済み
  • [ ] KV・D1・R2などのバインディングをwrangler.jsoncとダッシュボードに追加済み

切り替え前検証

  • [ ] workers.devでルートURLが正常に表示される
  • [ ] workers.devでSPAの深いURLが404でなく200を返す
  • [ ] APIルートが正常に応答する
  • [ ] 静的アセットにキャッシュヘッダーが付いている

カスタムドメイン

  • [ ] ドメインがCloudflare管理のネームサーバーで運用されている(Workers必須条件)
  • [ ] Pagesからカスタムドメインをはずした
  • [ ] Workerにカスタムドメインをつけた
  • [ ] カスタムドメインでサイトの動作を確認済み

CI/CD

  • [ ] Workers Buildsをリポジトリに接続済み、またはGitHub Actionsのワークフローを更新済み
  • [ ] Pagesの自動デプロイを無効化済み
  • [ ] 移行後の最初のビルドが成功している

後片付け(1〜2週間安定稼働を確認してから)

  • [ ] Pagesプロジェクトを削除済み
  • [ ] リポジトリからPages向けの設定を削除済み

別リポジトリの変更でCloudflare Pagesのリビルドをトリガーするだけでよく、Workersへの移行は不要な場合は別リポジトリの変更でCloudflare Pagesを再デプロイするを参照。