IBM Support

Preventing Massive Re‑Creates Caused by for_each Key Drift in AzureRM VM Modules

Troubleshooting


Problem

A routine configuration change, such as updating tags, triggers a Terraform plan that proposes to destroy and recreate a large set of resources, including virtual machines, network interfaces, public IPs, and disks. The plan indicates resource address changes, even though no resources were intentionally renamed.

Example plan output:

- module.vm.azurerm_network_interface.nic["app-01"] will be destroyed
+ module.vm.azurerm_network_interface.nic["app01"] will be created

This behavior can cause significant outages, IP address changes, and failures in virtual machine extensions, despite the minor nature of the initial configuration change.

Cause

This issue is caused by for_each key drift. The unique keys used in the for_each meta-argument changed between Terraform runs. Terraform treats for_each keys as the unique identity for each resource instance. If a key changes, Terraform interprets this as a request to destroy the instance associated with the old key and create a new one with the new key. This change cascades to all dependent resources.

Common causes of key drift include:

  • Case normalization: Applying a function like lower() to keys that were not previously normalized.
  • Separator removal: Using a function like replace("-", "") on keys.
  • Unstable key sources: Using display names or other mutable values instead of stable, unique identifiers for keys.
  • Computed values: Deriving keys from values that are computed during the Terraform run.
  • Data transformation: Input from YAML or JSON sources may be implicitly normalized or trimmed.

Solutions

Solution 1: Detect Key Drift

First, confirm that key drift is the cause. Generate a plan file and inspect it to identify the exact changes in the resource instance keys.

  1. Create a plan file.

    $ terraform plan -out=tfplan
  2. Convert the plan to JSON for easier inspection.

    $ terraform show -json tfplan > tfplan.json
  3. List the current state to compare addresses. Look for pairs with subtle differences, such as ["app-01"] versus ["app01"].

    $ terraform state list | grep module.vm.azurerm_network_interface.nic

Solution 2: Stabilize for_each Keys in Configuration

To prevent future drift, modify your configuration to use immutable identifiers for for_each keys. Never base keys on mutable values like display names.

Before (Unstable Keys):

This example derives keys from display names, which can change.

# Keys derived from display names
locals {
  vms = {
    for vm in var.vms :
    # KEY = lower(replace(vm.name, "-", "")) # <- this drifts
    lower(replace(vm.name, "-", "")) => vm
  }
}

resource "azurerm_network_interface" "nic" {
  for_each = local.vms
  name     = "${each.value.name}-nic"
  # ...
}

After (Stable Keys):

This example uses a dedicated, immutable id attribute for the key, while the mutable name can change safely.

# Provide or derive a stable key once and keep it forever
variable "vms" {
  type = list(object({
    id   = string ## immutable key (e.g., "app01")
    name = string ## display name (e.g., "app-01")
    ## other attributes...
  }))
}

locals {
  vms_by_id = { for vm in var.vms : vm.id => vm } ## stable keys
}

resource "azurerm_network_interface" "nic" {
  for_each = local.vms_by_id
  name     = "${each.value.name}-nic" ## can change safely
  # ...
}

If you cannot change the input variable structure, you can create a stable surrogate key, for example by using the hash of a canonical field, and ensure that logic never changes.

Solution 3: Remap State to New Keys Without Recreation

If the keys have already drifted in your configuration, you can instruct Terraform to update the state addresses instead of recreating the infrastructure.

Option A: Use moved Blocks (Terraform 1.1+)

Add a moved block to your configuration for each resource that was affected by the key change. This declaratively tells Terraform to rename the instance in the state file.

moved {
  from = azurerm_network_interface.nic["app-01"]
  to   = azurerm_network_interface.nic["app01"]
}

moved {
  from = azurerm_public_ip.pip["app-01"]
  to   = azurerm_public_ip.pip["app01"]
}

moved {
  from = azurerm_virtual_machine.vm["app-01"]
  to   = azurerm_virtual_machine.vm["app01"]
}

After adding the blocks, run terraform plan and terraform apply. Terraform will update the state without modifying your infrastructure.

Option B: Use terraform state mv

Alternatively, you can use the terraform state mv command to manually rename each resource instance in the state. Run these commands in a controlled environment with state locking enabled.

Move leaf resources (like IPs and NICs) before moving dependent resources (like VMs) to avoid temporary conflicts.

$ terraform state mv 'module.vm.azurerm_network_interface.nic["app-01"]' 'module.vm.azurerm_network_interface.nic["app01"]'
$ terraform state mv 'module.vm.azurerm_public_ip.pip["app-01"]' 'module.vm.azurerm_public_ip.pip["app01"]'
$ terraform state mv 'module.vm.azurerm_virtual_machine.vm["app-01"]' 'module.vm.azurerm_virtual_machine.vm["app01"]'

Solution 4: Implement Safeguards to Prevent Cascading Changes

Adopt these best practices to minimize the impact of future changes:

  • Ensure all dependent resources use the same stable key source.
  • Avoid using the create_before_destroy lifecycle argument unless necessary, as it can hide underlying issues.
  • Use lifecycle { ignore_changes = [tags] } sparingly. It can mask symptoms of drift rather than addressing the root cause.
  • Confirm that your state backend has locking enabled to prevent concurrent operations from corrupting the state.

Solution 5: Perform a Final Validation

After applying a fix, run a dry run to validate that the issue is resolved and no destructive changes are planned.

  1. Initialize Terraform and upgrade providers.

    $ terraform init -upgrade
  2. Validate the configuration syntax.

    $ terraform validate
  3. Generate a plan and confirm that it proposes zero resource destructions.

    $ terraform plan
  4. Apply the changes, monitoring the output to ensure no infrastructure is recreated.

    $ terraform apply

Outcome

By implementing these solutions, you can achieve the following outcomes:

  • No unintended resource destructions or recreations.
  • Stable IP addresses and network interface attachments.
  • Faster and more predictable plan and apply operations.
  • A clean state file where resource instances are tied to stable, immutable keys.

Document Location

Worldwide

[{"Type":"MASTER","Line of Business":{"code":"LOB77","label":"Automation Platform"},"Business Unit":{"code":"BU048","label":"IBM Software"},"Product":{"code":"SSGH5YK","label":"IBM Terraform Self-Managed"},"ARM Category":[{"code":"","label":""}],"ARM Case Number":"","Platform":[{"code":"PF025","label":"Platform Independent"}],"Version":"All Version(s)"}]

Historical Number

47291930108563

Document Information

Modified date:
16 March 2026

UID

ibm17266069