Every Terraform user knows the plan that shows an "update in place" which isn't a change at all: an IAM policy whose JSON got reordered, a security-group rule list shuffled, "80" that became 80, a tags = {} that flipped to null. The before and after are semantically identical — AWS stores the value in its own canonical form — but the plan never goes away. The usual "fix" is a reflexive lifecycle { ignore_changes = [...] }, which makes the diff disappear and silently hides any real future change to that attribute.
So I wrote permadiff to stop doing that.
What it does
permadiff reads terraform show -json output and, for every in-place update, tries to prove the diff is cosmetic by canonicalising both sides — sorting policy statements, treating set-semantic lists as sets, coercing scalar types, normalising DNS names, and so on. If (and only if) the canonical before and after are identical, it labels the attribute a no-op, explains in plain English why AWS treats them as equal, and suggests the fix that makes the diff disappear at the source. Real changes are listed plainly and never suppressed.
terraform show -json plan.tfplan | permadiff
One rule: prove it or it's real
A false negative (missed noise) is fine; a false positive (a real change mislabelled as noise) is not — that's how a tool gets someone owned. So every pattern ships with a look-alike "real twin" fixture that must stay classified as real, and the canonicalisers fail closed on anything they don't fully understand.
Take the most common perma-diff there is. You apply an IAM policy; AWS stores the statements back in a different order; the next terraform plan shows the whole
policy = jsonencode(...) as an update-in-place — a wall of diff over a document that means exactly the same thing. permadiff sorts the statements (order is insignificant in IAM), proves the before and after are identical, and reports a no-op.
Now the thin ice: a genuine permission change surfaces in that same ~ policy block. Change one Resource from arn:aws:s3:::app-data/Backups/* to .../exports/* and the plan looks almost exactly like the harmless reorder — but it isn't. Guess that "policy diffs are usually just noise" and you've quietly shipped a real change. permadiff canonicalises both sides and compares them: the reorder collapses to equal, the resource edit doesn't — so it stays a real change.
Details
It's fully offline and deterministic — rule-based against a YAML pattern catalogue compiled into a single static Go binary. No network calls, no telemetry, never edits your files. v1 covers 13 AWS pattern families (IAM/S3/KMS/SQS policy JSON, security-group rule sets, tags, ECS container definitions, AWS Batch job definitions, type coercion, Route 53 names, set-semantic lists, computed-field churn, curated JSON-document attributes). Other providers' resources pass through
untouched as real changes.
go install github.com/itsveems/permadiff/cmd/permadiff@latest
itsveems
/
permadiff
Separate Terraform plan noise from real changes — and fix the noise at its source.
permadiff
Separate Terraform plan noise from real changes — and fix the noise at its source.
Reproduce this output: permadiff examples/demo-plan.json
permadiff reads terraform show -json output and identifies perma-diffs
update-in-place changes that are not real changes — artifacts of provider
normalisation, JSON reordering, type coercion, or formatting, where before and
after are semantically identical. For each one it explains why it's a no-op
and suggests the correct fix, so the diff disappears instead of getting
buried under a reflexive ignore_changes.
permadiff 6 changes: 2 perma-diff no-ops (with fixes) · 4 real changes
PERMA-DIFF NOISE (2) — semantically identical before and after; nothing changes in AWS
──────────────────────────────────────────────────────────────────────────────
~ aws_iam_policy.app_data
• policy — IAM policy JSON normalisation
AWS stores IAM policy documents in its own normalised form: object keys reordered,
whitespace stripped, single-element arrays and scalars interchanged, and statements
reordered. None of that changes what the policy allows…It's deliberately conservative — it'll miss noise rather than risk mislabelling a real change. If you have a perma-diff it doesn't catch, contributing is usually just YAML + two fixtures (the no-op and its real twin), no Go required. I'd love to hear which perma-diffs hurt the most in your stacks.
Top comments (0)