Your terragrunt (or terraform) plan is 4,000 lines. Only two of them matter.
Your terragrunt (or terraform) plan is 4,000 lines. Only two of them matter.
你的 Terragrunt(或 Terraform)计划有 4,000 行,但只有两行是有用的。
You know the ritual. terragrunt run-all plan. Then you scroll. Past forty units of “Refreshing state…”. Past the ninth identical count instance. Past a tags_all.LastModified that changes on every single run because your CI stamps a timestamp into it. Somewhere in there are the two lines you actually needed to see — probably the # forces replacement on a database. You scroll back up. You lose it. You pipe it to a file and grep for “must be replaced”. You approve anyway, because it’s 6pm.
你一定很熟悉这个流程:运行 terragrunt run-all plan,然后开始滚动屏幕。跳过四十个“Refreshing state…”单元,跳过第九个重复的 count 实例,跳过因为 CI 注入时间戳而每次运行都在变的 tags_all.LastModified。在那堆信息中,藏着你真正需要关注的两行——很可能是数据库的 # forces replacement。你向上滚动,找不到了。你把它重定向到文件并用 grep 搜索 “must be replaced”。最后,因为已经下午 6 点了,你还是直接批准了。
I got tired of that, so I wrote tgsieve. What it does: It runs the plan for you, reads the structured output instead of the prose, throws away the noise you declared as noise, collapses everything that repeats, and prints what’s left.
我厌倦了这一切,于是写了 tgsieve。它的作用是:为你运行计划,读取结构化输出而非冗长的文本,过滤掉你定义的噪音,折叠所有重复项,并打印出剩下的关键信息。
DESTROY / REPLACE (1)
envs/prod/a ± aws_db_instance.main
engine_version "14.7" → "15.3" forces replacement
UPDATE (5)
5 units envs/dev/a, envs/dev/b, envs/prod/a, +2 more
~ null_resource.pin
triggers.region "eu-central-1" → "us-west-2"
SUMMARY
±1 replace ~5 update
severity: 1 high, 5 medium
hid 214 attributes across 3 rules (--explain to see them)
That’s five units of a real terragrunt plan — the same run terraform prints as several hundred lines. The report nests three deep — where, then what, then which fields: 这就是一个真实 Terragrunt 计划中五个单元的概览——同样的操作,Terraform 原生输出需要几百行。报告采用三层嵌套结构——在哪里(where)、是什么(what)、哪些字段(which fields):
UPDATE (5)
envs/prod/c ← the unit, said once
~ aws_s3_bucket.this ← the resource
tags_all.entity "tgb" → "tgc" ← the attributes that changed
A change that’s identical across units replaces the directory with the set it covers, so the first column always answers the same question: where. 如果多个单元发生了相同的变更,它会用覆盖的目录集合来代替,因此第一列始终回答同一个问题:在哪里。
It doesn’t scrape text
它不进行文本抓取
This matters, because the obvious implementation is fragile garbage. You might reach for terragrunt run-all plan -json. It doesn’t work: terragrunt forwards terraform’s own NDJSON straight through, so lines from units running in parallel interleave with no way to tell them apart. So tgsieve asks terragrunt for machine-readable artifacts and reads those:
这一点很重要,因为显而易见的实现方式(文本抓取)极其脆弱。你可能会尝试 terragrunt run-all plan -json,但这行不通:Terragrunt 会直接透传 Terraform 原生的 NDJSON,导致并行运行的单元输出交织在一起,无法区分。因此,tgsieve 会向 Terragrunt 请求机器可读的制品并进行读取:
| What | Flag it passes | What it gets |
|---|---|---|
| per-unit plans | --json-out-dir | one tfplan.json per unit — the full terraform show -json document |
| live progress | --log-format json | NDJSON events tagged with working-dir, so failures surface the moment they happen |
| run report | --report-file/--report-format json | per-unit result and duration, including units that failed before producing a plan |
| 功能 | 传递的参数 | 获取的内容 |
|---|---|---|
| 单元计划 | --json-out-dir | 每个单元一个 tfplan.json —— 完整的 terraform show -json 文档 |
| 实时进度 | --log-format json | 带有 working-dir 标签的 NDJSON 事件,确保故障即时显现 |
| 运行报告 | --report-file/--report-format json | 每个单元的结果和耗时,包括在生成计划前就失败的单元 |
From each plan it computes a real attribute-level diff — flattening before/after/after_unknown into dotted paths, honouring replace_paths, sensitivity, and unknown subtrees. No regexes over ~ resource "aws_...". OpenTofu works too — terragrunt defaults to tofu, and --tf-path / TG_TF_PATH picks explicitly.
它从每个计划中计算出真正的属性级差异——将 before/after/after_unknown 扁平化为点分路径,并遵循 replace_paths、敏感度设置和未知子树。不再使用针对 ~ resource "aws_..." 的正则表达式。OpenTofu 也同样支持——Terragrunt 默认使用 tofu,也可以通过 --tf-path 或 TG_TF_PATH 显式指定。
Nothing is hidden until you say so
除非你指定,否则什么都不会被隐藏
The whole tool is one .tgsieve.yaml, looked up from the working directory upwards and merged, nearer files winning. tgsieve init writes a starter one.
整个工具仅依赖一个 .tgsieve.yaml 配置文件,它会从当前工作目录向上查找并合并,越靠近的配置文件优先级越高。tgsieve init 可以生成一个初始配置。
version: 1
extends: [builtin/aws-tags] # curated rule sets, opt-in
hide:
unchanged_units: true # units with nothing left to say become a count
reads: true # data sources resolved during apply: they create nothing
ignore:
- name: tag churn
attrs: ["tags.LastModified", "tags.git_commit", "tags_all.*"]
- name: waiting on the provider fix
type: aws_ecs_service
attrs: ["capacity_provider_strategy.*"]
expires: 2026-12-01 # after this date the rule stops hiding, loudly
- name: dev is not interesting
unit: "envs/dev/**"
attrs: ["*"]
never_hide:
actions: [delete, replace]
Three things I’d want to know before trusting a tool that hides my infrastructure changes: 在信任一个会隐藏基础设施变更的工具之前,我有三点顾虑:
-
A resource only disappears when every one of its attributes was hidden. One survivor keeps the whole resource on screen.
-
An attribute that forces replacement is never hidden, whatever the rules say.
-
Destroys and replacements can’t be silenced by default either.
-
--explainshows every hidden attribute and the rule that hid it, and the footer always states how much was hidden. -
The counts count real resources, not rendered blocks.
-
只有当资源的所有属性都被隐藏时,该资源才会消失。只要有一个属性存活,整个资源就会显示出来。
-
无论规则如何设置,强制替换(forces replacement)的属性永远不会被隐藏。
-
销毁(Destroys)和替换(replacements)默认情况下也无法被静默。
-
--explain会显示每个被隐藏的属性及其对应的隐藏规则,页脚始终会统计被隐藏的数量。 -
计数统计的是真实的资源,而不是渲染后的代码块。
Suppressions that expire
会过期的抑制规则
That expires: 2026-12-01 is my favourite line in the config. Past that date the rule stops hiding anything and the report names it: 1 rule expired and no longer hides anything: waiting on the provider fix. It fails open, on purpose. A suppression that quietly outlives its reason is exactly the failure this tool exists to prevent. So the lapse restores the changes rather than silently continuing to swallow them.
配置中的 expires: 2026-12-01 是我最喜欢的一行。过了这个日期,该规则将停止隐藏任何内容,并在报告中明确指出:1 rule expired and no longer hides anything: waiting on the provider fix。这是故意设计的“失败即开放”机制。抑制规则如果在其失效后依然静默运行,正是本工具旨在防止的故障。因此,过期后它会恢复显示变更,而不是继续静默吞没它们。
The small readability things
提升可读性的小细节
These are the ones that made me realise how much terraform’s renderer was costing me. 正是这些细节让我意识到 Terraform 原生渲染器让我付出了多大的代价。
- Sets are compared by members, not positions. Terraform renders sets as arrays, so a set that comes back in a different order looks like every index changed at once.
tgsievesays what actually happened: 集合按成员比较,而非位置。 Terraform 将集合渲染为数组,因此如果集合返回顺序不同,看起来就像所有索引都变了。tgsieve会告诉你实际发生了什么:input.cidrs reordered (4 items, same members) input.cidrs - "10.0.2.0/24" input.cidrs + "10.0.9.0/24" - Objects inside a collection get matched by an identity field (id, name, key, cidr_block, a few others) when every member carries one and it’s unique. So an edited security group rule reads as an edit:
ingress["web"].to_port 80 → 8080rather than one object leaving and a nearly identical one arriving. 集合内的对象通过标识字段匹配(如 id, name, key, cidr_block 等),前提是每个成员都有且唯一。因此,编辑安全组规则会显示为修改:ingress["web"].to_port 80 → 8080,而不是一个对象被移除、另一个几乎相同的对象被添加。 - Long values are trimmed around the difference, not from the start. Two values sharing a 200-character prefix would otherwise print that prefix twice and hide the part that changed. 长值会围绕差异点进行截断,而不是从开头截断。否则,两个共享 200 字符前缀的值会重复打印该前缀,从而掩盖真正改变的部分。
- A string that’s itself a JSON document — an IAM policy, say — is shown as that document rather than as an escaped string. 本身是 JSON 文档的字符串(例如 IAM 策略)会以文档格式显示,而不是转义字符串。
- Repeated failures get counted, not repeated. A removed provider configuration produces one diagnostic per orphaned resource, which terraform prints as forty paragraphs. The count is the news:
重复的故障会被计数,而不是重复打印。 移除的提供程序配置会为每个孤立资源生成一个诊断信息,Terraform 会将其打印为四十个段落。而计数才是重点:
Same at stack level. One expired credential hits every unit; you get it once, with the list: 堆栈级别也是如此。一个过期的凭证会影响所有单元;你只会收到一次报告,并附带受影响的列表:FAILED (1) ✗ infra/networking ×38 Error: Provider configuration not present: To work with module.peering-… (orphan)
Folding never drops where the problem is — diagnostics sharing a message but naming different lines list those lines. Drift is a finding, not noise. 折叠功能绝不会丢掉问题所在——共享相同消息但指向不同行的诊断信息会列出所有这些行。漂移(Drift)是一个发现,而不是噪音。FAILED (5) ✗ 5 units, same error envs/dev/a, envs/dev/b, envs/prod/a, +2 more Error: no valid credential sources found