跳到内容
观澜涉川 · 札记EN

AI 时代的 Cloud Engineering:纪律与修养

· 约 15 分钟阅读

写在前面

本文由 AI 执笔,人类立意——生成交给黑盒,把关留给人。 Written by AI, imagined by a human.

软件工程里最老、也最少被遵守的一句忠告,Jeff Atwood 说得最直白:最好的代码,就是没有代码。AI 没让它过时,反而让忽略它变得更贵——它让写代码变快了,却没让“改对了”变简单:未经核实的变更照样打挂集群,嘈杂的 PR 照样耗尽注意力,散落的部落知识照样带来混乱。何况,写代码的人正越来越依赖 AI,也越来越少经历那种把纪律磨成本能的重复;代码生成越来越像黑盒,每一步怎么来的,都难以看清。于是那个老问题又冒了出来:这些老纪律还成立吗?答案是肯定的:它们不但成立,反而更值钱了——生成可以交给黑盒,而黑盒外那几道由人把守的关卡,才守得住质量、压得住混乱,甚至让流程更快。

把经验收拢起来,值得守的习惯大多落在三个地方——版本控制(Git)、基础设施管理(IaC,Pulumi)、应用部署(ArgoCD)。都是些“古法”:不新,不酷,甚至有点老古董;可偏偏是它们,在出事的时候最靠得住。工具和语法从不稀缺,AI 还能现写;稀缺的,是判断和节制。目的不是立规矩,而是让 AI 加速之后的工作流,依然安全、确定、可扩展。

下面不按这三个地方来分——同一条原则,往往同时管着 Git、Pulumi 和 ArgoCD,硬按工具切开反而割裂。所以这里分成三组:四条流程原则,讲变更怎么从成形走到落地;一条设计原则,讲代码与配置怎么组织;外加两条底线,不属于任何单个阶段,却是每条原则都得守的。每条都先讲清它想守住什么,再给能照做的做法。老实说,没有一条是新的。这些原则,说到底都在回答同一个问题:生成变便宜之后,判断与节制该落向哪里——答案或许是,落到一条轨迹上,而不只是一个终态。这也是展开它们之前,需要先摆清楚的前提。

终态是图,轨迹是路

先说一个前提:真实世界里的环境,是有时间维度的——不管它由代码搭建,还是靠手工一步步搭起来。让 AI 一口气把 dev、test、staging、prod 四个环境的配置都生成齐,命名一致、结构齐整,一眼就是“最终形态”;可现实里它从来不是这样长出来的——先在 dev 和 test 里搭起来、验证,再提升到 staging 观察一段时间,确认无事,才轮到 prod。四个环境最终都有这套配置,但它是一级一级攒出来的,不是一次铺满的。

AI 很可能看到的、给出的,都是终态;而工程真正要交付的,却是一条轨迹。前者横着铺开:一句 prompt,就能把终态一次画全,还天然倾向于把能写的都写上;后者竖着走:变更沿一条风险递增的阶梯向上挪,dev → test → staging → prod,每一级都是一次曝光、一轮观察、一次“还能重来”的尝试——所谓“提升”,本质是信心在逐级累积。

这一点在复刻现成的环境时尤其隐蔽:仓库现成、配置现成,一键部署,看上去一步到位。可那套“终态”是从一条轨迹里长出来的,轨迹却几乎无法复制——于是你拿到了结果,却没拿到结果背后的证据。

跳过这条轨迹,等于拿着终态的图去盖生产环境那张“已验收”的章——章是真的,却没人体检过。AI 很可能把终态画得出来,却画不出通往终态的那条路;而判断要不要往上走的依据,全在这条路上。 接下来的四条流程原则,做的正是同一件事:把 AI 折叠掉的那条时间维度重新展开,给“快”重新铺一条能逐级验证、也能逐级回头的轨道。

流程四原则

先看变更本身——从成形到落地,该怎么走。

原则一:小步且可验证

把变更拆小、每步都验证,听起来像常识,作用却很实际:真出问题时,你能一眼看出是哪一小步,也能单独回退,而不必在一坨几百行的改动里大海捞针。

主干开发

先澄清一点:主干开发(Trunk-Based Development)并不等于直接往 main 推代码——对 main 的直接提交是受限制的。

