ローカルで動かしたWorkerランタイムがそのままエッジプラットフォームへ上がっていく過程を表した代表画像

概要

この記事は Cloudflare Workers 静的サイトガイド - デプロイからSEOまで シリーズの一部です。

Cloudflare Workersを扱う仕事は、実質的にwrangler を扱う仕事です。ローカルで立ち上げるのも、デプロイするのも同じCLIです。

ところが名前のせいで誤解が一つ生まれます。wrangler dev はフロントエンドの開発サーバーではありません。 筆者も最初はここでしばらく悩みました。

この記事で扱うこと

  • 何をインストールすべきか — そしてランタイム依存は増えないということ
  • Workerコードの型設定を分離すべき理由
  • wrangler dev が実際に何をするのか · vite dev との違い
  • なぜ静的サイトのビルドが先でなければならないのか
  • Workerがレスポンスを直したか確認する方法
  • デプロイ — 環境ごとの値の注入 · CIで動かすときの注意点

扱わないこと

Workers Static Assetsの構造とリクエストの流れは Cloudflare Workers 静的サイトホスティング - リクエストの流れと課金基準 にあります。

前提

Cloudflareのアカウントがあり、静的サイトをビルドできること(npm run builddist/)以外に必要な事前知識はありません。


wrangler のインストール

CloudflareのCLIです。ローカル実行とデプロイの両方を担当します。コマンド一覧は Commands ドキュメント にあります。

インストールしないという選択肢

wranglerデプロイツールであってアプリケーションコードではありません。 そのため依存に入れず、必要なときだけダウンロードして使うこともできます。

1# As a devDependency
2npm i -D wrangler
3npx wrangler dev
4
5# Or without installing - pin the major version
6npx wrangler@4 dev
7pnpm dlx wrangler@4 dev

後者の利点は、ローカルとCIが同じバージョンを使い、アプリの package.json にデプロイツールが入らないことです。欠点は毎回ダウンロードの時間が少しかかることです。

どちらにせよランタイム依存は増えません。 WorkerはCloudflareが実行します。

型は別パッケージです

FetcherHTMLRewriterExecutionContextcaches のようなWorkersのグローバルは型パッケージから来ます。

1npm i -D @cloudflare/workers-types

型設定を分離すべき理由

一つのプロジェクトの中に実行環境が2つあることで生じる問題です。

1src/                  runs in the browser   has document · no HTMLRewriter
2cloudflare/workers/   runs on workerd       no document · has HTMLRewriter

使えるグローバルが互いに正反対なのに、TypeScriptはファイルを見ただけではどちらなのか分かりません。tsconfigに書いたものしか知りません。そのため「どのフォルダがどの環境なのか」を教える必要があり、そのためには設定が2つ必要です。

ちなみにWorkersはNodeではありません。fsprocess のようなNode APIもなく、ブラウザの document もない第三のランタイムです。そのため既存のNode用設定をそのまま使うこともできません。

設定が一つだけなら、たいていはブラウザ基準です。その設定でWorkerコードまで検査すると、以下のコードがコンパイルを通ってしまいます。

1export default {
2  async fetch(request: Request): Promise<Response> {
3    // There is no document in a Worker. This still type-checks.
4    const el = document.getElementById('root');
5    return new Response(el?.textContent ?? '');
6  },
7};

デプロイすると document is not defined で落ちます。型検査が捕まえるべきだったものを、ランタイムまで先送りしたことになります。

逆方向もあります。@cloudflare/workers-types を入れなければ、HTMLRewriterExecutionContextcaches がすべて「名前が見つかりません」になります。

一つの設定に両方を詰め込むのも答えではありません。DOMとWorkersの型を一緒に入れると、RequestResponsecaches のように両側に同じ名前が違う形で存在するものが混ざり、見当違いの型が当たってしまいます。

そこで設定を分け、project referenceで束ねます。各設定が決めるのは3つです。

