跳至正文
Cloudflare — Dynamic Workers:Worker Loader、受控 Binding 与安全执行

Dynamic Workers:Worker Loader、受控 Binding 与安全执行

AI 参与说明(Agent:Codex;模型:gpt-5.6-terra;reasoning effort:ultra):本文由 Codex 根据作者提供的主题、对话素材与 Cloudflare 官方文档辅助生成,属于 AI 生成/整理内容,非作者原创。请读者自行甄别并交叉验证。 本次由 Codex(模型:gpt-6-astra;reasoning effort:medium;执行入口:Codex Desktop;Codex CLI 版本:0.153.4;提供方:OpenAI)于 2026-09-07 修订示例与适用条件;代码按文中说明区分演示骨架和实际验证范围。

这是一篇“把 API 放回运行时”的实战页。Dynamic Workers 不是把一段字符串直接执行掉:先部署一个可信的 Loader Worker;Loader 再决定动态代码、它可拿到的 binding、网络出口和资源上限。真正重要的接口只有三层:WorkerLoaderWorkerCodeWorkerStub / entrypoint

Dynamic Workers 仍在快速演进,本文按 2026-08-07 的官方文档整理。它目前面向 Workers Paid;部署前应重新核对限额、价格与 beta 状态。

先记住对象关系

flowchart LR
  U["用户请求"] --> H["可信 Loader Worker"]
  H --> A["认证、租户解析、代码版本选择"]
  A --> L["env.LOADER.load() 或 get()"]
  L --> C["WorkerCode:modules + bindings + 网络策略"]
  C --> S["WorkerStub"]
  S --> E["getEntrypoint()"]
  E --> D["动态 Worker 的 fetch / RPC 入口"]
  D --> B["仅被显式授予的 capability"]

  H -. "ctx.exports Service Binding" .-> B

不要把 get(id) 当作“拿到一个常驻进程”。它只是尽力复用已加载 isolate 的缓存:同一个 ID 的两次请求也可能进入不同 isolate,getCodeCallback 也可能在以后再次调用。动态代码必须把可靠状态放在 Durable Objects、D1、R2 等外部能力中。

1. 配置与最小调用

先在宿主 Worker 配置一个 Loader binding:

jsonc
{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "tenant-code-host",
  "main": "src/index.ts",
  "compatibility_date": "2026-08-07",
  "worker_loaders": [
    { "binding": "LOADER" }
  ]
}

随后,宿主在每次请求中创建或取得一个 WorkerStub,再调用它的默认 entrypoint:

ts
export default {
  async fetch(request, env) {
    const worker = env.LOADER.get(
      "greeting:v3:7f61e2",
      async () => ({
        compatibilityDate: "2026-08-07",
        mainModule: "index.js",
        modules: {
          "index.js": [
            "export default {",
            "  fetch() {",
            "    return new Response('hello from dynamic code');",
            "  }",
            "};",
          ].join("\n"),
        },
        globalOutbound: null,
        limits: { cpuMs: 50, subRequests: 10 },
      }),
    );

    return worker.getEntrypoint().fetch(request);
  },
};

get() 本身同步返回 stub,不需要 await;若代码尚未装载,首次向 stub 发出的请求会等待装载完成。装载失败时,实际调用会抛错。

2. API 速查:应该在什么时机调用谁

对象接口使用方式与边界
env.LOADERload(code)每次调用创建一个新的 Dynamic Worker;适合一次性、每次代码都不同的 AI 工具调用。
env.LOADERget(id, getCodeCallback)用不可变版本 ID 尽力复用代码/isolate;适合同一租户同一版本的小应用。
WorkerStubgetEntrypoint()取得默认模块 entrypoint;通常接着调用 fetch(request)
WorkerStubgetEntrypoint(name)取得命名的 WorkerEntrypoint,用于明确暴露的 RPC API。
WorkerCodemodulesmainModule给出入口模块和已打包的依赖图;这不是运行时 TypeScript 编译器。
WorkerCodeenv把结构化可克隆数据或 Service Binding 作为动态代码的 env
WorkerCodeglobalOutbound决定全局 fetch()/connect() 是继承、拒绝还是被网关接管。
WorkerCodelimitstails为不可信代码设置 CPU/subrequest 上限,并投递日志和异常到 Tail Worker。

WorkerCode 还可携带 compatibility flags 和实验开关;应把它们也视为版本的一部分。只要代码、依赖、权限、bindings、compatibility 配置任一项变化,就必须换新的 get() ID,例如 tenant-42:editor:v17:content-hash

3. load() 还是 get()

场景选择ID 原则
LLM 刚生成、只运行一次的工具调用load()不需要缓存身份。
一个租户的固定版本预览应用get()租户 + 版本 + 内容 hash。
同一代码但权限不同get()权限也进入 ID;不能共用旧 ID。
调试期间每次都改 bundleget()每次构建产生新 hash。

错误做法是把租户 ID 单独作为 ID,却在 callback 中根据“当前最新版本”返回不同代码。恢复或重新装载时,缓存语义会变得不确定,也可能把旧会话悄悄切到新权限。

4. 不要把 KV、密钥或数据库直接交给动态代码

最小权限的核心是:动态 Worker 得到的是业务 capability,不是原始 Cloudflare binding。下面的宿主 TenantStore 在可信 Worker 内运行,因此可进行鉴权、租户隔离与审计;动态代码只能调用它允许的 readProfile()

