跑过 Azure Terraform 的人大概都经历过这种时刻:你只改了一个 NSG 规则,terraform plan 却打印出几十行变更。每条规则都标着”changed”,但你明明只动了一条。
第一次遇到的时候我以为自己搞错了什么。重新跑了 init,检查了 state 文件,甚至怀疑是不是有人偷偷改了 main branch。排查了半小时,答案最终出现在 HashiCorp 的 GitHub issue 里:这不是你的问题,是 Terraform Set 类型的比较机制在搞鬼。

torumakabe 写的这个 Skill 解决的正是这件事。不到 500 行 Python,零外部依赖,把 plan JSON 丢进去,它就能告诉你哪些变更是真的,哪些只是 Terraform 在诈唬。
老实说,我一开始看到”Agent Skill”这个定位的时候有点疑惑。这玩意不就是个 CLI 脚本吗,怎么还成了 Agent Skill?但仔细看了它的设计之后,我发现这个定位恰恰是它最聪明的地方。
问题到底出在哪
Terraform 的 Set 类型有个很反直觉的设计:它在做 diff 的时候按位置比较,不是按 key 比较。
打个比方。你有一个列表,里面有三个元素 A、B、C。现在你在中间插入一个 D,列表变成 A、D、B、C。按位置比较的话,B 对不上、C 也对不上,看起来”两个元素被改了”。但实际上 B 和 C 的内容没有任何变化,只是位置挪了。
Azure 里大量资源都用 Set 类型存储配置块:
-
Application Gateway 的后端池 -
负载均衡的转发规则 -
NSG 的安全规则 -
Firewall Policy 的规则集合
这些全是 Set。只要你增加或删除一条规则,整个配置块看起来就像被翻了个底朝天。
这不是 AzureRM Provider 的 bug。Set 类型的设计初衷就是不保证顺序,因为顺序在实际 API 调用里没有意义。问题在于 diff 的呈现方式把这种无意义的排序变化当成了”变更”展示出来。
在 CI/CD pipeline 里,这个问题会直接导致两种后果。第一,PR review 时 reviewer 面对几十行虚假变更,要么逐条检查浪费时间,要么直接跳过从而漏掉真正的风险点。第二,自动化的变更检测逻辑会被假阳性触发,产生噪音告警。

它到底做了什么
核心逻辑其实不复杂。脚本读取 terraform plan 的 JSON 输出,遍历所有 azurerm_ 前缀的资源,对每个已知的 Set 属性做重新匹配。匹配的依据不是位置,而是元素内部的关键属性,比如 name、id、host_name。
匹配完之后,变化被分到三个桶里:
一类是纯排序变化。这些元素的 key 在新旧 plan 中都能对上,内容完全一致,只是位置不同。这是完全没有影响的假阳性。
一类是实际的内容变化。元素被新增、删除或者修改了实际值。这些才是真正需要 review 的东西。
还有一类更严重的,就是资源的 delete 加 create 组合。Terraform 判断必须销毁再重建。这种情况可能意味着 downtime,需要特别关注。
三级分类这件事看起来简单,但它把一个原本需要人肉逐行比对的过程,变成了看一眼就能做决策的事情。这个设计的高明之处在于,它不是给你更多信息,而是帮你过滤掉噪音信息。
从 GitHub 上的仓库结构来看,作者在设计时显然考虑了两个使用场景:一个是 Agent Skill 模式,把整个 skill 文件夹丢到 .github/skills 下面,让 Copilot 或 Claude Code 在工作流中自动调用;另一个是纯 CLI 模式,直接 python3 analyze_plan.py plan.json。
# 生成 plan JSON
terraform plan -out=plan.tfplan
terraform show -json plan.tfplan > plan.json
# 分析
python3 analyze_plan.py plan.json --format markdown
支持的输出格式有三种。markdown 模式适合贴到 PR comment 里,人类可读。json 模式适合下游程序消费。summary 模式给出一行摘要,适合 CI/CD 日志。三种格式覆盖了从人工 review 到自动化决策的完整链路。
支持的 Azure 资源覆盖了最常见的重灾区:
-
azurerm_application_gateway:后端池、监听器、路由规则、重写规则集等十几个 Set 属性 -
azurerm_firewall_policy_rule_collection_group:规则集合的嵌套 Set 结构 -
azurerm_frontdoor:后端池、路由配置 -
azurerm_network_security_group:安全规则 -
azurerm_virtual_network_gateway:IP 配置、VPN 客户端配置
属性定义文件 azurerm_set_attributes.json 是开放维护的,缺了什么资源可以自己加。这个设计很务实,不指望作者一个人把所有 Azure 资源都覆盖全。