決めるもの
includeこのルールをどのフォルダに適用するか
lib標準環境に何があるとみなすか("DOM"windowdocument
types追加のグローバルパッケージ(@cloudflare/workers-typesHTMLRewritercaches
1// tsconfig.worker.json
2{
3  "extends": "./tsconfig.node.json",
4  "compilerOptions": {
5    // No DOM. Workers globals instead.
6    "types": ["@cloudflare/workers-types"]
7  },
8  "include": ["src/worker"]
9}
1// tsconfig.json
2{
3  "files": [],
4  "references": [
5    { "path": "./tsconfig.app.json" },
6    { "path": "./tsconfig.worker.json" }
7  ]
8}

ローカルで立ち上げる — 最も紛らわしい部分

wrangler dev はフロントエンドの開発サーバーではありません

名前が dev なので vite dev のようなものと誤解しやすいのですが、やることがまったく違います。

wrangler devCloudflareのエッジとまったく同じランタイム(workerd)をローカルにWebサーバーとして立ち上げます。 実際にデプロイされればCloudflareのデータセンターで動くそのプログラムを、自分のコンピューターでそのまま動かすのです。HTMLRewritercachesenv.ASSETS がすべてプロダクションと同じ実装です。

つまり**「プロダクションの縮小版をローカルに立てること」**であって、ソースを監視して変換してくれるツールではありません。

vite devwrangler dev
何なのかフロントの**開発サーバープロダクションランタイムの複製**
入力src/ のソースビルド成果物(dist/
ソースの変更HMRで即時反映反映されない — もう一度ビルドする必要がある
Worker存在しない動く

だから静的サイトのビルドが先です

env.ASSETS は**「デプロイされた静的アセットの束」**を指します。その束がなければWorkerが取り出すものがありません。

ローカルでその束をどこから読むかを教えるのが --assets です。

1npx wrangler@4 dev --assets dist
2#                            ^^^^ build output, not src/

順序は常にこうです。

1# 1. Build the static site first -> dist/
2npm run build
3
4# 2. Boot workerd with dist/ as its asset store
5npx wrangler@4 dev --assets dist
1⛅️ wrangler 4.x
2Ready on http://localhost:8787

画面のコードを直したなら1番からやり直す必要があります。 wrangler devdist/ だけを見ているので、src/ を直しても気づきません。「確かに直したのになぜそのままなのか?」のほとんどがこれです。

スクリプトにまとめておくと便利です。

1// package.json
2{
3  "scripts": {
4    // cf- prefix: plain "worker" collides with Web Worker / Service Worker / worker_threads
5    "preview:cf-worker": "npm run build && wrangler dev --assets dist"
6  }
7}

ポートは設定に固定できます。

1// wrangler.jsonc
2{ "dev": { "port": 6173 } }

作業の流れ

2つのサーバーを両方立ち上げたまま使うわけではありません。

  • 画面を作っている間vite dev。Workerは動きません。
  • Workerを触ったときだけビルドしてから wrangler dev で確認します。

確認は「生のレスポンス」で行います

Workerがレスポンスを直すコードなら、レンダリングされた画面を見てはいけません。 画面はJSが作った結果なので、Workerがやったこととブラウザがやったことが混ざります。Workerをまったく通さなくても、画面は同じようにきちんと見えます。

ブラウザのソース表示

1view-source:http://localhost:6173/products/1234

開発者ツールのElementsパネルはここでは使えません — それは現在のDOMであり、すでにJSが実行された後だからです。

開発者ツールのNetworkタブ — ドキュメントのリクエストを選んでResponseを見ると、受け取った本文がそのまま出ます。レスポンスヘッダーまで見られるので最も正確です。

curl — 欲しいものだけ抜き出したり、スクリプトで回したりするときに便利です。

1curl -s http://localhost:6173/products/1234 | head -20
2curl -s -D - -o /dev/null http://localhost:6173/products/1234   # headers only

値が変わっていなければWorkerを通っていないということです。run_worker_first にそのパスが入っているかをまず確認しましょう。


デプロイ

ローカルで確認できたら、同じCLIでアップロードします。

1npx wrangler@4 deploy --assets dist

環境ごとに変わる値

サイトのアドレス・APIのアドレス・キーのように環境によって変わる値は、設定ファイルに埋め込まずCLIで渡す方がよいです(環境変数ドキュメント)。ファイルに書くと環境ごとに分かれ、食い違っても静かに誤動作します。

1npx wrangler@4 deploy --assets dist \
2  --name "prod-my-site" \
3  --var SITE_URL:"https://example.com" \
4  --var API_BASE_URL:"https://api.example.com"

CLIの値が設定ファイルに勝つので、同じ値を2か所に書いておく必要がありません。

1export interface Env {
2  ASSETS: Fetcher;
3  SITE_URL: string;
4  API_BASE_URL: string;
5}

デプロイ履歴を残す

--tag--message はWorkerのバージョンに付くラベルです。ダッシュボードで「今上がっているのはどのコミットなのか」に答えられるようにしてくれます。

1npx wrangler@4 deploy --assets dist \
2  --tag "$GIT_SHA" \
3  --message "ref: $GIT_BRANCH"

なければロールバックのときに目視で突き合わせることになります。

CIで動かすとき

wrangler はデプロイの最後にあれこれ尋ねてきます(テレメトリーの同意など)。CIには答える人がいないので、そこで止まります。

1CI=true WRANGLER_SEND_METRICS=false npx wrangler@4 deploy --assets dist

認証は環境変数で渡します。wranglerがこの名前を直接読むので、別途exportする必要はありません。

1CLOUDFLARE_API_TOKEN
2CLOUDFLARE_ACCOUNT_ID

存在しないWorkerにデプロイしない

wrangler deploy --name X はXがなければ作り、あれば上書きします。便利に見えますが、名前を間違えたデプロイが静かに「成功」します。 見当違いのWorkerが新しく作られ、肝心の見ているサイトは変わりません。

CIならデプロイ前に存在確認を一度しておく方が安全です。APIでその名前を照会してステータスコードだけを見れば十分です。

 1# -o /dev/null  discard the body - we only want the status
 2# -w            print just the status code
 3code=$(curl -s -o /dev/null -w '%{http_code}' \
 4  -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
 5  "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/workers/scripts/$NAME")
 6
 7case "$code" in
 8  2[0-9][0-9]) ;;                                  # exists - go ahead
 9  404) echo "no such Worker: $NAME" >&2; exit 1 ;; # typo, or first-ever deploy
10  401|403) echo "token lacks Workers permission" >&2; exit 1 ;;
11  *) echo "check failed (HTTP $code)" >&2; exit 1 ;;
12esac

