Reading time: 2 minutes, 46 seconds
You run terraform plan. It wants to change something. You apply. Nothing meaningful happens to the actual infrastructure. You run plan again — and it wants to make the same change again.
Welcome to the permadiff (perpetual diff): a change Terraform proposes on every single run that never actually settles. It is one of the most common sources of noise in a mature Terraform codebase, and left alone it does real damage — not because the change is dangerous, but because it trains your team to ignore plan output. Once people stop reading plans because “it always shows that,” they stop noticing the plan that matters.
Let me show you why it happens, how to tell a harmless permadiff from a dangerous one, and the precise tool to silence it without going blind.
Why a value won’t settle
A permadiff almost always comes from one of two situations:
The platform mutates the value after you create it. You set something with Terraform, and then Azure — or another controller — changes it. Next plan, Terraform sees the live value differs from your configuration and proposes to “correct” it. You apply, Azure changes it back, and round you go.
The value is recomputed every run. A timestamp, a generated suffix, a normalised form of an input. Terraform compares the new computed value to the stored one, they differ, and it shows a diff that means nothing.
The classic Azure example is AKS with the cluster autoscaler. You declare node_count = 3. The autoscaler — doing its job — scales the pool to 7 under load. Now your config says 3 and reality says 7. Every plan wants to drag it back to 3, which would either do nothing useful or actively fight your autoscaler:
resource "azurerm_kubernetes_cluster" "this" { # ... default_node_pool { name = "system" enable_auto_scaling = true min_count = 3 max_count = 10 node_count = 3 # the autoscaler owns this now, not you }}
The fix: tell Terraform who owns the value
lifecycle { ignore_changes = [...] } tells Terraform: “I set this once; after that, whoever changes it is allowed to — stop proposing to revert it.”
resource "azurerm_kubernetes_cluster" "this" { # ... default_node_pool { name = "system" enable_auto_scaling = true min_count = 3 max_count = 10 node_count = 3 } lifecycle { ignore_changes = [ default_node_pool[0].node_count, ] }}
Now the autoscaler owns node_count. Terraform sets the initial value and then leaves it alone, and your plans go quiet.
The Azure resources where this bites most
Once you know the pattern, you start seeing it everywhere. The repeat offenders on Azure:
Tags injected by Azure Policy. Governance policies that auto-apply CreatedBy, CostCentre, or CreatedDate tags will fight your Terraform tag block forever. Ignore the specific managed keys — not the whole tags map, or you lose the ability to manage your own tags:
lifecycle { ignore_changes = [ tags["CreatedBy"], tags["CreatedDate"], ]}
App settings the platform owns. On azurerm_linux_web_app / azurerm_windows_web_app, settings like WEBSITE_*, App Insights connection strings injected by the portal, or values swapped in by deployment slots will reappear every plan.
Generated or rotated values. VM custom_data, anything seeded from a timestamp(), or a secret that a rotation process changes outside Terraform.
In every case the question is the same: does Terraform own this value, or does something else own it after creation? If something else owns it, ignore_changes makes that ownership explicit in code.
The part nobody tells you: benign vs. dangerous diffs
Here is the skill that separates people who use ignore_changes well from people who use it to paper over real problems: read the plan and find the words # forces replacement.
A permadiff on an in-place attribute is cosmetic noise. The same permadiff on an attribute that triggers replacement is a resource that gets destroyed and recreated on every apply — a perpetual outage generator:
~ resource "azurerm_linux_web_app" "this" {
~ some_attribute = "a" -> "b" # forces replacement
}
If you see # forces replacement on a value that keeps drifting, ignore_changes is not optional — it is the difference between a stable service and one that gets rebuilt nightly. But before you reach for it, ask why that immutable attribute is changing at all. Sometimes the right answer is ignore_changes; sometimes it is fixing the upstream process that mutates it.
Two cautions before you sprinkle it everywhere
ignore_changes silences Terraform, it does not stop the drift. The infrastructure still diverges from your config — you have just told Terraform to stop reporting it. That is correct for values another system legitimately owns, and wrong as a way to hide configuration you simply have not reconciled. Use it for the former, never the latter.
ignore_changes = all is a big hammer. It is occasionally right — typically for resources adopted via import that something else fully manages — but it means Terraform will never reconcile any attribute again. Reach for the specific-attribute list first; keep all for the rare case where you genuinely only want Terraform to guarantee existence.
The takeaway
A permadiff is Terraform telling you there is a value it thinks it owns but doesn’t. The fix is not to suppress plans wholesale — it is to make ownership explicit, one attribute at a time, and to always check whether the drifting value forces replacement before you decide how much it matters. Do that, and your plans go back to meaning something — which is the only reason to run them.