它的意思是:main(旧仓库里可能是 master)是唯一的事实来源,大家用短、快的节奏迭代。

  • 短命特性分支:从 main 切一个分支,完成一个具体的小任务,提 PR,然后尽快合回 main(理想情况下一两天内)。
  • 避开长命分支:特性分支最好别开上几周。它和 main 分离越久,集成越难;如果确实要长期存在,就经常把 main 合并回自己的分支。
  • 从最新的 main 切分支:动手前先 git checkout main && git pull。从陈旧的代码或已存在的特性分支切出去,会把无关的 diff 带进新 PR。
  • 一个任务 = 一个分支 = 一个 PR = 删除:PR 一合并,这个分支的使命就结束了,别再复用。复用会带来“脏状态”(残留的本地提交、旧的 PR 评论历史、跟踪错配)——删掉、从 main 重开,每次都从一张干净的白纸开始。
  • CODEOWNERS 要求:合入 main 至少需要一位指定 CODEOWNERS 成员批准,且不能是自己。

小而原子的 PR

拆一个大特性,本质上也是一种表达——只不过介质是代码,而不是自然语言。既然是表达,就该让读的人一眼看懂:一个 PR 只讲一件事,讲完就结束。

  • 拆成小的、单一职责的 PR:每个 PR 最好都能独立验证,且合进去不会破坏现网、也不会让系统卡在不一致状态。小而安全的改动,审阅者能更快、更有信心地批准——这是加快评审与合并最有效的办法。
  • 复杂特性早点开 Draft PR:如果特性复杂或有架构影响,在开发早期就开一个 Draft PR,让团队先对高层方案做一次“预评审”,免得临近合并时才发现要推倒重来。
  • 让改动可追踪、可复现:先过流水线再合,能消掉藏在个人脑中的隐性知识,让任何人都能顺着记录追踪、理解,并在各环境复现。

原则二:为审阅者着想

快速获批的关键在发起方的准备。评审需要专注,每一个 PR 请求都是一次上下文切换。发起方把 PR 自审干净、背景写清楚,审阅者就能把精力花在架构和设计上,而不是基础修复或猜意图。

  • 代码评审是反馈,不是评判:它的目的是分享知识、拉齐质量标准、一起发现盲点,而不是针对作者本人。评论尽量具体、建设性,收到反馈也当作一次协作打磨。
  • 先自审,再请人审:AI 工具和代码片段能提速,但它们只是起点。发出去之前快速过一遍——有没有残留的占位符、逻辑对不对、上下文是否自洽——省的是所有人的时间。
  • 让审阅者省点心:作为作者,在 PR 描述里交代改了什么、为什么改、怎么验证的。准备到位,审阅者两三分钟就能看完并批准。
  • 紧急改动走快速通道:对紧急、简单、或只有合并后才能完整验证的改动,在描述里写明背景,审阅者才知道该审到什么深度,周转自然更快。
  • PR 模板也要守(AI 助手同理):如果用 AI 工具(如 Claude Code)起草 PR 描述或提 PR,让它遵守仓库的 .github/pull_request_template.md——包括结构、章节标题和 Markdown 格式。AI 生成的描述应当补充团队的标准格式,而不是绕过它,除非有充分理由。

原则三:用环境说话

一句话:别只在自己机器上“感觉没问题”,就让流水线和真实环境来作证。

验证:别只说“本地能跑”

Kent Beck 有句名言:make it work, make it right, make it fast(出处)。这里只取它的前半程:先保证结果正确,再谈代码是否优雅。PR 描述里的“验证”小节,做的就是把验证的过程和结果摆出来,而不只是一句“我本地跑过了”。

  • 给出结果:附上可核实的凭据——流水线运行链接、终端输出,或已创建云资源的截图。
  • 交代过程:说明是在哪个中立环境里验证的(CI 流水线或沙箱),而不是只靠本地,免得上线时踩到意外。
  • 先对齐,再动手:先对齐交付物与验收标准,再动手写代码;一个特性只有真实环境端到端验证通过、并交付了预期价值,才算真正完成。

在 ArgoCD 中做隔离验证

引入新 add-on、修改现有服务,或验证结构性 manifest 变更时,如果直接把代码合进 main,ArgoCD 会立刻把未经核实的变更应用到目标集群(前提是 Application 启用了自动同步 syncPolicy.automated;否则合并后只会显示 OutOfSync,需要人工 sync)。

其实没必要为了“试试能不能跑”就抢着合 main。未验证就合并,换来的往往是试错式的提交噪声、跨多个碎片化 PR 的范围蔓延,以及大段只存在于个人脑子里的隐性知识。

对非平凡的变更,更划算的做法是让每个 PR 或基础设施改动自包含、可验证,在真实环境端到端验证后再合入 main;至于简单的取值更新,走轻量评审、快速合并就够了。

隔离验证的工作流如下:

阶段targetRevision操作效果
① 铺路(PR 1)→ feature/my-addon-test合入 mainArgoCD 改为从特性分支同步到集群
② 迭代验证feature/my-addon-test在特性分支上 push、测试在集群里安全地验证真实行为
③ 恢复(PR 2)→ main合入 main回到干净的生产同步

