<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ilya Rubinchik</title>
    <description>The latest articles on DEV Community by Ilya Rubinchik (@im_citius).</description>
    <link>https://dev.to/im_citius</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3200498%2Fb5199cdf-a5d9-44d8-aa94-be728a948292.jpg</url>
      <title>DEV Community: Ilya Rubinchik</title>
      <link>https://dev.to/im_citius</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/im_citius"/>
    <language>en</language>
    <item>
      <title>Your terragrunt (or terraform) plan is 4,000 lines. Only two of them matter.</title>
      <dc:creator>Ilya Rubinchik</dc:creator>
      <pubDate>Mon, 10 Aug 2026 21:18:23 +0000</pubDate>
      <link>https://dev.to/im_citius/your-terragrunt-or-terraform-plan-is-4000-lines-only-two-of-them-matter-4p8</link>
      <guid>https://dev.to/im_citius/your-terragrunt-or-terraform-plan-is-4000-lines-only-two-of-them-matter-4p8</guid>
      <description>&lt;p&gt;You know the ritual.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;terragrunt run &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; plan
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you scroll. Past forty units of &lt;code&gt;Refreshing state…&lt;/code&gt;. Past the ninth&lt;br&gt;
identical &lt;code&gt;count&lt;/code&gt; instance. Past a &lt;code&gt;tags_all.LastModified&lt;/code&gt; that changes on every&lt;br&gt;
single run because your CI stamps a timestamp into it. Somewhere in there are&lt;br&gt;
the two lines you actually needed to see — probably the &lt;code&gt;# forces replacement&lt;/code&gt;&lt;br&gt;
on a database.&lt;/p&gt;

&lt;p&gt;You scroll back up. You lose it. You pipe it to a file and grep for &lt;code&gt;must be&lt;br&gt;
replaced&lt;/code&gt;. You approve anyway, because it's 6pm.&lt;/p&gt;

&lt;p&gt;I got tired of that, so I wrote &lt;a href="https://github.com/imcitius/tgsieve" rel="noopener noreferrer"&gt;&lt;strong&gt;tgsieve&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;It runs the plan for you, reads the &lt;strong&gt;structured&lt;/strong&gt; output instead of the prose,&lt;br&gt;
throws away the noise you declared as noise, collapses everything that repeats,&lt;br&gt;
and prints what's left.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's five units of a real terragrunt plan — the same run terraform prints as&lt;br&gt;
several hundred lines.&lt;/p&gt;

&lt;p&gt;The report nests three deep — &lt;strong&gt;where&lt;/strong&gt;, then &lt;strong&gt;what&lt;/strong&gt;, then &lt;strong&gt;which fields&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UPDATE (5)
  envs/prod/c                            ← the unit, said once
    ~ aws_s3_bucket.this                 ← the resource
        tags_all.entity  "tgb" → "tgc"   ← the attributes that changed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A change that's identical across units replaces the directory with the set it&lt;br&gt;
covers, so the first column always answers the same question: &lt;em&gt;where&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  It doesn't scrape text
&lt;/h2&gt;

&lt;p&gt;This matters, because the obvious implementation is fragile garbage.&lt;/p&gt;

&lt;p&gt;You might reach for &lt;code&gt;terragrunt run --all -- plan -json&lt;/code&gt;. It doesn't work:&lt;br&gt;
terragrunt forwards terraform's own NDJSON straight through, so lines from units&lt;br&gt;
running in parallel interleave with no way to tell them apart.&lt;/p&gt;

