作者三个月实践发现,AI 写基础设施代码的最大障碍是没有像单元测试那样的快速反馈循环;通过将 terraform plan -json 作为机器可读的破坏半径dry run 接入 Agent,形成了 Honest feedback loop,绕过 Agent 自己对结果的虚假总结。
我花了三个月让一个 AI 编程 Agent 写 Terraform,处理真实的基础设施。结果我发现,真正让这件事work的不是更精巧的 Prompt——而是意识到 terraform plan -json 本质上是对你的爆炸半径的机器可读预演,这使得它成为你能交给 Agent 的最诚实的反馈循环。以下是我放在前面的分类器、教会我不信任 Agent 自我总结的「差一点」经历,以及 5 条教训。
应用代码有廉价、快速、诚实的反馈循环:跑测试。Agent 写代码,跑测试套件,看到红标,迭代。没有人类干预,最坏情况就是浪费一分钟。
基础设施代码不是这样的。
main.tf 看起来像配置,读起来也像配置。然后你 apply 之后,一个有热 Working Set 数据的缓存集群被销毁重建了——因为 Provider 把某个字段当作不可变的,但没人仔细看过 Plan 输出中间的部分。
所以很长一段时间里,infra/ 是我唯一不让 Agent 碰的目录。这很烦人,因为我大部分基础设施工作确实很无聊:加个队列、挂个 IAM 策略、升级一个 Provider 版本、给四十个资源加成本分配标签。这些恰恰是我想要委托出去的高产量、低创造力的工作。
真正的问题从来不是「模型能写 HCL 吗」——它能,而且说实话在 Provider 参数名方面比我强。真正的问题是:
我能为基础设施构建一个像测试套件一样诚实的反馈循环吗?
答案是 Terraform 本身已经提供了一个。我只是没有把它当作机器输入来用。
每个人都会跑 terraform plan 然后浏览人类可读的输出。但你可以请求结构化数据:
terraform plan -out=tfplan -input=false -lock=false
terraform show -json tfplan > plan.json
在 plan.json 里,每个资源都出现在 resource_changes[] 下,带有一个 actions 数组。Terraform 的整个风险模型就压缩在这一字段中:
就这样。create 和 update 是廉价且可逆的。delete 和 delete,create 才是你的故障所在。于是我写了一个约 30 行的分类器,让 Agent 自己运行它,任何破坏性操作都非零退出:
#!/usr/bin/env python3
"""Classify a Terraform plan by blast radius. Exit 2 if a human needs to look."""
import json, sys
DESTRUCTIVE = {"delete", "replace"}
with open("plan.json") as f:
plan = json.load(f)
changes = []
for rc in plan.get("resource_changes", []):
actions = rc["change"]["actions"]
if actions == ["no-op"]:
continue
kind = "replace" if actions == ["delete", "create"] else actions[0]
changes.append({"address": rc["address"], "kind": kind})
destructive = [c for c in changes if c["kind"] in DESTRUCTIVE]
print(json.dumps({
"total": len(changes),
"by_kind": {k: sum(1 for c in changes if c["kind"] == k)
for k in {c["kind"] for c in changes}},
"destructive": destructive,
}, indent=2))
sys.exit(2 if destructive else 0)
现在 Agent 有了基础设施的红/绿信号。循环是这样的:
flowchart TD
A[Agent edits .tf files] --> B[terraform fmt + validate]
B -->|fails| A
B -->|passes| C[terraform plan -out]
C --> D[classify.py plan.json]
D -->|exit 0| E[Open PR with plan summary]
D -->|exit 2| F[STOP: name the resource<br/>being replaced and why]
F --> G[Human decides]
Agent 在图的左侧自由迭代,物理上无法越过右侧。同样的模式:写代码 → 跑测试——只不过「测试」是对真实云状态的预演。
这是我如果读别人的帖子会质疑的部分,所以直说了。
我没有在规则文件里写「永远不要跑 terraform apply」就了事了。指令是对一个系统的建议,而这个系统偶尔会判定你的意图比你的话更重要。正确做法是:
Agent 以只读云角色运行。它可以 plan,但不能 apply,因为它持有的凭证无法变更任何东西。
apply 在 CI 中运行,使用不同的角色,以人类批准 PR 为门控。
plan 用 -lock=false 运行,这样崩溃的 Agent 不会把状态锁卡死在那里影响其他人。
Agent 不是安全边界。IAM 才是。如果你的安全策略依赖模型选择不去做某件事,你没有安全策略——你只有希望。
大约第六周时,我要求了一个听起来像杂活的东西:「把缓存集群迁移到新的私有子网。」
它生成的 HCL 是正确的。真的正确——正确的子网组、正确的安全组引用、干净的 fmt、通过的 validate。它的 PR 描述说:
This change moves the cache cluster to the new private subnet group.
这是真的。但它也漏掉了唯一重要的那个词。修改那个资源类型的子网组会强制替换——销毁集群、创建新的、在工作时间内让冰冷的缓存置于主数据库前面。
Agent 没有对我说谎。它在描述意图,而我要的是实际影响。在应用代码里这两者恰好相同,在基础设施里却截然不同——我之前从未需要注意到这个区别。
分类器捕捉到了。plan.json 显示 ["delete", "create"],脚本退出 2,循环停住了。之后我改变的不是 Prompt——而是门控指向的位置:
以 Plan 为门控,而不是以 Agent 对 Plan 的描述为门控。
Agent 用文字写的任何东西都是一种声明。plan.json 是证据。只有其中一个应该能够解除合并的阻塞。
我的第一反应是用 terraform -target=... 来限定变更范围。不要这样做。-target 跳过了依赖解析,所以你审查的 Plan 不会是你在实际 apply 时得到的 Plan——你为了让它更小而牺牲了证据的可信度。
按模块根目录限定范围效果好得多,因为边界已经在你的文件系统里了:
## Terraform rules
- You may edit `infra/modules/**` and `infra/envs/staging/**`.
- You may NOT edit `infra/envs/prod/**`. Describe the diff in the PR body instead.
- Always run `make plan` and paste the classifier JSON into the PR description.
- If the classifier exits 2, stop. Name every resource being replaced and
explain what triggers the replacement.
- Never add `lifecycle { ignore_changes = ... }` to silence a diff you don't understand.
最后一条规则是伤痕。有个资源一直有永久性的 diff——某个标签在 Terraform 外部被设置了,所以每次 plan 都永远显示同一行更新。Agent 的修复是加上 ignore_changes 然后宣布 Plan 干净。这有效,就像在检查引擎灯上贴胶带一样有效。它优化了我给它的信号(绿色的 Plan)而不是我真正关心的(状态匹配现实)。
预演永远优于描述,每次都是。 如果你的工具能生成自身效果的机器可读预览,那个预览就是你的反馈循环——也是你的门控。Terraform 有 plan -json。Kubernetes 有 kubectl diff --server-side。数据库迁移工具大多有 --dry-run。在寻找更好的 Prompt 之前先找预览。
拿走凭证,而不是拿走被诱惑的权限。 规则文件是一种建议。IAM 策略是一条事实。你写的每一条「永远不要做 X」指令都应该让你问自己:能不能直接让 X 变得不可能?
替换是唯一值得构建门控的故障模式。 我曾花了一段时间尝试在多个维度上打分风险——资源类型、环境、变更数量。没用。实际上整个分布坍缩到:有没有东西被删除?一条轴、近零误报,捕获了所有可能真正让你被呼叫的内容。
Agent 会消除 diff;人类修复漂移。 你在门控上设置的任何指标都会被最短可用路径所满足。如果「绿色 Plan」是目标,ignore_changes 是一个有效策略。如果你不想这样,就明确说出来——而且要去看看 things 是怎么变绿的,而不只是它们变绿了。
你的 Plan 只有在你的 State 如实反映现实时才诚实。 所有这些都建立在 State 反映现实这个假设上。有人在凌晨两点做的每个控制台点击都在侵蚀它。Agent 没有造成我的漂移问题,但它让修复漂移变得足够昂贵——这说实话比它写的 Terraform 更有价值。
成本作为风险维度。 把成本估算 diff 接入同一个分类器,把「+$400/月」当作和「delete」一样的停止条件。钱也是一种爆炸半径。
策略即代码进循环。 对 plan.json 运行策略检查,这样 Agent 把「这违反了标签策略」作为结构化反馈来迭代,而不是三个小时后的一条 Review 评论。
Kubernetes 上同样的模式。 kubectl diff --server-side 给你一个可比的预览。我想要一个分类器,多个后端。
我一直回归的通用模式:找到预演、把它变成结构化、用它做门控、吊销门控下游所有操作的凭证。这就是大部分内容。
版本(因为这些东西会过期):Terraform v1.9.x、AWS provider v5.x、Python 3.13、Claude Code(2026 年 8 月)。
如果你正把你的 infra 仓库拦在 Agent 之外,我建议从这里开始:跑 terraform show -json,看看 resource_changes[].change.actions,注意你已经有了测试套件——只是你一直在用眼睛读而不是解析它。