分步说明:

  1. 铺好路由(PR 1):对 main 提一个小 PR,把 ArgoCD Application 定义里的 targetRevision 从 main(或 HEAD)改指向你的特性分支(如 feature/my-addon-test),然后合并。
  2. 安全地迭代与验证:直接在特性分支上开发、推送、测试你的 manifest。ArgoCD 会从该分支同步,让你能在集群里完整观察真实行为。
  3. 恢复并干净合并(PR 2):测试通过、准备晋升时,把 targetRevision 改回 main(或 tag / release),再把特性分支的代码合入 main。

注意两点:修改 targetRevision 本身也要等一次 sync 才会生效;如果这个 Application 由 ApplicationSet 或 app-of-apps 管理,该字段可能被上层模板覆盖。

基础设施变更只走流水线

本地用 pulumi preview 看改动没问题。但除非是极端紧急,pulumi up 都该交给流水线,而不是在自己机器上跑。

  • 身份与权限漂移:本地用户和流水线服务主体通常挂着不同的 IAM 角色与权限范围。本地部署容易带来意外失败、权限错配,或云资源归属不当。
  • 状态归属锁(特定项目):在后端存储账户 ACL 较严的仓库里,本地开发者账户反而权限更高。本地 pulumi up 可能顺手把 state 对象的归属改写成你的个人身份,把自动化的 runner 锁在门外,之后再也更新不了。
  • 审计与可观测:本地的 pulumi preview 和 pulumi up 不留集中、持久的日志。走 CI/CD,每一次部署的日志、执行上下文和 stdout/stderr 才会被永久归档,日后排障、合规、审计都用得上。

竞态:重新 preview。 像 Terraform 这类工具,部署是严格的两阶段(CI 生成 plan 文件,CD 再 apply),以保证 CI 里审过的就是 CD 里应用的。Pulumi 也有对应机制——Update Plans(pulumi preview --save-plan 生成、pulumi up --plan 应用),但它目前仍是实验特性(需 PULUMI_EXPERIMENTAL),未必都已启用。在没有锁定 plan 的情况下,pulumi up 会在部署时重新评估实时代码,所以尤其要留心最终到底会应用什么。

  • 覆盖风险:两位工程师几乎同时从不同特性分支对同一个 stack 执行 pulumi up,或者你的 PR 还没合、另一次部署先完成了,都可能不小心覆盖别人的改动。
  • 中间有任何动静就重跑 preview:如果在初次 pulumi preview 与最终 pulumi up 之间发生过其他流水线运行或部署,值得重新 pulumi preview,重新核对 diff。
  • 尽快合并以免阻塞:为避免竞态、状态冲突或阻塞队友,基础设施 PR 一经批准并验证就尽快合并。

原则四:敬畏生产

生产是唯一一个“改错要道歉、甚至要熬夜”的地方。凡是可能落到它身上的改动,都值得多看一眼。

PR 里附上 preview 运行

IaC 的代码逻辑,并不总能让人看清它对现网云资源的实际影响。一处两行的改动、一个属性的微调,都可能悄悄触发资源的销毁重建(+- replace)。

  • 桥接代码与云的现实:开 PR 时,让 CI 工作流跑一次 pulumi preview,把输出粘贴或链接到 PR 描述里。
  • 把意图摆到台面上:这样审阅者一眼就能看到云侧影响(+ Create、~ Update、+- Replace、- Delete),不必在脑子里推演 Pulumi 会怎么评估代码。

还要留意两类意外的 diff:

  • 意外的更新(~):preview 里冒出了你并不打算动的属性,停下来查一查。这通常意味着底层状态漂移,或云厂商有未处理的默认属性。
  • 销毁性替换(+-):数据库、存储桶、静态 IP、有状态服务一旦被替换,就是数据丢失或停机。preview 报出替换时,先确认这个属性是否真的需要重建,或者是否需要显式加 protect: true、ignoreChanges,再往下走。

GitOps 与自动纠偏

对 Kubernetes 应用与 add-on,用 ArgoCD 做严格的 GitOps:Git 仓库是集群状态的唯一事实来源。

  • 别手动改集群:任何在集群里直接做的改动(kubectl edit、kubectl apply,或 UI 覆盖)都会被识别为漂移,并被 ArgoCD 覆盖 / 自动纠偏回 Git 定义的状态。
  • 改环境,先改 Git:想让环境变,就把改动提交到 Git。

自动纠偏有个前提:Application 需要配置 syncPolicy.automated.selfHeal: true。而且漂移检测依赖 refresh,并非实时触发。

及时、增量发布

