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

This article is extracted from the chat log with AI. Please identify it with caution.

说明:本文由 Codex 根据作者提供的主题、对话素材与 Cloudflare 官方文档辅助生成,属于 AI 生成/整理内容,非作者原创。请读者自行甄别并交叉验证。

这是一篇“把 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:

{
  "$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:

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()

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);
  },
};

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

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 生成的代码,默认值应是:

globalOutbound: null

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

export class OutboundGateway extends WorkerEntrypoint {
  async fetch(request) {
    const url = new URL(request.url);
    const allowed = new Set(["api.example.com", "hooks.example.net"]);

    if (!allowed.has(url.hostname)) {
      return new Response("destination not allowed", { status: 403 });
    }

    const headers = new Headers(request.headers);
    headers.set("authorization", "Bearer " + this.env.PARTNER_TOKEN);
    return fetch(new Request(request, { headers }));
  }
}

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

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 或宿主提供的受控 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 等持久层。

参考资料#

本文共 2523 字,创建于 Aug 7, 2026

相关标签: Cloud, DevOps, TypeScript, ByAI