ts
import { WorkerEntrypoint } from "cloudflare:workers";

export class TenantStore extends WorkerEntrypoint {
  async readProfile() {
    const tenantId = this.ctx.props.tenantId;
    return this.env.PROFILES.get(tenantId + ":profile");
  }
}

export default {
  async fetch(request, env, ctx) {
    const tenantId = await authenticateAndGetTenantId(request);
    const version = await getPublishedVersion(tenantId);

    const worker = env.LOADER.get(
      "tenant:" + tenantId + ":v:" + version,
      async () => ({
        compatibilityDate: "2026-08-07",
        mainModule: "index.js",
        modules: {
          "index.js": await loadBundledCode(tenantId, version),
        },
        env: {
          STORE: ctx.exports.TenantStore({
            props: { tenantId },
          }),
        },
        globalOutbound: null,
        limits: { cpuMs: 50, subRequests: 10 },
      }),
    );

    return worker.getEntrypoint().fetch(request);
  },
};

动态代码一侧只需了解自己被授予的接口:

ts
export default {
  async fetch(_request, env) {
    const profile = await env.STORE.readProfile();
    return Response.json({ profile });
  },
};

这样做有三个直接好处:

  1. 动态代码看不到 PROFILES binding,也看不到宿主密钥。
  2. 宿主可以对每个方法做参数验证、配额、日志和对象级授权。
  3. 将来改变存储实现时,动态代码不需要知道 KV、D1 或 DO 的细节。

5. 网络是另一个 capability,不是默认便利功能

如果省略 globalOutbound,Dynamic Worker 会继承 Loader 的网络能力,通常意味着可访问公共互联网。对于用户上传或 AI 生成的代码,默认值应是:

ts
globalOutbound: null

只有确实需要请求第三方时,才把全局出站指到宿主的网关 Service Binding。网关负责 allowlist、方法限制、请求大小、速率、审计以及在宿主侧注入密钥;动态代码只使用普通 fetch(),永远不接触 token。

ts
export class OutboundGateway extends WorkerEntrypoint {
  async fetch(request) {
    const url = new URL(request.url);
    // 示例只授予一个只读端点;其他服务使用各自的策略和凭据。
    if (
      url.origin !== "https://api.example.com" ||
      url.pathname !== "/v1/profile" ||
      url.search !== "" ||
      url.username !== "" ||
      url.password !== "" ||
      request.method !== "GET"
    ) {
      return new Response("destination not allowed", { status: 403 });
    }

    // 只构造所需请求头,不转发来路 Cookie 或其他敏感头。
    return fetch(url.href, {
      method: "GET",
      headers: {
        accept: "application/json",
        authorization: "Bearer " + this.env.PARTNER_TOKEN,
      },
      redirect: "error",
    });
  }
}

// WorkerCode 中:
// globalOutbound: ctx.exports.OutboundGateway({ props: { tenantId } })

这个片段只演示目的地和凭据转发策略,未在真实服务部署。固定端点还必须在宿主中按 this.ctx.props.tenantId 及业务对象做授权;共用服务凭据不能替代租户级权限。本地使用虚构 token 和模拟 fetch 检查了允许请求、HTTP、非默认端口、错误路径及方法等分支。

只检查 hostname 不够:HTTP 和其他端口也可能匹配;自动重定向还可能把敏感头发往另一域名。这里使用精确 HTTPS origin 并禁止跟随重定向;需要重定向时,必须逐跳重新检查目标和凭据范围。Cloudflare 官方明确说明 redirect: “follow” 可能跨域转发 Authorization 等请求头。Workers Request

6. 模块、入口、状态与可观测性

  • modules 可承载 JavaScript、CommonJS、Python、文本、数据和 JSON 模块;普通字符串源文件要使用正确的扩展名。
  • TypeScript 与 npm 依赖必须在交给 Loader 前完成转译和 bundle。不要期待生产请求在运行时安装依赖。
  • Python 需要相应的 python_workers compatibility flag,且启动特性与 JavaScript 不同,应单独压测。
  • limits 可在 WorkerCode 或 entrypoint 调用处收紧;有效上限取更小值。遇到超过 CPU/subrequest 上限的异常时,按失败处理而不是无限重试。
  • 使用 tails 将日志、异常和请求执行信息送到 Tail Worker;日志至少带 tenant ID、代码版本与请求 ID,但不要写入原始密钥或用户敏感数据。
  • 动态代码若真正需要按实体持久化/串行协调,应考虑 Durable Object Facets:概念、使用场景与 Durable Objects 对比 或宿主提供的受控 DO capability,而不是依赖 isolate 内存。

7. 上线前最短检查单

  • get() 的 ID 同时包含代码、依赖、权限和 compatibility 配置的版本信息。
  • 不可信代码的 globalOutbound 显式设为 null,或明确指向审计网关。
  • 动态代码得到的是小型 RPC capability,而不是原始 KV、R2、D1、DO 或 secret。
  • 代码在部署前已 bundle;TypeScript、npm 依赖和 Python flag 都已验证。
  • CPU、subrequest、日志、租户配额和异常处理有明确策略。
  • 任何需要可靠状态的设计都已移到 DO / D1 / R2 等持久层。

参考资料

本文共 2162 字,创建于 Aug 7, 2026
博客助手

正在打开博客助手…