审批可能要花时间,但合并后就要尽快部署到目标环境。与其把多个 PR 攒成一次大发布,不如一小步一小步地走——小部署爆炸半径小,bug 好定位,好的代码也不会被连累回滚。

设计原则

再看一层:代码与配置该怎么组织。

原则五:用重复换取解耦

DRY 是一种本能,但在跨环境、跨项目的 IaC 里,它常被用过头。判据不该是“两段东西长得像不像”,而是“它们是不是同一份知识、会不会朝同一个方向变”:会一起变,才值得收敛成一份;会各自演化,重复反而更解耦。

真正必须只存一份的,是知识——命名、结构这类所有环境都要一字不差的东西;“哪个环境要建哪些资源”这种会随环境分叉的意图,宁可各写各的。把“看起来一样”的东西过早抽成共享代码,换来的往往是改一处、牵动全环境:独立演化与爆炸半径隔离同时失效。

资源取舍分环境,资源形状一处定义

非生产与生产有差异是常态,处理原则就一条:要建哪些资源,归各环境自己的取舍;资源长什么样,在共享代码里只定义一次——让一处改动尽可能只落在单个环境里。

  • 取舍分环境:要建哪些资源、用什么规格,写在各环境自己的配置里(如 Pulumi.dev.yaml、Pulumi.prod.yaml)——这些本就会随环境演进、各不相同。
  • 形状一处定义:命名、结构、父子关系、通用标签这类必须处处一致的知识,放进共享代码,只写一次。
  • 别拿逻辑分支代替取舍:把 if env == "prod" 塞进共享代码,等于把“该独立演化”的东西焊死在一起——动一处,全局跟着变。

这条取舍的完整论证,见《Duplication over DRY:用重复换取解耦》。

两条底线

最后两条不属于任何单个阶段,而是每条原则都得守住的底线。

原则六:责任在人,不在工具

工具能提速,也会出错,但从不会替你负责——最后一道关,永远是人。

  • 作者负责:审阅者是第二双眼睛,但代码的安全与质量,最终还由作者兜底。
  • 工具提速,不背锅:AI 起草的代码、CI 跑绿的流水线,都只是辅助;它们没覆盖到的边界,仍然要由你判断——绿灯不等于没问题。

原则七:保护密钥

密钥一旦暴露(API key、云凭据、密码、私钥),代价往往比想象中大——不管它落在 main、临时特性分支,还是 AI 的上下文里。这道底线有两半:别提交进 Git,也别让 AI 读到。

  • 安全工具在盯着:Aikido、GitHub Secret Scanning、Trufflehog 这类平台会自动扫描每次 push。密钥一进任何远程分支,立刻被记录并标记。
  • 删分支删不掉密钥:删掉特性分支或 force-push 一个干净提交,都无法把密钥从 GitHub 抹掉。GitHub 会在后端历史里保留陈旧提交一段时间,泄露的值往往还会长期留在 PR 提交历史、时间线日志和审计记录里。
  • 真泄露了怎么办:如果密钥被误提交并推送——哪怕只在短命分支上待了几秒——第一时间通知团队。从 Git 里删掉代码不够,泄露的凭据要立即吊销并轮换。
  • 提前防御(Git Hooks):可以在本地配上 pre-commit hook(如 git-secrets、detect-secrets),提交前在机器上先扫一遍,把泄露拦在到达 GitHub 之前。
  • 别让 AI 读到密钥(尤其重要):AI 编码助手(如 Cursor、Claude Code)会把工作区文件、打开的上下文和终端输出送进模型,所以“没提交”并不等于“没泄露”——.env、本地凭据文件,甚至 shell 里 export 的 token,只要落进上下文,就可能被处理或留存。
    • 把密钥挡在上下文之外:用工具自己的忽略配置(如 Cursor 的 .cursorignore)把 .env、凭据目录排除出 AI 的可读范围。但它只是尽力而为——官方也承认 LLM 行为难以完全预测,别把它当成唯一防线。
    • 别留明文,交给 secret manager:Vault、云 KMS、1Password 之类按需注入,工作区里不落明文。
    • 用短时、最小权限凭据:能用 OIDC 换临时 token,就别用长期 key——窗口越短,真出事时代价越小。
    • 别把生产密钥贴进 AI 对话:为了“帮忙排障”把密钥粘进聊天框,和提交进 Git 一样危险。

结语

回头看,这些“古法”既不高明,也不时髦,更拦不住 AI 写得比你快。它们守的,其实是一件很朴素的事——让“快”别变成“乱”:黑盒越黑,外面那几道由人把守的关卡就越要紧,而这些关卡得沿着环境一级级抬高,信心才有时间慢慢攒起来。AI 很可能给出的是一张终态的图,工程要补上的,是一条能走、能验、也能回头的路。

参考