If you work with Azure resources and Terraform, sooner or later, you’ll face this.
You deploy a resource (say, a Managed Identity) using Terraform, and after a few days, you want to add something simple — maybe a Role Assignment.
But as soon as you run:
terraform plan
Boom — you see a terrifying message:
-/+ resource "azurerm_user_assigned_identity" "my_identity" {
id = "/subscriptions/xxxxxxx/resourceGroups/xyz/providers/Microsoft.ManagedIdentity/userAssignedIdentities/my_identity"
location = "eastus"
name = "my_identity"
~ principal_id = "11111111-1111-1111-1111-111111111111" => "22222222-2222-2222-2222-222222222222"
~ tenant_id = "aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" => "aaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
}
Plan: 1 to add, 0 to change, 1 to destroy.
You start thinking —
“Wait, what?? Why will it recreate my Managed Identity? I did nothing except plan to add a role assignment!”
🚨 Scenario 1: Managed Identity “False Drift” and Dangerous Recreate
Let’s see an example:
resource "azurerm_user_assigned_identity" "my_identity" {
name = "my-managed-id"
resource_group_name = azurerm_resource_group.my_rg.name
location = azurerm_resource_group.my_rg.location
}
resource "azurerm_role_assignment" "assign_identity" {
principal_id = azurerm_user_assigned_identity.my_identity.principal_id
role_definition_name = "Contributor"
scope = azurerm_resource_group.my_rg.id
}
You deploy this → everything is fine.
Later when you run terraform plan, Terraform fetches principal_id and tenant_id from Azure backend.
If even a tiny representation change occurs (like casing or invisible metadata updates by Azure), Terraform thinks → “Mismatch! Resource drifted!”
Since principal_id is immutable, Terraform’s only option is to recreate the resource.
And that is the dangerous part → this is your production Managed Identity → re-creating it will break access for anything using that identity.
🚨 Scenario 2: VM Data Disk Drift and Full VM Recreate
This can happen with Azure Virtual Machines + Data Disks too.
You provision a VM with a data disk, and later Azure updates metadata like:
-
-
- Disk
lun - Disk
id - VM NIC private IP (when dynamic)
- Disk
-
When Terraform compares the state with the Azure backend, any mismatch in these immutable properties leads Terraform to plan a destroy and recreate action — even if the change is trivial or backend-controlled.
-/+ resource "azurerm_linux_virtual_machine" "example_vm" {
id = "/subscriptions/xxxxxx/resourceGroups/xyz/providers/Microsoft.Compute/virtualMachines/example-vm"
name = "example-vm"
location = "eastus"
resource_group_name = "xyz"
~ storage_data_disk {
lun = "0" => "0"
managed_disk_id = "/subscriptions/xxxxxx/resourceGroups/xyz/providers/Microsoft.Compute/disks/example-vm-disk-old" => "/subscriptions/xxxxxx/resourceGroups/xyz/providers/Microsoft.Compute/disks/example-vm-disk-new"
caching = "ReadWrite"
disk_size_gb = 128
create_option = "Attach"
}
}
Plan: 1 to add, 0 to change, 1 to destroy.
This is even worse → destroying a VM unnecessarily can result in downtime or data loss if not carefully handled!
🤔 Why does Terraform behave like this?
Short answer → by design.
Terraform works in a declarative model → it assumes state == source of truth.
Azure resources often have attributes that are:
-
- Computed after creation (known after apply) → like
principal_id,client_id,tenant_id,resource_id, IPs. - Immutable → cannot be changed → if drift happens → resource must be recreated.
- Computed after creation (known after apply) → like
So, even harmless metadata drift leads Terraform to think → resource needs recreation.
✅ What to do: Always inspect Terraform Plan carefully
Your Terraform Plan output is your best friend.
Whenever a plan shows -/+ (Destroy/Create), immediately stop and check:
-
-
- Did something really change?
If yes → update Terraform code or import changes. - Or is it just backend-computed attributes drifting?
If yes → safe to ignore usingignore_changes.
- Did something really change?
-
💡 Solution → Use ignore_changes Smartly
Terraform has a powerful feature called ignore_changes that saves us here.
resource "azurerm_user_assigned_identity" "my_identity" {
name = "my-managed-id"
resource_group_name = azurerm_resource_group.my_rg.name
location = azurerm_resource_group.my_rg.location
lifecycle {
ignore_changes = [
principal_id,
client_id,
tenant_id
]
}
}
✅ Now, even if Azure updates these backend attributes → Terraform will ignore them during plan → no drift → no re-create.
⚡ Important — Don’t Overuse ignore_changes
While ignore_changes is life-saving, don’t use ignore_changes = all unless you know exactly what you are doing.
lifecycle {
ignore_changes = all
}
➡️ This disables drift detection entirely → even for genuine changes → very dangerous → you may miss critical changes and your resources will be outdated without you knowing.
Instead:
Always specify only those backend-computed attributes which are known to cause false drift.
For example:
-
principal_id,client_id,tenant_id(for Managed Identities)private_ip_address(for dynamic IP NICs)disk.lunordisk.id(for VM disks in specific scenarios)
🛠️ Temporary Workaround: Using Local .terraform Module Copy
Many times, the team working with Terraform may not have modify access to backend modules. Or even if they do, updating a shared module (to add ignore_changes) may involve Pull Requests, reviews, and approvals — which can take time.
But what if you need a solution right now to execute terraform plan and terraform apply without destroying the Azure resource?
Well, there is a trick !
Whenever you run terraform init, all the backend modules (which you typically call through your main.tf or module.tf) are copied into your local working directory inside a folder called .terraform.
If you navigate to .terraform, you will find a path like this (depending on module source):
.terraform/modules/<module-name>/<...>
This is a local copy of the module source code.
✅ If you need a quick fix and cannot wait for backend module updates, you can temporarily edit this local copy of the module.
Add the ignore_changes block right inside the corresponding resource block in the local module copy.
Once done → run terraform plan again → and the issue should be resolved locally → no more destroy plan.
⚡ However, remember — this is only a temporary workaround:
-
- If you delete
.terraformor runterraform init -reconfigure, your changes will be lost. - Other team members (on their own local directories) will not get this fix unless they also update their local copies.
- Therefore, as a permanent solution, you must eventually update the backend module itself.
- If you delete
Still, this trick can be a lifesaver when you need to avoid accidental resource destruction in urgent scenarios!
🎯 Summary (Rule of Thumb)
-
- Always check
terraform plancarefully. - Understand if a resource will be recreated → is that necessary or a false drift.
- If drift is false (backend-computed attributes only) → use
ignore_changes. - Never use
ignore_changes = allunless in rare, special cases. - Use precise
ignore_changesblocks for predictable and clean Terraform workflows.
- Always check
🚀 Conclusion
Terraform’s strict declarative model is great — but in cloud platforms like Azure, computed attributes can cause unwanted drifts and dangerous re-creations.
Thankfully, ignore_changes gives us a safe and clean way to handle this → without ignoring genuine changes.
Next time you see an unnecessary resource destroy → remember this trick → and save your environment from accidental outages!