Skip to Content
平台維運平台佈建

平台佈建

dispatcher Worker 只能經由 edge/department-workers/scripts/deploy-platform.mjs 佈建(刻意排除在開發者 npm 套件之外)。dispatcher 綁定三個 dispatch namespace 位置(見〈環境契約〉),每一個都沒有綁定 Outbound Worker — 部門 Worker 的對外網路存取不受限制 — 而腳本會拒絕任何試圖加上 outbound binding 的設定:

node scripts/deploy-platform.mjs --environment staging|production [--check|--dry-run|--drift] [--allow-api-overwrite]

模式最多只能給一個(--check、--dry-run、--drift;都不給就是真正的部署)。--allow-api-overwrite 與 --check 或 --drift 一起使用會被拒絕;任何其他參數都會以 unsupported argument: <arg> 失敗。

自後端 v5.10.10 起,staging 部署與 production release 會在 dispatcher 原始碼漂移時自動執行它,並排在後端之前(見下方〈CI:dispatcher 先於後端部署〉);漂移蓋章看不到的變更,仍要靠手動執行出貨。

必要環境(Infisical 範圍)

EDGE_WORKERS_CLOUDFLARE_API_TOKEN(機密)、…_ACCOUNT_ID(32 hex)、…_DISPATCH_NAMESPACE(共用位置的 namespace,或該環境自己的舊版 namespace — 其他值一律以 EDGE_WORKERS_CLOUDFLARE_DISPATCH_NAMESPACE does not match the selected environment 失敗)、…_WORKERS_SUBDOMAIN(DNS 標籤)、…_DISPATCH_KEY_ID、…_DISPATCH_ISSUER、…_DISPATCH_AUDIENCE、…_DISPATCH_PUBLIC_JWK(Ed25519、僅供 verify、kid 必須等於 key id)、…_DISPATCH_PRIVATE_KEY(機密、PKCS8 PEM)、…_DISPATCH_ORIGIN(https://<dispatcher>.<subdomain>.workers.dev,恰好四個標籤),以及 EDGE_WORKERS_R2_BUCKET,即後端已在使用的 edge 產物 bucket(缺少或為佔位符:EDGE_WORKERS_R2_BUCKET is not configured safely;不是合法的 bucket 名稱:EDGE_WORKERS_R2_BUCKET must name the hosted edge artifacts bucket)。另有一個選填輸入:EDGE_WORKERS_SERVING_ROUTES,即啟用對照表(見下)。

硬性預檢失敗:任何佔位符值(REPLACE_WITH_ENV_*、CHANGE_ME、DEPLOY_VIA_SCRIPT_ONLY);私鑰推導出的公鑰 ≠ 公開 JWK(以一次實際測試簽章證明);套件目錄裡只要存在任何 .env / .dev.vars* 檔案(憑證只能來自當前行程環境);注入後設定檔仍含 JWK 哨兵值。

環境契約

環境Dispatcher Worker舊版 outbound Worker(2026-09-03 已在兩個環境退役;此步驟現在只是一次唯讀 GET)
stagingteamsync-department-worker-dispatcher-stagingteamsync-department-worker-outbound-deny-staging
productionteamsync-department-worker-dispatcher-productionteamsync-department-worker-outbound-deny-production

兩個環境的 dispatcher 綁定同樣三個 dispatch namespace 位置,各用自己的 binding 名稱:

Binding位置
DEPARTMENT_WORKERS共用位置 — 部署上傳 canary 的位置
DEPARTMENT_WORKERS_LEGACY_STAGING舊版 staging 位置
DEPARTMENT_WORKERS_LEGACY_PRODUCTION舊版 production 位置

dispatcher 設定在選定環境中必須恰好列出這三個 binding,各出現一次且 namespace 符合腳本契約(否則 Wrangler <env> topology does not match the TeamSync platform contract),而且任何 binding 上都不得帶 outbound 鍵(頂層、env.staging、env.production 皆然)。綁了 outbound Worker 的設定會以 Wrangler <env> topology binds an outbound Worker; department Workers must keep unrestricted egress 驗證失敗,所以帶閘門的位置不可能出貨。舊版 Worker 欄位只為了讓部署能退役早期拓撲部署過的全拒 Worker;退役後不再有任何東西參照它。

腳本不會建立 dispatch namespace:請在這個 dispatcher 第一次部署之前先建立共用位置。某個部門 Worker 由哪個位置服務,不是由這裡決定,而是 dispatcher 在請求當下決定:有啟用(啟用對照表條目,或後端搬移 Worker 時記錄的啟用)就只問被釘定的位置,斷言中指名了 Worker 位置的簽章呼叫則受該位置約束(調度協定);兩者皆無則依序嘗試舊版 staging、舊版 production、共用位置(調度協定)。

子網域釘定(任何變更之前)

一次線上 GET /accounts/{id}/workers/subdomain:帳號的 workers.dev 子網域必須等於 EDGE_WORKERS_CLOUDFLARE_WORKERS_SUBDOMAIN,且 EDGE_WORKERS_DISPATCH_ORIGIN 必須恰為該帳號上該環境的 dispatcher。之後後端閘道在簽任何租戶請求前也要求同一個被認證的標籤 — 兩側各自獨立釘住同一個 origin。(後端依自己的 API 環境挑選預期的 dispatcher,而不是依 namespace 變數。)

啟用對照表(EDGE_WORKERS_SERVING_ROUTES)

選填輸入 EDGE_WORKERS_SERVING_ROUTES 會被寫入 dispatcher 變數 EDGE_WORKER_SERVING_ROUTES(注意是單數 WORKER)。它是由伺服端擁有的啟用對照表,對公開 URL 與簽章請求都適用:一個 JSON 物件,從 script 名稱對應到唯一負責服務它的位置。這裡的條目優先於後端在搬移 Worker 時記錄的啟用。自後端 5.22.0 起,後端會在簽章呼叫中指名該 Worker 已記錄的位置,所以為尚未被搬移的 Worker 所設的條目(它的紀錄是 epoch 0,而對照表條目的 epoch 至少要是 1),或其他不等於後端紀錄的條目,都會讓該 script 的這些呼叫回 409 serving_target_mismatch。

{ "dw-<48 hex>": { "dispatch_namespace": "<bound namespace>", "routing_epoch": 1 } }
規則值
大小≤ 32 768 字元(32 KiB)
條目≤ 128
鍵script 名稱:dw- 加 48 個小寫 hex 字元
值鍵恰好是 dispatch_namespace 與 routing_epoch
dispatch_namespace三個已綁定 namespace 之一
routing_epoch安全整數 ≥ 1

違反任何一條,腳本會以 serving routes configuration is invalid 失敗(不是 JSON 物件的文字:serving routes is not valid JSON)。腳本會把對照表重新序列化為緊湊格式並依鍵排序後再安裝;之後的讀回比對的就是這份正規化文字。dispatcher 在每個請求都重新檢查同樣規則並 fail-closed:對照表格式錯誤時,每個需要查找的請求都回 503 dispatcher_unavailable。

  • 沒有明確值的部署會保留線上對照表。 --check、--dry-run 與部署會先讀線上 dispatcher 的 settings,並把它目前的 EDGE_WORKER_SERVING_ROUTES 帶過去(不存在視為 {};這類 binding 超過一個,或不是 plain_text,則以 serving routes readback is invalid 失敗),所以例行的原始碼部署不會重設任何啟用。這次讀取不重試:HTTP 429 或 5xx 會以 Cloudflare serving routes readback returned a retryable Cloudflare response 失敗。
  • 明確的值會整份取代線上對照表(不合併)。啟用是營運者的明確動作;wrangler.jsonc 內附帶的 {} 只是預設值,永遠不會抹掉線上的對照表。腳本判斷的是變數是否不存在,所以空字串算「已設定」,並會因 JSON 無效而失敗。
  • 全新的 dispatcher 首次部署需要設定這個變數。 保留線上值的那次讀取不容許 dispatcher 不存在:在該環境的 dispatcher 從未部署過的帳號上,除 --drift 外的每個模式都會以 Cloudflare serving routes readback returned HTTP 404 失敗,直到設定 EDGE_WORKERS_SERVING_ROUTES(首次部署設為 {})。
  • 對照表不是 digest 的輸入。 變更它永遠不會顯示為漂移(見〈原始碼 digest 蓋章與 --drift〉)。
  • 診斷。 在共用拓撲中,dispatcher 會在 Worker(或退役墓碑)產生的每個回應上蓋 x-teamsync-serving-namespace 與 x-teamsync-routing-epoch,並覆蓋 Worker 自己回傳的同名標頭;它們只是平台診斷用途 — 不要據此建立邏輯。

兩個 Wrangler 階段 + 退役(deploy 模式)

  1. Dispatcher — 來自暫存設定檔(wx、0600):哨兵值換成真正的公開 JWK、啟用對照表與原始碼 digest 分別蓋成 EDGE_WORKER_SERVING_ROUTES 與 EDGE_DISPATCHER_SOURCE_DIGEST、env 縮到選定環境,然後過圍欄 — 序列化文字不得包含私鑰、Cloudflare token 或任何佔位符(outbound 鍵早在寫出暫存檔之前就已被 validatePlatformConfigs 拒絕)。
  2. Dispatcher settings 讀回(Cloudflare API;僅 deploy 模式)— GET /accounts/{id}/workers/scripts/{dispatcher}/settings 必須恰好有環境契約規定的三個 dispatch_namespace binding(否則 deployed dispatcher does not bind the exact shared and legacy namespaces)、其中任何一個都沒有 outbound,並且恰好有一個 plain_text 的 EDGE_WORKER_SERVING_ROUTES,內容等於設定的或保留的對照表(否則 deployed dispatcher serving routes do not match the activation configuration)。任何失敗都會在動到舊版 Worker 之前讓部署失敗(殘留 outbound Worker 時:deployed dispatcher still binds an outbound Worker; refusing to retire the legacy deny-all Worker),舊拓撲原封不動。這道閘門的由來:設定檔沒有 outbound 時,Wrangler 的資源佈建會把已部署的 dispatch namespace binding 以 type: "inherit" 上傳,等於保留遠端的 outbound Worker;所以部署改以 --no-experimental-provision 執行 Wrangler,並以這次讀回證明 binding 真的變了。(2026-09-03 沒有這道閘門的順序在 dispatcher 仍參照舊 Worker 時就把 staging 的全拒 Worker 刪掉,所有部門 Worker 呼叫都回 502,直到 binding 被修復為止。)
  3. 退役舊版全拒 Outbound Worker(Cloudflare API,不是 Wrangler;僅 deploy 模式)— GET /accounts/{id}/workers/scripts/{legacy_name}/settings;不存在(404/錯誤碼 10007)→ 跳過;存在 → DELETE /accounts/{id}/workers/scripts/{legacy_name}?force=true,再以有界重試讀回同一個 settings GET 直到回報不存在。失敗會在 canary 執行前中止部署,所以 canary 永遠不會量到退役到一半的拓撲。Worker 消失之後,這一步在往後每次部署都只是一次唯讀 GET。
  4. Canary — 一個可拋棄式 Worker,以推導的 script 名稱(隨機部門 UUID;公司 UUID 在 production 是隨機的,在 staging 是固定的測試公司)上傳進共用位置:先證明不存在、上傳、以有界重試讀回 settings,然後帶真正的 30 秒簽章斷言走簽章 dispatcher 通道呼叫(最多六次嘗試,等待 0、1、2、4、8、12 秒,每次嘗試上限 5 秒,因為新 Worker 可能在控制平面已可見時,edge 路由尚未收斂)。成功要求:HTTP 200、精確的隨機標記,以及 canary 自己對 https://www.cloudflare.com/cdn-cgi/trace 的外部 fetch(不跟隨轉址、body 上限 4 KiB)回 200 且含 h=www.cloudflare.com 一行的證明 — 回報為 outbound_status: 200、outbound_target: "www.cloudflare.com" — 加上合法的 x-teamsync-request-id。一個請求同時證明了簽章驗證、script 名稱推導、namespace 路由與不受限制的對外連線;而且 canary 沒有啟用(對照表中沒有條目,後端也沒有記錄)、只存在於共用位置,所以要在兩個舊版位置都回報找不到之後才會被查到,通過也就同時證明了這段往下一個位置的查找。若收到 403 outbound_fetch_denied(殘留的 Outbound Worker)、任何非 200 或非預期的 body,canary 會以 502 回 outbound_denied/outbound_unreachable/outbound_unexpected,部署則以 signed dispatcher canary did not prove assertion, namespace routing, and unrestricted outbound 失敗。清理(刪除 + 讀回不存在)在 finally 執行,即使 Wrangler 曖昧失敗也一樣。

Wrangler 以 --strict --no-experimental-provision、shell: false、凍結的六變數允許清單環境執行(CI、CLOUDFLARE_ACCOUNT_ID、CLOUDFLARE_API_TOKEN、NODE_ENV、PATH、WRANGLER_SEND_METRICS=false)— 沒有 HOME、proxy、自訂 CA、Infisical 或簽章金鑰;輸出上限 2 MiB 且經遮罩(任何 ≥8 字元的機密 → [REDACTED]);10 分鐘逾時,SIGTERM 後 SIGKILL。

--check vs --dry-run vs --drift vs deploy

模式環境+設定驗證線上唯讀 GETWrangler舊版 deny Worker 退役Canary
--check✔子網域釘定;另加一次保留線上對照表的 dispatcher settings GET(除非已設定 EDGE_WORKERS_SERVING_ROUTES)———
--drift設定拓樸 + 只要 …_API_TOKEN/…_ACCOUNT_ID(永遠不需要簽章金鑰)一次 dispatcher settings GET(不讀啟用對照表)———
--dry-run✔同 --check2 × wrangler deploy --dry-run(dispatcher、canary)—(不做任何 API 變更)不上傳、不呼叫
deploy✔同 --check2 次真部署先做 dispatcher settings 讀回(三個 binding、無 outbound、對照表完全一致);再 GET,若存在則 DELETE ?force=true + 讀回不存在完整簽章證明 + 清理

--allow-api-overwrite 只對 dispatcher 階段解除 --strict。用於手動以 Cloudflare API 修復 dispatcher 之後的那一次部署(否則 Wrangler 會以 「last updated via the script API」 中止);settings 讀回仍必須通過,canary 階段維持 strict。與 --check 或 --drift 一起使用會被拒絕。

--dry-run 不是真部署證明。通過的 --check、--dry-run 或部署會印出一行並以 exit 0 結束(Platform deployment preflight passed for <env>; no changes were made.、Platform Wrangler dry-run passed for <env>; no changes were made.、Platform deployment and signed dispatcher canary passed for <env>.)。失敗印出 edge_worker_platform_deploy_failed: <message>,不含堆疊或機密內容,任何模式都以 exit 1 結束。

原始碼 digest 蓋章與 --drift

每次 --dry-run 與部署都會計算 EDGE_DISPATCHER_SOURCE_DIGEST — 依序對 src/dispatcher.ts、wrangler.jsonc、package-lock.json 取 sha256,每個檔名與內容都以 8 位元組長度框住,domain 為 teamsync-edge-dispatcher-source-v1\0 — 並以純文字變數蓋進 dispatcher。--drift 讀一次線上 dispatcher 的 settings,只印一行:

dispatcher drift: in_sync|drift|unstamped live=<8 hex|none> source=<8 hex>
Exit意義
0in_sync — 線上蓋章等於 checkout
10drift,或 unstamped(蓋章機制出現之前部署的 dispatcher)
1無法證明線上狀態(讀回失敗 — 包括 dispatcher 尚不存在的 HTTP 404 — 或蓋章不是單一純文字 sha256 變數)

digest 不涵蓋 Infisical 的值(因此也不含 EDGE_WORKERS_SERVING_ROUTES),也不涵蓋 deploy-platform.mjs 本身:金鑰輪替、EDGE_WORKERS_* 變更、啟用對照表變更,或只改部署腳本之後,要從乾淨的 master checkout 手動部署。

CI:dispatcher 先於後端部署

dispatcher 部署工作流程(僅 workflow_call)由 staging 部署(environment=staging:authorize → edge-dispatcher → build-and-deploy)與 production release(authorize → edge-dispatcher → release)呼叫。它先跑 --drift;exit 0 就結束,exit 10 則依序執行 --check、含簽章 canary 的完整部署,再跑一次 --drift 且必須為 0;其他結果一律失敗。dispatcher job 失敗會擋住後端 job — 新的 dispatcher 接受舊後端,但新後端遇上舊 dispatcher 可能讓所有 edge 呼叫都回 401。每個環境同時只跑一個,執行中絕不取消。憑證經由 GitHub OIDC 從 Infisical 取得(每個環境一個唯讀身分,在 npm ci 之後才取得);GitHub 內不存放任何長期機密。這個 job 會匯出該環境的 EDGE_WORKERS_* 值,所以由 CI 觸發的部署會安裝那裡的 EDGE_WORKERS_SERVING_ROUTES(沒有設定時則保留線上對照表)— 但光是這個值變更,永遠不會觸發部署。

機密處理規則

Cloudflare token 只經由子行程環境允許清單抵達 Wrangler — 絕不進 argv 或設定檔。私鑰永不落盤;在行程內載入、用來推導/驗證公開 JWK 並簽 canary 斷言。暫存檔皆 0600 且在 finally 移除。出貨的 wrangler.jsonc 刻意不可部署(每個區段都是哨兵 JWK,頂層名稱與 namespace 為 deploy-via-script-only),手動 wrangler deploy 產出的 dispatcher 會對一切回 503。

Dispatcher 執行期變數

EDGE_WORKER_ASSERTION_PUBLIC_JWK / _ISSUER(teamsync-backend)/ _AUDIENCE(teamsync-department-workers)、EDGE_WORKER_CPU_MS(50)、EDGE_WORKER_SUBREQUESTS(500)、EDGE_WORKER_SERVING_ROUTES(出貨設定的兩個環境區段中皆為 {};不存在時視為 {};無效 → 503 dispatcher_unavailable),以及部署時蓋上的 EDGE_DISPATCHER_SOURCE_DIGEST。兩個上限變數套用於公開通道,以及沒帶 limits claim 的簽章請求 — 後端會把自己的 EDGE_WORKERS_LIMIT_* 值簽進每個斷言,並以它為準。解析 fail-closed(無效 → 需要它們的每個請求都回 503 dispatcher_unavailable)。這些上限是部門 Worker 網路流量的唯一界限:沒有 Outbound Worker、egress 允許清單或政策變數。EDGE_WORKER_SUBREQUESTS 在 2026-09-07(#1205)於 wrangler.jsonc 從 10 改為 500,但直到兩個 dispatcher 於 2026-09-19 重新部署才生效 — 在 CI job 出現之前,合併的設定變更要等到有人執行這支腳本才會生效。可觀測性:logs 全量 head sampling、traces 5%;超過上限會記錄為 edge_worker_dispatch_limit_exceeded,附 limitsSource(assertion 或 env)。

Last updated on