zudo-cloudflare-wisdom
GitHub リポジトリ

検索したい単語を入力

いつでも検索バーを開ける

ゼロからのデプロイ

リポジトリベースの Worker をゼロからプロビジョニングする -- ダッシュボードウィザードの罠、wrangler deploy、.assetsignore、CI トークン権限

既存のリポジトリから新しい Cloudflare アカウントに Worker を立ち上げるのは一見単純だが、ゼロから始める道のりには、どこにもまとまって書かれていない落とし穴が点在している。このページではライフサイクルを順に辿り -- Worker の作成とデプロイ、バインディングのプロビジョニング、フレームワークのビルド出力の配信、そして CI によるデプロイ自動化 -- 各ステップの罠を指摘する。

このメモは 2 つの Worker(アプリ Worker + 別個のドキュメント Worker)をゼロからプロビジョニングした経験に基づく。内容はすべて一般的な Workers の知識であり、特定のスタック固有のものではない。

ダッシュボードから Worker を作らない

これが最大の罠。コードがすでにリポジトリにある場合でも、人は反射的にダッシュボードの 「Create a Worker」ウィザード("Ship something new" -- git リポジトリのインポート、テンプレート、Hello World、アップロード)に手を伸ばす。リポジトリベースの Worker にとって、これらの経路はすべて間違いだ:

  • テンプレート / Hello World / アップロードの経路は、リポジトリとは無関係の空の別 Worker を作ってしまう。

  • 「git リポジトリのインポート」経路は、本来動かしたい CI と競合する Cloudflare の git ビルドパイプラインを配線してしまう。

正しい経路はリポジトリからの wrangler deploy。これは作成と更新が一体になっている:

npx wrangler deploy
  • 初回デプロイ:Worker が誕生する。名前は wrangler 設定の name フィールドから取られる。ダッシュボードでの手順は不要 -- まだ存在しない名前をデプロイすることこそが Worker の作成方法だ。

  • 以降のデプロイ:同じコマンドが更新になる。

ダッシュボードウィザードは完全にスキップする

「先に Worker を登録してからリポジトリを接続する」というステップは存在しない。ダッシュボードウィザードが生むのは孤児 Worker か競合するビルドパイプラインだけ。リポジトリからの wrangler deploy がプロビジョニングのすべてだ。

長期的なベストセットアップは、main への push で CI に wrangler deploy を実行させること。コミット済みの設定が唯一の真実になる。ワークフローは本番デプロイを参照。

workers.dev の URL フォーマットは固定

すべての Worker には、正確にこの形の無料 URL が付与される:

<worker-name>.<account-subdomain>.workers.dev

その配下に任意のサブドメインをでっち上げることはできない -- doc.app.<account-subdomain>.workers.dev のような URL は存在しえない。アプリとドキュメントに別々の URL を持たせる方法は、単純に Worker を 2 つにすること:それぞれが自動的に自分の <name>.<account-subdomain>.workers.dev を得る。

app.example.com / doc.example.com のようなきれいなサブドメインには、本物のカスタムドメイン -- アカウント内のゾーン -- が必要で、wrangler 設定の routes(またはダッシュボード)で配線する:

{
  "routes": [{ "pattern": "app.example.com", "custom_domain": true }]
}

Wrangler の「Add It on Your Behalf?」プロンプトは断る

CLI からのストレージのプロビジョニング(wrangler d1 createwrangler kv namespace create)は、対話的な申し出で終わる:

Would you like Wrangler to add it on your behalf?

断る(N)。 yes と答えると、wrangler 設定に対して 2 つの悪いことが起きる:

  • 間違ったバインディング名で重複したバインディングエントリを追記し、コードが実際に使うバインディング(DBSTATE など)はプレースホルダーの id のまま放置される。

  • ファイル全体を再フォーマットする -- 2 スペースインデントがタブになり、配列が展開され、末尾の改行が削られる。

代わりに、コマンドが出力した id / database_id をコピーして、既存のバインディングに手で貼り付ける:

