Alchemy v2.0.0-beta.56 深度解析:230 个 Cloudflare 资源、unsafe nuke 与 S3 状态存储
【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code
本文基于 alchemy(Infrastructure-as-Effects)v2.0.0-beta.56 版本发布说明展开,核心讲解该版本将 Cloudflare 提供商从 22 个资源扩展到 230 个的实现路径、Provider.list生命周期操作与alchemy unsafe nuke命令、GitHub.events事件源、以及 S3 后端状态存储等关键能力。读完本文,你将掌握如何用一个 Effect 程序描述完整的 Zero Trust 部署、如何用测试驱动的方式批量生成云端资源、如何在 Worker 中订阅 GitHub 事件并校验签名,以及如何为 AWS 栈配置远程状态后端。
版本概览:一次跨越 542 个文件的 Cloudflare 资源扩张
v2.0.0-beta.56 是 alchemy 的一次里程碑式发布:Cloudflare 提供商覆盖的资源类型从22 个跃升至 230 个。这一轮扩张由单个 PR 落地,累计约 10.1 万行代码、横跨 542 个文件,并新增了189 个 live-test 测试套件——每个套件都针对真实 Cloudflare 账户运行。最终覆盖超过一百个 Cloudflare 服务,包括:
- Zero Trust 全家桶:Access、Tunnels、Devices、Gateway、DLP
- 网络与流量:Magic Transit、Magic Network Monitoring、Load Balancing、Spectrum
- DNS 与安全:DNS、DNS Firewall、SSL/TLS 与证书、API Shield、Bot Management、Page Shield、Turnstile
- 边缘计算:Workers for Platforms、Snippets、Zaraz、Waiting Rooms、Logpush
- 其他长尾服务:Registrar、Addressing、Alerting 等
从仓库源码结构看,这一扩张在 Cloudflare 目录 中体现得尤为直观:Access/、Tunnel/、Devices/、Gateway/、Dlp/、MagicTransit/、MagicNetworkMonitoring/、LoadBalancer/、WaitingRoom/、Logpush/、Spectrum/、Turnstile/、ApiShield/、BotManagement/、PageShield/、WorkersForPlatforms/、Snippets/、Zaraz/、Registrar/、Alerting/等子目录均已落地为独立资源模块,每个子目录内的.ts文件即对应一个个可声明、可计划、可部署的资源类型。
用一段 Effect 代码部署完整 Zero Trust 网络
资源类型覆盖的意义不止于数量。它意味着一个完整的 Zero Trust 部署——隧道、私有网络路由、身份门控的应用、WARP 设备配置——现在只需要一个 Effect 程序。Zero Trust 资源由 Andy Jefferson 在贡献 PR 中完成:
// 一个仅能通过 WARP + Access 访问的私有网络 const tunnel = yield* Cloudflare.Tunnel("Corp", { ingress: [ { hostname: "dashboard.example.com", service: "http://localhost:3000" }, { service: "http_status:404" }, ], }); yield* Cloudflare.TunnelRoute("PrivateNet", { tunnelId: tunnel.tunnelId, network: "10.4.0.0/16", }); const allowCorp = yield* Cloudflare.AccessPolicy("AllowCorp", { name: "Allow corp users", decision: "allow", include: [{ emailDomain: { domain: "example.com" } }], }); yield* Cloudflare.AccessApplication("Dashboard", { type: "self_hosted", domain: "dashboard.example.com", sessionDuration: "24h", policies: [allowCorp.policyId], }); yield* Cloudflare.DeviceDefaultProfile("Default", { mode: "exclude", splitTunnelExclude: [{ address: "10.0.0.0/8", description: "RFC1918" }], excludeOfficeIps: true, });这段代码的要点:
Cloudflare.Tunnel声明隧道及其 ingress 规则:dashboard.example.com转发到本机 3000 端口,其余请求返回 HTTP 404;Cloudflare.TunnelRoute把10.4.0.0/16私有网段路由进隧道;Cloudflare.AccessPolicy定义"仅允许 example.com 域邮箱用户"的访问策略,返回的policyId直接注入AccessApplication;Cloudflare.DeviceDefaultProfile配置 WARP 设备默认配置文件,将 RFC1918 私网段10.0.0.0/8排除在隧道之外,并开启excludeOfficeIps。
资源之间通过tunnel.tunnelId、allowCorp.policyId等输出属性互相引用,形成依赖图。这正是 alchemy"基础设施即 Effect"的核心体验:没有 YAML,没有第二个运行时,基础设施声明与业务逻辑同处一个类型安全的 Effect 程序里(见 README)。
实现路径:测试驱动的 SDK 飞轮
230 个资源不是手工一行行写的。发布说明揭示了背后的方法论:资源由 AI Agent 集群按服务逐个实现,每个 Agent 针对 distilled——alchemy 自研的生成式 Cloudflare SDK——工作。每个 Agent 先实现一个服务的资源,然后对真实 API 做 live-test;当测试撞上类型没有覆盖的 API 行为时,修复被写进 SDK,成为类型化错误补丁(typed-error patch),而不是在消费端加一个 catch 块:
// distilled/packages/cloudflare/patches/turnstile/getWidget.json { "errors": { "WidgetNotFound": [{ "code": 10404 }, { "code": 10407 }], "Forbidden": [{ "status": 403 }] } }这个补丁文件的含义:当 Turnstile 的getWidget接口返回错误码10404或10407时,SDK 应将其识别为类型化的WidgetNotFound错误;返回 HTTP 403 时识别为Forbidden。这样一来,API 的真实行为就以类型的形式被记录下来,惠及 SDK 的每一个未来消费者——错误处理是编译期可见的 Effect 错误,而非运行时猜测。
把这个循环跑在 288 个 live-test 套件上,效果显著:SDK 的补丁集从一个发布周期内的369 个增长到 1,087 个操作,覆盖 Cloudflare 114 个服务中的 94 个。每个补丁都是一条"API 实际如何表现"的类型化记录。
这个循环的完整故事——闭环、补丁以及驱动它们的工厂——记录在 Looping the Generation of IaC and SDKs 一文中,感兴趣的读者可以继续深入。
Feature Flags:Cloudflare.FlagshipApp
Cloudflare 的 Flagship 特性开关(feature flag)服务在本版本以完整资源形态落地,并附带了 Effect 原生的 Worker 绑定。声明应用及其开关:
const app = yield* Cloudflare.FlagshipApp("Flags", {}); yield* Cloudflare.FlagshipFlag("NewCheckout", { appId: app.appId, key: "new-checkout", defaultVariation: "off", variations: { off: false, on: true }, });从 App.ts 源码 可以看到资源实现细节:
AppProps仅含可选的name字段,省略时按${app}-${stage}-${id}生成唯一名称;AppAttributes返回服务端生成的appId——它跨更新保持稳定,作为 flag、Worker 绑定以及所有求值调用的作用域标识;此外还有accountId、createdAt、updatedAt、updatedBy等元数据;- 应用名称可原地修改,但
appId一旦生成永不变更。
FlagshipApp.bind(app)把绑定挂到外围 Worker 上,并返回一个运行时客户端用于求值:
export const App = Cloudflare.FlagshipApp("Flags", {}); export default Cloudflare.Worker( "FlagsWorker", { main: import.meta.filename }, Effect.gen(function* () { const flags = yield* Cloudflare.FlagshipApp.bind(App); return { fetch: Effect.gen(function* () { const enabled = yield* flags.getBooleanValue("new-checkout", false, { userId: "user-42", }); return HttpServerResponse.text(enabled ? "on" : "off"); }), }; }).pipe(Effect.provide(Cloudflare.FlagshipBindingLive)), );关键设计:运行时Flagship绑定的每一个方法都被镜像为 Effect(源码中ReadFlags、ReadFlagsBinding、ReadFlagsHttpClient、ReadFlagsLocal等模块共同支撑了这条链路),因此 Worker 代码里无需Effect.tryPromise包装即可直接yield*调用;异步 Workers 则可以把 app 绑定在env上,直接使用原始绑定。
Provider.list与alchemy unsafe nuke
list():枚举环境的全部实时资源
本版本为 Provider 引入了新的生命周期操作list():它枚举当前账户/区域/Zone 中该类型的所有实时资源,返回的每一项都与read产出的Attributes形状一致,因此可以直接被删除。这为后续的清理能力打下了基础。
alchemy unsafe nuke:一键清空测试环境
基于list()构建的alchemy unsafe nuke命令,枚举 profile 下所有 Provider 能看到的资源并全部删除。当 230 种资源类型都可枚举时,清理一个被"烧毁"的测试账户就变成一行命令:
alchemy unsafe nuke \ --include "Cloudflare.*" \ --filter 'resource.workerName === "alchemy-state-store"' \ --dry-run命令的完整语义在 nuke 文档 中有详尽说明,这里提炼关键点:
--include/--exclude接受provider-ID glob(picomatch 语法,如'Cloudflare.*'、'Cloudflare.Worker'),可重复指定,--exclude在--include之后应用;--filter接受JavaScript 表达式,求值时resource在作用域内(另有合成的Type、LogicalId字段);表达式为 truthy 的资源会被"赦免"(spared),不会被删除;表达式抛异常或非布尔值时按 false 处理;--dry-run只预览不删除;--yes跳过确认提示;- 该命令刻意从
--help中隐藏——它删除的是真实基础设施,危险操作不该被轻易发现。
nuke 的安全边界
nuke 的破坏力远超一般认知,需要特别注意:
- 它不限定于某个 stack、stage 或状态存储:stack 文件仅用于发现注册了哪些 Provider,随后每个 Provider 的
list()会枚举该账户/区域/Zone 中的所有同类型资源——包括 alchemy 从未创建过的资源——并全部删除,不可撤销; - 状态存储全程不被读写:枚举只基于实时云状态,删除后状态条目会残留为 stale,需要用
alchemy state clear清理; - 幸存清单:
nuke.singleton设置(始终存在的配置,其 delete 只是重置默认值)、nuke.skip资源(没有真实删除 API 的资源)、被--include/--exclude过滤掉的 Provider、被--filter赦免的资源、以及删除持续失败的资源(会在运行结束时汇报); - 删除采用多轮扫描:每轮尝试所有剩余资源,失败的进入下一轮,依赖顺序无需构建图即可自然浮现;当某轮无进展时停止并报告
N resource(s) could not be deleted; --concurrency控制 Provider 并行扫描/删除数(默认 16,0表示不限制——但通常更慢,因为会触发 Provider 侧限流与重试退避;同一 Provider 内的资源始终并发删除);--timeout是每个list/delete调用的 per-Provider 超时(默认 120 秒),防止单个慢 Provider 拖垮整次运行。
大多数情况下,你真正想要的是alchemy destroy(见 destroy 文档)——它是 stack 作用域、状态驱动的正规卸载方式;unsafe nuke是烧毁测试环境的最后手段。
GitHub 仓库与 Webhook 事件
GitHub.Repository:稳定 ID 驱动的收敛式仓库管理
Justin Bennett 贡献了GitHub.Repository资源——完整的仓库生命周期管理,其核心亮点是一个收敛式 reconciler(converging reconciler):它按稳定的数字 ID 读取仓库。这意味着即使有人在 stack 之外把仓库重命名了,状态也不会被污染(不会因为 name 变化而误判为删除+重建):
const repo = yield* GitHub.Repository("internal-tools", { owner: "my-org", name: "internal-tools", visibility: "private", deleteBranchOnMerge: true, });GitHub.events:把仓库变成 Worker 的事件源
在Repository之上,GitHub.events把一个仓库变成 Cloudflare Workers 的事件源。在 Worker 的 init 阶段订阅,会顺带配置一个指向 Worker URL 的GitHub.Webhook;运行时,每次投递都用常数时间的 HMAC-SHA256 签名校验验证,然后以完全类型化的 payload交到你的 handler 手中:
const secret = yield* Config.redacted("GITHUB_WEBHOOK_SECRET"); // `event.name` 被收窄为 "push" | "pull_request" yield* GitHub.events({ owner: "my-org", repository: "my-repo", events: ["push", "pull_request"], secret, }).subscribe((event) => Effect.log(`received ${event.name} (${event.id})`), );从 RepositoryEventSource.ts 源码 可以看到事件源的类型系统设计:
GitHubEventName源自@octokit/webhooks的EmitterWebhookEventName,并排除event.action变体(如pull_request.opened)——因为 Webhook 按裸事件名配置;WebhookEventName = GitHubEventName | "*","*"订阅所有事件;events默认["push"];WebhookEvent是完整的可辨识联合(discriminated union),以name为判别键,每个事件都有完全类型化的payload;SelectedEvent类型会把联合收窄到你选中的事件集合,因此switch (event.name)可以穷尽收窄event.payload;secret是Redacted.Redacted<string>(如Config.redacted产生),配置后事件源会用该 secret 配置 Webhook,运行时拒绝X-Hub-Signature-256头不匹配的投递——强烈建议设置,否则任何知道投递 URL 的人都能伪造事件。
事件源的使用也体现在 github/events 文档 中:除了GitHub.events(文档中的consumeRepositoryEvents)这种高层抽象,还有更底层的GitHub.Webhook资源——它管理 Webhook 自身的生命周期(首次部署时创建、后续部署原地更新、销毁时删除),url接受Output<string>从而可以指向同栈内的 Worker 资源。值得注意的是,Cloudflare.Workers.GitHubRepositoryEventSourceLive目前是RepositoryEventSource契约的唯一实现(暂无 AWS Lambda 事件源);在其他宿主上仍可直接用GitHub.Webhook指向任意投递 URL,但路由与签名校验需要自己实现。
S3 状态存储:AWS.state()
AWS 栈在本版本获得了一等公民的远程状态后端,与 Cloudflare 的Cloudflare.state()对齐:
const Stack = Alchemy.Stack( "my-stack", { providers: AWS.providers(), state: AWS.state() }, Effect.gen(function* () { // ... }), );从 AWS/StateStore/State.ts 源码 可以确认状态布局与默认行为:
- 状态对象存放于
s3://{bucket}/{prefix}{stack}/{stage}/{fqn}.json; - 默认桶是自动创建的账户区域级(account-regional)桶,命名遵循
alchemy-state-{accountId}-{region}-an约定;自定义名称也必须符合<prefix>-<accountId>-<region>-an的账户区域命名空间约束; prefix指定桶内键前缀(默认""即桶根,尾部/会自动补上);- 桶默认启用
AES256服务端加密(encryption可覆盖); - 同 stage 前缀下还有一个
__stack_output__.json记账对象,存放 stack 已解析的 output,list结果在 FQN 解码前必须把它过滤掉;批量删除遵循 S3 单次DeleteObjects最多 1000 个键的上限(DELETE_BATCH_SIZE = 1000)。
关键设计:凭证解析与桶创建都被推迟到第一次状态操作时才发生——因此构造 Layer 的阶段完全不会触碰 AWS。这意味着alchemy plan在没有任何 AWS 凭证的情况下也能快速完成 diff,只在真正读写状态时才发起网络请求。
本版本其他改进
bundle: falseonCloudflare.Worker:原样上传预先构建好的main(byte-for-byte),不做 rolldown 打包、不做压缩,支持用 glob 形式的rules选择附加模块。这是 OpenNext 等外部打包产物的标准工作路径。alchemy plan近即时启动:warm 启动从 8.2s 降到 1.8s,手段是懒加载 TUI 和 vite 插件、延迟 distilled schema 的构建。- 稳定输出属性:当首个域名未变化时,
worker.url跨部署保持稳定,下游引用它的资源(如 Webhook 投递 URL)不再出现"幽灵更新";同理worker.durableObjectNamespaces在 DO 类集合未变化时保持稳定。 - Lambda 函数 URL 支持完整配置:
url: { authType: "AWS_IAM", cors, invokeMode: "RESPONSE_STREAM" };IAM 认证的 URL 会省略公开的权限声明。 alchemy dev韧性改进:apply 失败时保持存活,记录完整原因而非静默退出。- StaticSite 转发
dev.env(含 Redacted 密钥)给外部 dev-server 命令;本地 assets binding 在alchemy dev中不再返回 500。 - Drizzle drift 检测按 plan/apply 周期缓存:drizzle-kit 的交互式重命名提示只出现一次,不再挂死
alchemy dev。
延伸阅读
围绕本版本的各项能力,仓库内提供了完整的文档与源码可供继续深入:
- Looping the Generation of IaC and SDKs——资源工厂与 SDK 生成闭环的完整故事
- Concepts › Providers——Provider 抽象与生命周期操作
- Concepts › State Store——状态存储概念
- CLI guide——CLI 全景
- nuke 命令文档——
unsafe nuke的完整参数表与安全边界 - Webhooks & events——GitHub Webhook 资源与 Worker 事件源的使用指南
- Deploy a Lambda Function——Lambda 部署指南
- Cloudflare Flagship App 源码——特性开关资源的属性与绑定实现
- AWS S3 State 源码——S3 状态后端的默认值与状态布局
- GitHub RepositoryEventSource 源码——事件源的类型系统与签名校验
【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考