&lt;p&gt;So tgsieve asks terragrunt for machine-readable artifacts and reads those:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Flag it passes&lt;/th&gt;
&lt;th&gt;What it gets&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;per-unit plans&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--json-out-dir&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one &lt;code&gt;tfplan.json&lt;/code&gt; per unit — the full &lt;code&gt;terraform show -json&lt;/code&gt; document&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;live progress&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--log-format json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;NDJSON events tagged with &lt;code&gt;working-dir&lt;/code&gt;, so failures surface the moment they happen&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;run report&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--report-file/--report-format json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;per-unit result and duration, including units that failed before producing a plan&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;From each plan it computes a real attribute-level diff — flattening&lt;br&gt;
&lt;code&gt;before&lt;/code&gt;/&lt;code&gt;after&lt;/code&gt;/&lt;code&gt;after_unknown&lt;/code&gt; into dotted paths, honouring &lt;code&gt;replace_paths&lt;/code&gt;,&lt;br&gt;
sensitivity, and unknown subtrees. No regexes over &lt;code&gt;~ resource "aws_..."&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;OpenTofu works too — terragrunt defaults to &lt;code&gt;tofu&lt;/code&gt;, and &lt;code&gt;--tf-path&lt;/code&gt; /&lt;br&gt;
&lt;code&gt;TG_TF_PATH&lt;/code&gt; picks explicitly.&lt;/p&gt;
&lt;h2&gt;
  
  
  Nothing is hidden until you say so
&lt;/h2&gt;

&lt;p&gt;The whole tool is one &lt;code&gt;.tgsieve.yaml&lt;/code&gt;, looked up from the working directory&lt;br&gt;
upwards and merged, nearer files winning. &lt;code&gt;tgsieve init&lt;/code&gt; writes a starter one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;

&lt;span class="na"&gt;extends&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;builtin/aws-tags&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# curated rule sets, opt-in&lt;/span&gt;

&lt;span class="na"&gt;hide&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;unchanged_units&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;   &lt;span class="c1"&gt;# units with nothing left to say become a count&lt;/span&gt;
  &lt;span class="na"&gt;reads&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;             &lt;span class="c1"&gt;# data sources resolved during apply: they create nothing&lt;/span&gt;