{
  "d1_databases": [
    {
      "binding": "DB",
      "database_name": "my-app-db",
      "database_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    }
  ]
}

続くプロンプト「For local dev, connect to the remote resource?」も N にする -- ローカル開発はローカルストレージに対して行うのがデフォルトとして正しい。

アセットディレクトリに _worker.js を出力するアダプター

フレームワークのアダプター(例:@takazudo/zfb-adapter-cloudflare)は、Worker エントリ dist/_worker.js を、静的アセットとして配信されるのと同じ dist/ ディレクトリに書き出すことがある。すると wrangler deploy はハードエラーになる:

Uploading a Pages _worker.js file as an asset

修正は、Wrangler から見たアセットディレクトリのルート.assetsignore ファイルを置き、Worker エントリと、アセットと同じ場所に出力される静的インポートされたサーバーモジュールを列挙すること:

_worker.js
_zfb_inner.mjs

フレームワークがビルドのたびに public/ ディレクトリを dist/ にコピーするなら、.assetsignorepublic/ に置く -- 自動的にデプロイされるアセットルートに配置されるので、ローカルデプロイと CI の両方で修正が永続する(次のビルドで消されることがない)。

これに当たるのはアダプターがアセットディレクトリに出力するビルドだけ

main がアセットディレクトリのにあるアプリ Worker は、このエラーを決して踏まない。assets.directory として登録されたディレクトリにサーバーエントリを出力するアダプター特有の問題だ。

CI トークン:「Edit Cloudflare Workers」では足りない

CI デプロイ用に API トークンを作る際、ダッシュボードは 「Edit Cloudflare Workers」 トークンテンプレートを提示する。罠:このテンプレートには D1:Edit が含まれていない。それがないと wrangler d1 migrations apply --remote が認可エラーで失敗する(code: 7403given account is not valid or is not authorized to access this service)。

同じテンプレートは Vectorize:ReadWorkers AI:Read も、D1:Edit とまったく同じように欠けている。ただし症状は違って見える:GET /accounts/{id}/vectorize/v2/indexes/{name} のような REST アサートは、素の HTTP 403 で失敗する -- D1 の 7403 のような Cloudflare エラーコードのラッパーがない。(Vectorize の Edit でもこのチェックは通る。読み取り専用のアサートなら Read で十分だ。)

診断の経験則(正確なコードは変わりうる -- シグナルとして扱うのはメッセージの方):

  • D1 コマンドでの認可エラー(例:7403)→ トークンに D1 権限が欠けている。

  • 「アカウントが見つからない」系のエラー(例:7404)→ 間違っているのはアカウント id であって、トークンではない。

  • REST アサートでの素の HTTP 403(エラーコードのラッパーなし)→ トークンにそのサービスの Read 権限が欠けている(Vectorize、Workers AI など)-- D1 のラップされた 7403 とはシグネチャが違う。

  • トークンは認証を通るが何も見つからないCLOUDFLARE_ACCOUNT_ID がそのトークンを発行したのと同じアカウントに属しているか確認する。アカウント A のトークンとアカウント B のアカウント id を組み合わせると認証はすんなり通り、その後すべてのルックアップが失敗する -- 権限バグに見えるが実際は違う。

  • 汎用の Authentication error [code: 10000]→ それぞれ別の対処が必要な、3 つの異なる原因がある:

原因対処
トークンが期限切れ、または失効している新しいトークンを発行するか、既存のトークンを再度有効化する
シークレットの値が壊れている(コピペ時の末尾の空白など)CI が実際に読む場所で値を再設定する -- GitHub Actions なら gh secret set CLOUDFLARE_API_TOKEN、ローカルデプロイならシェル環境変数
Account Settings: Read が欠けているトークンの権限を編集して追加する

wrangler secret put は壊れた CI トークンの修正にはならない

wrangler secret put はそれ自体が CLOUDFLARE_API_TOKEN を使って認証するコマンドだ -- その値が壊れている・失効しているものだった場合、書き込みに入る前にコマンド自体が失敗する。しかもこのコマンドが書き込むのはランタイムの Worker シークレット(コード内で env.SOME_KEY として読むもの)であり、CI のクレデンシャルではない -- うまくいっても CI には何も効かず、最悪の場合は CI が古い壊れた値を使い続けたまま、デプロイトークンを Worker コードに晒すことになる。壊れた・ローテーションした CI トークンは、CI が実際に読む場所で直す:GitHub Actions なら gh secret set CLOUDFLARE_API_TOKEN、ローカルデプロイならシェル環境変数。

Workers + D1 + KV + Vectorize + Workers AI スタックで動作する権限セット(実際に使うものに絞ってよい):

ScopePermission
Account -- Workers ScriptsEdit
Account -- D1Edit
Account -- Workers KV StorageEdit
Account -- VectorizeRead
Account -- Workers AIRead
Account -- Account SettingsRead
Zone -- Workers RoutesEdit(カスタムドメイン使用時のみ)
Zone -- DNSEdit(カスタムドメイン使用時のみ)

より大規模で、実際に検証済みのスタック -- Workers + D1 + R2 + Queues + Vectorize + Workers AI、routescustom_domain: true を指定してデプロイする場合 -- で動作した完全な権限セットは次の通り:

ScopePermission
Account -- Workers ScriptsEdit
Account -- D1Edit
Account -- Workers R2 StorageEdit
Account -- QueuesEdit
Account -- Workers KV StorageEdit(オプション -- 上のセットから維持しているだけ。実際に使うものに絞ってよい)
Account -- VectorizeRead
Account -- Workers AIRead
Account -- Account SettingsRead
Zone -- Workers RoutesEdit
Zone -- ZoneRead
User -- User DetailsRead

トークンとアカウント id は GitHub Actions のシークレット CLOUDFLARE_API_TOKENCLOUDFLARE_ACCOUNT_ID として保存する。

カスタムドメインにはゾーン権限が 2 つ 必要で、Zone Resources にゾーンを含める必要もある

custom_domain = true は DNS レコードとエッジ証明書を Cloudflare が作成・管理する仕組みなので、Zone -- Workers Routes: Edit だけでは足りない — Zone -- DNS: Edit が実際に効いている。両方を付与すること。

同じくらい見落としやすいのが、Zone Resources が空のままではゾーン権限を足しても何も起きない点。権限の行とリソースのスコープは別々の設定であり、ゾーンの行が 2 つ揃っていてもゾーンが含まれていないトークンは、毎回のデプロイでルート作成時に失敗する。

最小限のゾーン権限セットは固定ではない -- wrangler のバージョンとルートのモードに左右される

上の実証済みスタックは Workers Routes: Edit + Zone: Read だけ -- DNS:Edit なし -- でも問題なくデプロイできた。これは上の警告と真っ向から矛盾しているように見える。しかしどちらの観測も本物だ:最小限のゾーン権限セットは wrangler のバージョンと、ルートのモード(ダッシュボードでアタッチしたルートと custom_domain: true とでは挙動が違う)によって変わる。迷ったら Workers Routes: Edit + DNS: Edit + Zone: Read を、Zone Resources に実際のゾーンを含めた状態で付与し、デプロイが通ることを確認してから、1 つずつ権限を削って実際の最小構成を探るとよい。

手動のステップと引き換えに、ゾーン権限そのものを避ける方法もある

CI トークンのゾーン権限が繰り返しトラブルの種になるなら、それを完全に避ける方法がある:workers_dev = false のままにし、コミット済みのファイルにルート設定を一切宣言しない -- wrangler.jsonc なら "routes": [...] 配列、wrangler.toml なら [[routes]] テーブル、どちらも書かない -- そしてカスタムドメインはダッシュボード経由で一度だけ(またはパイプラインの外で一度だけ CLI を叩いて)アタッチする。こうすれば wrangler deploy はゾーン API に一切触れないので、CI トークンにゾーン権限はゼロで済む。これが成り立つのは、どちらの記法でもルート設定が一切残っていない場合だけだ -- どちらか一方でも書き戻せば、ゾーン API はまた関与してくる。トレードオフは、ドメインのアタッチが手動の一度きりのオペレーター作業になり、バージョン管理されないことだ。これはルート設定を書く経路に代わるものではなく、並行して使える選択肢として捉えるとよい。

トークンの権限編集はシークレットを維持するが、新規トークン作成はそうではない

既存のトークンに権限を追加しても、トークン文字列は変わらない -- CI のシークレットを更新する必要はない。新しいトークンを作る場合はそうはいかない -- 新しい値は CI が読む場所で再設定しなければならない:GitHub Actions なら gh secret set CLOUDFLARE_API_TOKEN、ローカルデプロイならシェル環境変数。新しく作ったトークンに足りない権限を付与して、なぜ CI が失敗し続けるのか悩んでしまうのはよくある落とし穴だ -- パイプラインはまだ古いシークレットの値で認証しているのだから。

割り当て直後のカスタムドメインはすぐには使えない

wrangler deploy はルートを作成した時点でカスタムドメインを報告する:

Deployed my-worker triggers (1.12 sec)
  my-worker.example.com (custom domain)

この行が意味するのはルートが存在することであって、ホスト名が動作することではない。その後 1 分ほどは、レコードと証明書が落ち着くまでエッジが Cloudflare 自身のエラーページ(error code: 1104 や 5xx)を返しうる。数秒後に走るデプロイ後チェックは、サイトではなくこのウィンドウと競走している。

現れる失敗は 2 種類あり、どちらもアプリのバグと誤診しやすい:

AAAA が A より先に来る。 Cloudflare は IPv6 レコードを先に公開する。GitHub Actions のランナーには IPv6 の経路がないため、その隙間の接続はすべて ENETUNREACH で失敗する — Worker 自体は完全に正常なのに。*.workers.dev の URL は影響を受けないので、そちらが 200 を返すなら Worker は正常で、見ているのは伝播の問題だと確認できる。

Node はこれを Happy Eyeballs 経由で AggregateError として報告するが、その集約エラー自体は code を持たないことがある — 実際のアドレスごとのコードは .errors[] にぶら下がる。.cause チェーンだけを辿る分類器は、これを完全に見逃す:

function codes(error) {
  const out = [];
  for (let e = error; e; e = e.cause) {
    if (e.code) out.push(e.code);
    if (Array.isArray(e.errors)) out.push(...e.errors.flatMap(codes)); // <- 必須
  }
  return out;
}

オリジンが繋がる前にエッジが 5xx を返す。 ホスト名は解決し TLS も完了するが、Cloudflare が自前のエラーページを返す。割り当て直後のドメインへの最初のプローブで 5xx を受けたら「まだ準備できていない」と扱い、成功レスポンスとして扱ってはいけない。

緩和は範囲を絞る。さもないと盲点になる

リトライして許容してよいのは最初のプローブだけ、しかも 1 回成功するまでの間だけである。ホストが一度応答すれば、それはデプロイ済みであることの証明であり、以降の失敗は本物の障害なので致命的なままにしなければならない。全リクエストに適用される緩和は、本物の障害を静かな成功に変えてしまう。

CI はコミット済みの設定からデプロイする

CI は main への push で、コミット済みwrangler.jsonc / wrangler.toml を使って再デプロイする -- ファイルをローカルで編集するだけでは足りない。コミット済みの設定に REPLACE_WITH_* プレースホルダーが残っていれば、ローカルのファイルがどれだけ正しくても CI は失敗し続ける。このハードな失敗はデフォルトの挙動であって、プラットフォーム側の要件ではない -- テンプレートやフォークのリポジトリなら、代わりに優雅に失敗させる選択肢を取れる。セルフスキップ・プリフライトパターンについてはテンプレートリポジトリの CIを参照。

本物の id はコミットする -- ただしシークレットは絶対にしない

コミット済み設定に属するもの:バインディング id(D1 の database_id、KV ネームスペースの id)、公開してよい vars(例:Firebase Web API キー -- 設計上公開情報)、Worker の namecompatibility_date

絶対にコミットしてはいけないもの:本物のシークレット、API トークン、環境固有のクレデンシャル。これらは wrangler secret put か GitHub Actions のシークレットを通す。

動作確認チェックリスト

プロビジョニング完了と呼ぶ前に確認する:

  • Worker は wrangler deploy で作成された(孤児のダッシュボード Worker も、競合する git ビルドパイプラインもない)

  • wrangler.jsonc / wrangler.toml に最終的な Worker name と本物のバインディング id が入っていて -- REPLACE_WITH_* プレースホルダーがなく -- コミット済みである

  • .assetsignore が出力されるアセットルートに存在する(フレームワークがビルド出力にコピーするなら public/ 経由で)

  • D1 マイグレーションがデプロイと同じ CLOUDFLARE_ACCOUNT_ID で実行される

  • CI トークンに、D1 を使うなら D1:Edit、Vectorize を使うなら(デプロイ前アサートを含め)Vectorize:Read、Workers AI を使うなら(デプロイ前アサートを含め)Workers AI:Read が含まれている -- さらに必要に応じて KV / R2 / Queues / Routes 権限も -- 「Edit Cloudflare Workers」テンプレートのままではない

  • シークレットは wrangler secret put / GitHub シークレットで設定されている -- コミット済み設定の中には 1 つもない

関連ページ:設定フォーマットは Wrangler 設定、アセットモデルは Workers Static Assets、CI ワークフローは本番デプロイ

Revision History

作成更新