CI/CD 集成才是真正的使用场景
单独在命令行跑一次的价值有限。真正让它有用的是放进 pipeline 里自动跑。
GitHub Actions 的集成方式很直接:在 PR 触发的工作流里跑 terraform plan,把 plan JSON 喂给脚本,输出 markdown 格式的分析结果,然后用 sticky comment 贴到 PR 下面。这样 reviewer 每次都能看到一份”这个 PR 到底改了什么”的摘要,而且是去噪之后的。
- name: Analyze Set Diff
run: |
python analyze_plan.py plan.json --format markdown > analysis.md
- name: Comment PR
uses: marocchino/sticky-pull-request-comment@v2
with:
path: analysis.md
还有一个更激进的用法,是用 exit code 做门禁。加上 --exit-code 参数后,脚本会根据分析结果返回不同的退出码。退出码 0 表示没有真实变更或只有排序变化,退出码 1 表示有实际的 Set 属性变更,退出码 2 表示有资源替换。在 pipeline 里可以设定:如果返回 2,直接 fail 掉这个 workflow,强制人工介入。
从架构推断,这个 exit code 设计是给安全敏感的环境用的。比如生产环境的 Application Gateway 变更,如果脚本检测到资源替换,与其让 pipeline 自动 apply,不如先拦住让人看一眼。在那种”apply 错了就要写事故报告”的场景下,多一层门禁的价值远超脚本本身的复杂度。
有意思的是,这个脚本完全用 Python 标准库写的,连 json 解析都是内置的。这意味着你不需要在 CI runner 上装任何额外的 pip 包,也不会有依赖版本冲突的问题。作者在文档里专门提了 Python 3.8+,因为更低版本的标准库 API 不兼容。这个最小依赖策略在 CI/CD 场景下非常关键,它消除了”这周的 pipeline 跑不动了因为上周有人更新了某个依赖”这种问题。

局限和适用边界
任何工具都有它不太适合的场景,这个也不例外。
它只支持 AzureRM Provider。如果你用的是 AWS、GCP 或其他云的 Terraform Provider,这个脚本帮不上忙。这是设计取舍,不是技术限制。做跨 Provider 的通用方案需要维护每个 Provider 的 Set 属性清单,维护成本太高,不如专注一个。
属性覆盖也不是 100%。azurerm_set_attributes.json 里列的是已知的 Set 属性,但 AzureRM Provider 更新频繁,新资源和属性会持续加入。如果你用的资源不在清单里,需要自己补一行 JSON。文档里给了完整的添加指南,但前提是你得知道你的资源里有 Set 属性,并且知道 key 是什么。
还有一个容易被忽略的限制:包含 after_unknown 的属性和 sensitive 属性无法完整比较。after_unknown 表示 apply 之后才能确定的值,sensitive 属性在 plan 输出中会被掩码。这两种情况下,脚本只能标记为”无法判断”。
我比较在意的一点是,这个脚本处理的是 plan 阶段的问题,但它不能防止真实变更被误判为假阳性。如果 key 属性本身变了,脚本会正确识别为实际变更。但如果两个元素的 key 相同但其他属性不同,它怎么处理?从代码逻辑来看,它会逐字段比较 key 匹配的元素,所以这种情况也会被正确标记为实际变更。
还有一个值得注意的设计选择:作者没有试图做一个”修复”工具,只做了”诊断”工具。它不会帮你重写 plan 输出,不会自动 apply,不会替你决策。它只是把噪音滤掉,把真正的变更标出来。这个边界设定我很认同。在基础设施变更这种高风险场景下,诊断型和执行型工具的职责必须严格分开。脚本一旦越界去做”自动忽略假阳性变更并 apply”,出问题的概率会成倍增加。
另外,从 Skill 生态的角度看,这个项目暴露了一个有趣的问题。它的核心功能是一个 CLI 脚本,但作者把它包装成了 Agent Skill。这不是多此一举。对于一个 AI coding agent 来说,知道”什么时候该跑这个脚本”和”怎么解读输出”这两件事,比脚本本身更有价值。SKILL.md 里写的触发条件、输出解读指南和 troubleshooting,才是 Agent 真正需要的东西。脚本只是执行载体。这个认知在我看了十几个 Agent Skill 之后才慢慢清晰的。很多 Skill 把重头戏放在工具实现上,忽略了使用时机和结果解释这两层,结果就是 Agent 拿到了一个强大的工具但不会正确使用。
资源地址
| 资源 | 地址 |
|---|---|
| GitHub 仓库 | https://github.com/torumakabe/terraform-azurerm-set-diff-analyzer |
| Smithery | https://smithery.ai/skills/github/terraform-azurerm-set-diff-analyzer |
总结
这个 Skill 的价值不在于技术复杂度。500 行 Python 加一个 JSON 配置文件,技术上没有任何惊艳的地方。它的价值在于把一个广泛存在但没人认真解决的痛点,用一个最简单的方案打透了。
Set 类型 diff 的假阳性问题不是新东西。HashiCorp 的 issue tracker 里至少有几年的讨论,AzureRM Provider 的维护者也清楚这个行为。但大部分团队的应对方式就是一个字:忍。review 的时候手动跳过那些明显是排序变化的行,或者干脆不仔细看 plan 输出了。
这个脚本改变了这个状态。它把一个”忍了”的问题变成了一个”可以自动化解决”的问题。而且它选了最容易被采纳的方式:零依赖、单文件、MIT 协议,你可以直接复制粘贴到自己的仓库里,不需要任何审批流程。
如果你的团队在 Azure 上用 Terraform,特别是管着 Application Gateway 或 NSG 这种东西的,把这个脚本塞到 CI pipeline 里。五分钟的配置,每次 PR 省五分钟的 review 时间,一周下来就回本了。