2xxを丸ごと受け取る必要があります。 このエンドポイントはスクリプトの本文を返しますが、静的アセットだけを含むWorkerは本文が空なので204が返ります。200 だけを確認すると、2回目以降のデプロイがすべてブロックされます。

そして**「存在しない」(404)と「権限がない」(401・403)を必ず分ける必要があります。** 両者をひとまとめにすると、トークンの権限が足りない状況で「初めて作るのだから作成を許可せよ」という見当違いの案内をすることになり、その言葉に従うとデプロイが認証で再び落ちます。


まとめ

  • ランタイム依存は増えません。 WorkerはCloudflareが実行します。開発用に @cloudflare/workers-typeswrangler だけあれば十分です。
  • wranglerインストールせず npx wrangler@4 で使うこともできます。バージョンを固定すれば、ローカルとCIが同じものを使います。
  • Workerコードは型設定を分離します。一つのtsconfigで検査すると、Workerで document を使っても通ってしまい、デプロイ後にはじめて壊れます。
  • wrangler dev はプロダクションランタイムのローカル複製であって、フロントエンドの開発サーバーではありません。dist/ を先にビルドする必要があり、画面のコードを直したならビルドし直す必要があります。
  • Workerがレスポンスを直したかどうかは生のレスポンスで確認します。レンダリングされた画面やDevToolsのElementsパネルでは区別がつきません。
  • 環境ごとの値はCLIの --var で渡します。設定ファイルに埋め込むと同じ値が2か所にでき、環境ごとに分かれます。
  • 存在しないWorkerにデプロイすると静かに成功します。CIなら存在確認を一度しましょう。

参考