&lt;span class="na"&gt;ignore&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tag churn&lt;/span&gt;
    &lt;span class="na"&gt;attrs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tags.LastModified"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tags.git_commit"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tags_all.*"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;waiting on the provider fix&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws_ecs_service&lt;/span&gt;
    &lt;span class="na"&gt;attrs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;capacity_provider_strategy.*"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;expires&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-12-01&lt;/span&gt;     &lt;span class="c1"&gt;# after this date the rule stops hiding, loudly&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev is not interesting&lt;/span&gt;
    &lt;span class="na"&gt;unit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;envs/dev/**"&lt;/span&gt;
    &lt;span class="na"&gt;attrs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;never_hide&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;delete&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;replace&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things I'd want to know before trusting a tool that hides my&lt;br&gt;
infrastructure changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A resource only disappears when &lt;em&gt;every one&lt;/em&gt; of its attributes was hidden.&lt;/strong&gt;
One survivor keeps the whole resource on screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An attribute that forces replacement is never hidden&lt;/strong&gt;, whatever the rules
say. Destroys and replacements can't be silenced by default either.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--explain&lt;/code&gt; shows every hidden attribute and the rule that hid it&lt;/strong&gt;, and the
footer always states how much was hidden. The counts count real resources,
not rendered blocks.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Suppressions that expire
&lt;/h3&gt;

&lt;p&gt;That &lt;code&gt;expires: 2026-12-01&lt;/code&gt; is my favourite line in the config. Past that date&lt;br&gt;
the rule stops hiding anything and the report names it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 rule expired and no longer hides anything: waiting on the provider fix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It fails &lt;strong&gt;open&lt;/strong&gt;, on purpose. A suppression that quietly outlives its reason is&lt;br&gt;
exactly the failure this tool exists to prevent. So the lapse restores the&lt;br&gt;
changes rather than silently continuing to swallow them.&lt;/p&gt;
&lt;h2&gt;
  
  
  The small readability things
&lt;/h2&gt;

&lt;p&gt;These are the ones that made me realise how much terraform's renderer was&lt;br&gt;
costing me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sets are compared by members, not positions.&lt;/strong&gt; Terraform renders sets as&lt;br&gt;
arrays, so a set that comes back in a different order looks like every index&lt;br&gt;
changed at once. tgsieve says what actually happened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="p"&gt;input.cidrs  reordered (4 items, same members)
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="p"&gt;input.cidrs  - "10.0.2.0/24"
input.cidrs  + "10.0.9.0/24"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Objects inside a collection get matched by an identity field (&lt;code&gt;id&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;key&lt;/code&gt;, &lt;code&gt;cidr_block&lt;/code&gt;, a few others) when every member carries one and it's&lt;br&gt;
unique. So an edited security group rule reads as an edit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ingress["web"].to_port  80 → 8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;rather than one object leaving and a nearly identical one arriving.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Long values are trimmed around the difference, not from the start.&lt;/strong&gt; Two&lt;br&gt;
values sharing a 200-character prefix would otherwise print that prefix twice&lt;br&gt;
and hide the part that changed. A string that's itself a JSON document — an IAM&lt;br&gt;
policy, say — is shown as that document rather than as an escaped string.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repeated failures get counted, not repeated.&lt;/strong&gt; A removed provider&lt;br&gt;
configuration produces one diagnostic per orphaned resource, which terraform&lt;br&gt;
prints as forty paragraphs. The count is the news:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FAILED (1)
  ✗ infra/networking  ×38
      Error: Provider configuration not present: To work with module.peering-… (orphan)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same at stack level. One expired credential hits every unit; you get it once,&lt;br&gt;
with the list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FAILED (5)
  ✗ 5 units, same error
      envs/dev/a, envs/dev/b, envs/prod/a, +2 more
      Error: no valid credential sources found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Folding never drops &lt;em&gt;where&lt;/em&gt; the problem is — diagnostics sharing a message but&lt;br&gt;
naming different lines list those lines.&lt;/p&gt;
&lt;h2&gt;
  
  
  Drift is a finding, not work
&lt;/h2&gt;

&lt;p&gt;Nothing in drift changes when you apply. So it's counted rather than listed, and&lt;br&gt;
a plan whose only findings are drift reports what terraform reports — no&lt;br&gt;
changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SUMMARY  no changes
  3 resources drifted outside terraform, none of them addressed by this plan (--drift to list)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--drift&lt;/code&gt; lists them, split by what the plan intends to do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DRIFT — this plan puts it back (2)
DRIFT — this plan leaves it (1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second is the one that bites — an attribute under &lt;code&gt;ignore_changes&lt;/code&gt;, or a&lt;br&gt;
resource the config no longer governs, stays drifted after the apply. Drift&lt;br&gt;
never trips &lt;code&gt;--fail-on&lt;/code&gt; or &lt;code&gt;--detailed-exitcode&lt;/code&gt;, because those describe what an&lt;br&gt;
apply will &lt;em&gt;do&lt;/em&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  &lt;code&gt;tgsieve apply&lt;/code&gt; applies the plan you reviewed
&lt;/h2&gt;

&lt;p&gt;Not a fresh plan made after you answered.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tgsieve apply &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;apply 9 changes across 5 units? [yes/no] yes
4 resources will be destroyed or replaced — type 'destroy' to confirm: destroy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second question only appears when something will be destroyed or replaced,&lt;br&gt;
and it wants &lt;em&gt;that word&lt;/em&gt; rather than another "yes" — those are the changes that&lt;br&gt;
running the tool again won't undo. Outside a terminal it refuses rather than&lt;br&gt;
assuming; &lt;code&gt;--auto-approve&lt;/code&gt; is how you say you meant it in CI.&lt;/p&gt;

&lt;p&gt;Review now, apply later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tgsieve plan  &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--keep-plans&lt;/span&gt; ./plans &lt;span class="nt"&gt;--out-dir&lt;/span&gt; ./plans
tgsieve apply &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--plans&lt;/span&gt; ./plans
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And when the apply finishes — or stops — the report says what terraform&lt;br&gt;
&lt;em&gt;actually did&lt;/em&gt;, which after a failure is the one question the plan can't answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;APPLY FAILED
  stopped after 15m45s — the report above is what was planned, not what landed
  ✗ Error: updating EKS Node Group (…) config: operation error EKS
  terraform changed 4 resources · 1 did not finish · slowest first
    ✗ terraform/live/ctrl/tests/eks module.eks…aws_eks_node_group.this — modifying, did not finish after 15m2s
    ✓ terraform/live/ctrl/tests/eks aws_security_group_rule.node created in 3s
  run tgsieve plan to see where things actually stand
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Living with a heavy stack
&lt;/h2&gt;

&lt;p&gt;If your stack takes twenty minutes, you need more than a pretty report.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You can see what's happening.&lt;/strong&gt; A window that updates in place instead of&lt;br&gt;
scrolling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;  envs/prod/a  aws_db_instance.main     modifying… 1m12s
  envs/prod/b  aws_instance.web[3]      creating… 22s
  envs/dev/a   null_resource.deploy     done 4s
⠴ applying · 7/12 applied · 1m30s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The denominator comes from &lt;code&gt;terragrunt find&lt;/code&gt;, so it's known before the first&lt;br&gt;
unit starts. Outside a terminal it collapses to a heartbeat line every 30&lt;br&gt;
seconds, so CI logs still show liveness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ctrl-C behaves.&lt;/strong&gt; It forwards the interrupt so terraform can release its state&lt;br&gt;
locks, prints the report for whatever finished, lists the rest under &lt;code&gt;NOT RUN&lt;/code&gt;,&lt;br&gt;
and exits 130.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;--resume&lt;/code&gt; picks up where an interrupted run stopped:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tgsieve plan &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--keep-plans&lt;/span&gt; ./plans          &lt;span class="c"&gt;# 40 units, Ctrl-C at 31&lt;/span&gt;
tgsieve plan &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--keep-plans&lt;/span&gt; ./plans &lt;span class="nt"&gt;--resume&lt;/span&gt; &lt;span class="c"&gt;# runs the missing 9&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reusing a plan is only sound if the code hasn't moved under it, so a run records&lt;br&gt;
the commit plus a fingerprint of uncommitted changes, and &lt;code&gt;--resume&lt;/code&gt; refuses to&lt;br&gt;
mix generations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;the plans in ./plans were made at 1cb275fd, the working tree is now at 4a91e0c2
  re-run without --resume to plan the stack fresh, or pass --force to mix generations
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That check also covers &lt;strong&gt;where each unit's code comes from&lt;/strong&gt;. A remote module&lt;br&gt;
pinned to a branch — or to a tag someone can move — reads identically before and&lt;br&gt;
after the code it names changes, so each remote ref gets resolved to a commit&lt;br&gt;
with &lt;code&gt;git ls-remote&lt;/code&gt; (one call per distinct repo+ref, not per unit;&lt;br&gt;
&lt;code&gt;--no-resolve-refs&lt;/code&gt; for air-gapped runs).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;--fast&lt;/code&gt; skips the refresh&lt;/strong&gt;, which on a heavy stack is the single biggest&lt;br&gt;
speed-up available. The summary says so every time, because a plan that never&lt;br&gt;
looked at reality can report "no changes" for a stack that has drifted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;state was not refreshed: anything changed outside terraform is invisible here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus: plan directories are locked while a run writes to them (&lt;code&gt;--lock-wait 2m&lt;/code&gt;&lt;br&gt;
for CI pipelines racing each other), and unit durations are remembered per&lt;br&gt;
directory with a two-week TTL, so &lt;code&gt;--timings&lt;/code&gt; stays meaningful across resumes.&lt;/p&gt;
&lt;h2&gt;
  
  
  In CI
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;--format&lt;/code&gt; decides the shape: &lt;code&gt;tty&lt;/code&gt; (default), &lt;code&gt;md&lt;/code&gt;, &lt;code&gt;json&lt;/code&gt; or &lt;code&gt;github&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Markdown&lt;/strong&gt; for pull request comments — destructive changes stay open,&lt;br&gt;
everything else folds into &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;, output is capped so GitHub doesn't&lt;br&gt;
reject it, and every report starts with &lt;code&gt;&amp;lt;!-- tgsieve --&amp;gt;&lt;/code&gt; so a bot can update&lt;br&gt;
its own comment instead of adding a new one each run.&lt;/p&gt;

&lt;p&gt;With Atlantis, a custom workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;workflows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;tgsieve&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;init&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tgsieve plan --all --format md --fail-on high&lt;/span&gt;
    &lt;span class="na"&gt;apply&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tgsieve apply --all --auto-approve --format md&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--fail-on high&lt;/code&gt; turns the plan step red only when something is destroyed or&lt;br&gt;
replaced — a pipeline can stop for a replacement without stopping for a new log&lt;br&gt;
group.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSON&lt;/strong&gt; is a versioned document with its own types, not the internals of the&lt;br&gt;
sieve:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tgsieve plan &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt; json | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.changes[] | select(.action=="replace") | .address'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sensitive values never appear in it — an attribute terraform marked sensitive is&lt;br&gt;
reported as &lt;code&gt;"sensitive": true&lt;/code&gt; with no &lt;code&gt;before&lt;/code&gt; or &lt;code&gt;after&lt;/code&gt;. A machine-readable&lt;br&gt;
report is the easiest place for a secret to end up somewhere it shouldn't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Actions&lt;/strong&gt; annotations, so failures land on the diff rather than only in&lt;br&gt;
the job log:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;::error file=modules-vpcs.tf,line=72,title=infra/networking::Error: Unsupported attribute…
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the exit codes distinguish the three ways a run can be unhappy:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;code&lt;/th&gt;
&lt;th&gt;meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ran fine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;tgsieve itself failed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;changes survived the sieve (&lt;code&gt;--detailed-exitcode&lt;/code&gt;, or &lt;code&gt;--fail-on&lt;/code&gt; was met)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;one or more units failed to plan&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;130&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;interrupted with Ctrl-C&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  No terragrunt? Still works
&lt;/h2&gt;

&lt;p&gt;A root module big enough to be unreadable has the same problem as a stack, minus&lt;br&gt;
the queue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tgsieve plan  &lt;span class="nt"&gt;--engine&lt;/span&gt; terraform
tgsieve apply &lt;span class="nt"&gt;--engine&lt;/span&gt; terraform
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same rules, same collapsing, same formats. The flags that only mean something&lt;br&gt;
with a queue behind them say so rather than being quietly ignored:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--all needs terragrunt: the terraform engine plans one root module
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;imcitius/tap/tgsieve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/imcitius/tgsieve@latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in a stack you already have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tgsieve plan &lt;span class="nt"&gt;--all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No config needed to start — with an empty &lt;code&gt;.tgsieve.yaml&lt;/code&gt;, nothing is hidden and&lt;br&gt;
you still get the nesting, the collapsing, the folded failures and the honest&lt;br&gt;
summary. Add rules once you've seen which noise is &lt;em&gt;your&lt;/em&gt; noise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tgsieve init            &lt;span class="c"&gt;# starter config at the project root&lt;/span&gt;
tgsieve rules           &lt;span class="c"&gt;# what config is in effect, and from where&lt;/span&gt;
tgsieve plan &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--explain&lt;/span&gt;   &lt;span class="c"&gt;# every hidden attribute and the rule that hid it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's MIT, written in Go, single binary, no daemon, no account.&lt;/p&gt;

&lt;p&gt;👉 &lt;strong&gt;&lt;a href="https://github.com/imcitius/tgsieve" rel="noopener noreferrer"&gt;github.com/imcitius/tgsieve&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I'd genuinely like to know what noise it &lt;em&gt;doesn't&lt;/em&gt; catch on your stack — that's&lt;br&gt;
the feedback that turns into the next preset. Issues, or just reply here.&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>devops</category>
      <category>opensource</category>
      <category>aws</category>
    </item>
  </channel>
</rss>
