Cloudflare Pulumi 架构模式实战:从组件化 Worker 到版本化发布
【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills
本篇指南以仓库内 patterns.md 为骨架,系统讲解如何用@pulumi/cloudflare(v6.x)编排 Cloudflare Workers 平台资源,覆盖组件化封装、全栈绑定、多环境、队列处理、微服务、事件驱动与蓝绿/金丝雀发布等十种高频架构模式。读完本文,你将能直接复刻这些模式,把 Pulumi 的 IaC 能力与 Workers 生态结合起来,搭建可维护、可演进、可灰度发布的云上应用。
前置认知:本文模式所依赖的 Pulumi 基础
所有模式都建立在同一个 Pulumi Provider 之上,在动手前先确认以下前提(详见 README.md 与 configuration.md):
- 包与版本:TypeScript/JS 使用
@pulumi/cloudflare,本文全部示例基于 v6.x API; - 认证:优先使用 API Token(
CLOUDFLARE_API_TOKEN环境变量),而不是遗留的 API Key; - 配置:
accountId应放入 stack 配置(Pulumi.<stack>.yaml)统一管理,大多数资源都要求它; - 代码形态:使用 ES modules 时务必设置
module: true,并始终设置compatibilityDate锁定 Worker 行为; - 依赖管理:通过
*Bindings(KV/D1/R2/Queue 绑定)把存储资源连接到 Worker,绑定名必须与 Worker 代码中env访问的名称完全一致(大小写敏感)。
这些原则在下面的每个模式中都会被反复用到,是理解代码的钥匙。
组件化资源:用ComponentResource封装 Worker 应用
当应用由 KV 命名空间、Worker 脚本和自定义域名三件套组成时,可以将其封装为一个 Pulumi 组件资源,实现“一处定义、多处复用”:
class WorkerApp extends pulumi.ComponentResource { constructor(name: string, args: WorkerAppArgs, opts?) { super("custom:cloudflare:WorkerApp", name, {}, opts); const defaultOpts = {parent: this}; this.kv = new cloudflare.WorkersKvNamespace(`${name}-kv`, {accountId: args.accountId, title: `${name}-kv`}, defaultOpts); this.worker = new cloudflare.WorkerScript(`${name}-worker`, { accountId: args.accountId, name: `${name}-worker`, content: args.workerCode, module: true, kvNamespaceBindings: [{name: "KV", namespaceId: this.kv.id}], }, defaultOpts); this.domain = new cloudflare.WorkersDomain(`${name}-domain`, { accountId: args.accountId, hostname: args.domain, service: this.worker.name, }, defaultOpts); } }要点拆解:
- 类型标识:
super("custom:cloudflare:WorkerApp", ...)中的custom:前缀是自定义组件资源的约定,Pulumi 会以此作为该组件在状态中的类型; - 父级关系:
{parent: this}把内部资源挂到组件下,删除组件时会级联清理其子资源,pulumi up的输出也会按树形展示,便于查看; - 统一命名:KV、Worker、域名均以组件
name为前缀生成唯一资源名,避免命名冲突; - 对外暴露:
kv、worker、domain作为组件属性,调用方可以继续读取.id、.name等输出(Output)用于后续组装; - 域名绑定:
WorkersDomain需要service: this.worker.name指向 Worker 脚本名,这就是 Worker 与自定义域名的连接点(详见 configuration.md 中 Workers Domains/Routes 一节)。
封装完成后,创建一套完整应用只需要一行:
const app = new WorkerApp("api", {accountId, workerCode: code, domain: "api.example.com"});全栈 Worker 应用:KV / D1 / R2 一站式绑定
一个典型的全栈应用同时需要 KV(缓存/配置)、D1(关系型数据)与 R2(对象存储)。模式代码如下:
const kv = new cloudflare.WorkersKvNamespace("cache", {accountId, title: "api-cache"}); const db = new cloudflare.D1Database("db", {accountId, name: "app-database"}); const bucket = new cloudflare.R2Bucket("assets", {accountId, name: "app-assets"}); const apiWorker = new cloudflare.WorkerScript("api", { accountId, name: "api-worker", content: fs.readFileSync("./dist/api.js", "utf8"), module: true, kvNamespaceBindings: [{name: "CACHE", namespaceId: kv.id}], d1DatabaseBindings: [{name: "DB", databaseId: db.id}], r2BucketBindings: [{name: "ASSETS", bucketName: bucket.name}], });几个关键细节:
- 内容来源:
content直接读取构建产物./dist/api.js,Pulumi 不会替你做打包——它会把读到的内容原样上传(见 gotchas.md 中 “No bundler/build step” 一节),因此生产环境必须先用npm run build产出dist目录; - 绑定即依赖:
namespaceId: kv.id、databaseId: db.id这类 Output 引用会自动建立隐式依赖,Pulumi 会保证存储资源先创建、Worker 后创建,无需手写dependsOn; - 绑定命名规范:绑定名
CACHE、DB、ASSETS是 Worker 代码中env.CACHE、env.DB、env.ASSETS的入口,必须与代码一致,否则运行时会得到env.XXX is undefined; module: true:以 ES module 形式上传脚本,这是现代 Worker 的推荐形态。
完整存储资源定义(KV 键值写入、R2 的location、D1 迁移)可以参考 configuration.md。
多环境管理:按 Stack 隔离部署
Pulumi 的 Stack 天然适合区分dev、staging、prod环境,通过pulumi.getStack()获取当前栈名并注入资源命名:
const stack = pulumi.getStack(); const worker = new cloudflare.WorkerScript(`worker-${stack}`, { accountId, name: `my-worker-${stack}`, content: code, plainTextBindings: [{name: "ENVIRONMENT", text: stack}], });模式价值:
- 同一份代码通过
pulumi up -s dev/pulumi up -s prod即可分别创建my-worker-dev与my-worker-prod,互不干扰; plainTextBindings把栈名作为普通文本绑定注入 Worker,代码里通过env.ENVIRONMENT即可感知当前环境,用于日志标记、配置分支等;- 不同环境的差异化配置(如
accountId、域名、配额)放在各自的Pulumi.<stack>.yaml中,由 stack 配置统一管理。
队列驱动处理:Producer / Consumer 解耦
面向异步处理的场景,cloudflare.Queue配合生产端queueBindings与消费端queueConsumers构成完整的队列模式:
const queue = new cloudflare.Queue("processing-queue", {accountId, name: "image-processing"}); // Producer: API receives requests const apiWorker = new cloudflare.WorkerScript("api", { accountId, name: "api-worker", content: apiCode, queueBindings: [{name: "PROCESSING_QUEUE", queue: queue.id}], }); // Consumer: Process async const processorWorker = new cloudflare.WorkerScript("processor", { accountId, name: "processor-worker", content: processorCode, queueConsumers: [{queue: queue.name, maxBatchSize: 10, maxRetries: 3, maxWaitTimeMs: 5000}], r2BucketBindings: [{name: "OUTPUT_BUCKET", bucketName: outputBucket.name}], });各字段含义:
queueBindings:把队列绑定到生产端 Worker,代码中通过env.PROCESSING_QUEUE.send(message)投递消息;queueConsumers:把 Worker 注册为队列消费者,queue传入队列名;maxBatchSize控制单批最大消息数(示例为 10);maxRetries控制失败重试次数(示例为 3);maxWaitTimeMs控制最长批等待时间(示例为 5000ms);- 消费端组合:消费 Worker 可以同时绑定其他资源——示例里处理器把处理结果写入 R2
OUTPUT_BUCKET,体现“消费 + 产出”的流水线组合。
微服务与服务绑定:Worker 间直接调用
多个 Worker 组成微服务时,用serviceBindings在服务间建立直接调用通道,避免走公网:
const authWorker = new cloudflare.WorkerScript("auth", {accountId, name: "auth-service", content: authCode}); const apiWorker = new cloudflare.WorkerScript("api", { accountId, name: "api-service", content: apiCode, serviceBindings: [{name: "AUTH", service: authWorker.name}], });service字段引用目标 Worker 的name(如auth-service),与代码中env.AUTH.fetch(request)的调用方式一一对应;- 与直接拼接 URL 相比,服务绑定走内部网络、时延更低,且由平台负责服务发现;
- 绑定建立后,
apiWorker与authWorker之间即形成隐式依赖,Pulumi 会保证被调服务先部署。
事件驱动架构:统一事件总线
当多个下游需要对同一事件流做出反应时,可以引入事件队列作为总线,生产端只负责投递,消费端按需订阅:
const eventQueue = new cloudflare.Queue("events", {accountId, name: "event-bus"}); const producer = new cloudflare.WorkerScript("producer", { accountId, name: "api-producer", content: producerCode, queueBindings: [{name: "EVENTS", queue: eventQueue.id}], }); const consumer = new cloudflare.WorkerScript("consumer", { accountId, name: "email-consumer", content: consumerCode, queueConsumers: [{queue: eventQueue.name, maxBatchSize: 10}], });与“队列驱动处理”模式的区别在于意图:这里的事件队列承担总线角色(event-bus),可以挂载多个语义不同的消费者(邮件通知、指标统计、审计日志等),新增下游只需新增一个带queueConsumers的 Worker,而不改动生产端——这正是事件驱动架构的松耦合红利。
v6.x 版本化部署:蓝绿与金丝雀发布
v6.x 引入“Worker + WorkerVersion + WorkersDeployment”三段式资源,把代码版本与流量分配分离,从而支持按百分比灰度:
const worker = new cloudflare.Worker("api", {accountId, name: "api-worker"}); const v1 = new cloudflare.WorkerVersion("v1", {accountId, workerId: worker.id, content: fs.readFileSync("./dist/v1.js", "utf8"), compatibilityDate: "2025-01-01"}); const v2 = new cloudflare.WorkerVersion("v2", {accountId, workerId: worker.id, content: fs.readFileSync("./dist/v2.js", "utf8"), compatibilityDate: "2025-01-01"}); // Gradual rollout: 10% v2, 90% v1 const deployment = new cloudflare.WorkersDeployment("canary", { accountId, workerId: worker.id, versions: [{versionId: v2.id, percentage: 10}, {versionId: v1.id, percentage: 90}], kvNamespaceBindings: [{name: "MY_KV", namespaceId: kv.id}], });模式要点(与 configuration.md、api.md 的版本化章节一致):
Worker:版本容器,本身不含代码,只定义name;WorkerVersion:不可变的代码 + 配置快照(content、compatibilityDate、compatibilityFlags);WorkersDeployment:把若干版本按percentage切分流量,并在部署层统一声明绑定(KV/D1/R2 等),示例实现 10% v2 / 90% v1 的金丝雀;调整percentage到 100% 即完成全量切换(蓝绿);- 适用场景:金丝雀发布、A/B 测试、蓝绿部署;
- 不适用场景:绝大多数单版本应用应使用
cloudflare.WorkerScript——它会自动完成版本管理,无需手动维护三段资源(详见 gotchas.md 中 “v6.x Worker versioning confusion” 一节)。若发现部署后 Worker 未接到流量,先检查是否误用了缺少WorkersDeployment的三段式写法。
生成 wrangler.toml:桥接 IaC 与本地开发
Pulumi 部署时不会读取wrangler.toml(见 gotchas.md 中 “wrangler.toml not consumed” 一节),本地wrangler dev与云端配置因此容易漂移。解决思路是反向生成:用@pulumi/command在资源创建后自动写出wrangler.toml,让本地开发与生产使用同一套绑定:
import * as command from "@pulumi/command"; const workerConfig = { name: "my-worker", compatibilityDate: "2025-01-01", compatibilityFlags: ["nodejs_compat"], }; // Create resources const kv = new cloudflare.WorkersKvNamespace("kv", {accountId, title: "my-kv"}); const db = new cloudflare.D1Database("db", {accountId, name: "my-db"}); const bucket = new cloudflare.R2Bucket("bucket", {accountId, name: "my-bucket"}); // Generate wrangler.toml after resources created const wranglerGen = new command.local.Command("gen-wrangler", { create: pulumi.interpolate`cat > wrangler.toml <<EOF name = "${workerConfig.name}" main = "src/index.ts" compatibility_date = "${workerConfig.compatibilityDate}" compatibility_flags = ${JSON.stringify(workerConfig.compatibilityFlags)} [[kv_namespaces]] binding = "MY_KV" id = "${kv.id}" [[d1_databases]] binding = "DB" database_id = "${db.id}" database_name = "${db.name}" [[r2_buckets]] binding = "MY_BUCKET" bucket_name = "${bucket.name}" EOF`, }, {dependsOn: [kv, db, bucket]}); // Deploy worker after wrangler.toml generated const worker = new cloudflare.WorkerScript("worker", { accountId, name: workerConfig.name, content: code, compatibilityDate: workerConfig.compatibilityDate, compatibilityFlags: workerConfig.compatibilityFlags, kvNamespaceBindings: [{name: "MY_KV", namespaceId: kv.id}], d1DatabaseBindings: [{name: "DB", databaseId: db.id}], r2BucketBindings: [{name: "MY_BUCKET", bucketName: bucket.name}], }, {dependsOn: [wranglerGen]});模式收益与前提:
wrangler dev与生产环境使用完全相同的绑定(KV/D1/R2 名称与 ID 均来自 Pulumi Output),杜绝配置漂移;- 单一事实来源:一切以 Pulumi 配置为准,
wrangler.toml只是派生物; pulumi.interpolate负责把 Output 安全地插值进 heredoc 字符串;dependsOn确保 KV/D1/R2 创建完成后再生成配置文件,Worker 则等配置文件就绪后再部署;- 反向模式:如果你的团队以 wrangler 为事实来源(配置文件由人维护),则反过来在 Pulumi 中读取
wrangler.toml,保持同一方向上的同步即可。二者选其一,不要两套并存互不同步。
构建与部署流水线:先构建、后上传
由于 Pulumi 不会打包 Worker 代码,构建步骤必须显式编排进 IaC。用command.local.Command把npm run build纳入资源图:
import * as command from "@pulumi/command"; const build = new command.local.Command("build", {create: "npm run build", dir: "./worker"}); const worker = new cloudflare.WorkerScript("worker", { accountId, name: "my-worker", content: build.stdout.apply(() => fs.readFileSync("./worker/dist/index.js", "utf8")), }, {dependsOn: [build]});- 构建命令在
./worker目录执行,产出./worker/dist/index.js; content通过build.stdout.apply(...)延迟读取构建产物,dependsOn: [build]保证顺序正确;- 这是 gotchas.md 中 “No bundler/build step” 问题的标准解法:直接把
src/index.ts原样上传会导致 Worker 报 “Cannot use import statement outside a module”,务必先构建、后部署。
内容 SHA 强制更新:打破“误判无变更”
Pulumi 通过内容哈希判断是否需要更新 Worker。当代码仅发生空白符、注释等不影响哈希的变化时,会出现“代码改了但pulumi up显示无变更”的假象。强制更新的惯用技巧是注入一个随每次发布变化的版本号绑定:
const version = Date.now().toString(); const worker = new cloudflare.WorkerScript("worker", { accountId, name: "my-worker", content: code, plainTextBindings: [{name: "VERSION", text: version}], // Forces deployment });- 每次
pulumi up都会生成新的时间戳文本,导致绑定值变化,从而强制触发一次新部署; - 附带收益:
env.VERSION让 Worker 运行时能上报自身构建版本,便于排障与日志追踪; - 代价是每次部署都会产生新的绑定差异,适合需要确定性“每次都部署”的场景(详见 gotchas.md 中 “False no-changes detection” 一节)。
模式落地时的常见陷阱与最佳实践
把上述模式组合进真实项目时,请对照 gotchas.md 自查:
- 绑定名大小写敏感:Pulumi 侧
kvNamespaceBindings: [{name: "MY_KV", ...}]必须与 Worker 代码env.MY_KV完全一致,否则运行时报env.MY_KV is undefined; - D1 迁移不进
pulumi up:D1Database只负责建库,不会执行 SQL。需要用一个command.local.Command(如wrangler d1 execute ${db.name} --file ./schema.sql)并dependsOn: [db],再让 WorkerdependsOn: [migration],保证迁移先于部署; - API Token 权限:若遇到
authentication error (10000),说明 Token 缺少权限,需要至少授予Account.Workers Scripts:Edit与Account.Account Settings:Read; - 导入后资源“异常变更”:
pulumi import后若pulumi preview显示变更,是状态与真实资源属性不一致,需调整 Pulumi 代码与真实资源对齐(可参考 api.md 中的导入命令清单); - 始终设置
compatibilityDate:锁定 Worker 运行时行为,避免 Cloudflare 平台升级引发破坏性变更。
相关参考
本文对应仓库中的完整参考集:
- pulumi/README.md:Provider 总览、认证方式、核心原则与阅读顺序;
- pulumi/configuration.md:Worker/KV/D1/R2/Queue/Pages/DNS 等资源完整配置;
- pulumi/api.md:Output、依赖、数据源、动态 Provider、导入与密钥管理;
- pulumi/gotchas.md:常见错误、最佳实践与平台限制;
- cloudflare-deploy/SKILL.md:Cloudflare 部署技能总览,含决策树与产品索引;
- 横向对比可参考 terraform(另一套 IaC 方案)与 wrangler(CLI 部署方案)。
从单一 Worker 到事件驱动微服务、再到金丝雀发布,这十种模式覆盖了 Cloudflare 平台上从“能跑”到“可灰度、可演进”的完整路径。按需选取、组合运用,即可用纯代码把整套云上基础设施管理起来。
【免费下载链接】skillsSkills Catalog for Codex项目地址: https://gitcode.com/GitHub_Trending/skills4/skills
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考