Terraform in recreating resources even if no changes in version, code

Hi,
I am just removing a resource (myvm) from a module (Azure), no changes in module definition, but my plan is showing it needs to recreate other VMs(myvm1) also. Why it is so? I have no change in version and other dependencies.

module "terraform-azurerm-ecb-qa-windows-vm" {
  source              = "../modules/windowsservers"
  resource_group_name = azurerm_resource_group.rg.name
  is_windows_image    = true
  admin_password      = var.vm_password
  admin_username      = var.vm_username
  nsg_name            = "nsg_dev${local.dev_env_number}"
  vnet_subnet_id      = module.terraform-azurerm-ecb-test-network.vnet_subnets[0]
  vm_hostname = {
    "myvm" = {
      hostname = "myvm",
      vm_os_id = local.image
    },
    "myvm1" = {
      hostname = "myvm1",
      vm_os_id = local.image
    },
  }
  tags = {
    environment = "dev${local.dev_env_number}"
    costcenter  = "it"
  }

  depends_on = [azurerm_resource_group.rg, module.terraform-azurerm-ecb-test-network]
}

Plan output:

  # module.terraform-azurerm-ecb-test-windows-vm.azurerm_virtual_machine.vm-windows["myvm1"] must be replaced
-/+ resource "azurerm_virtual_machine" "vm-windows" {
      + availability_set_id              = (known after apply)
        delete_data_disks_on_termination = true
        delete_os_disk_on_termination    = true
      ~ id                               = "/subscriptions/b8e7-4236-a75c-4b0a99245758/resourceGroups/rg-spoke-dev02/providers/Microsoft.Compute/virtualMachines/myvm1" -> (known after apply)
        license_type                     = "Windows_Server"
      ~ location                         = "westeurope" -> (known after apply) # forces replacement
        name                             = "myvm1"
      ~ network_interface_ids            = [

Hi @indm03,

Unfortunately what you’ve shared isn’t really enough information to determine what’s happening here. I’d like to see the definition of this resource "azurerm_virtual_machine" "vm-windows" you showed in your plan output, since I think the way you’ve defined location is likely to be a clue here. I also assume there must be something else changing in this plan alongside this VM which would then lead to that location changing, so if there were other changes described in the plan output then it would help to share those too, so it’s possible to see how planned changes are flowing between your multiple resources.

Hi @apparentlymart ,
Thanks for your reply. I am trying to explain below, please let me know if it is helpful.

My main.tf is below:

resource "azurerm_resource_group" "rg" {
  name     = "rg-spoke-dev${local.dev_env_number}"
  location = var.location
}

module "terraform-azurerm-ecb-test-windows-vm" {
  source              = "../modules/windowsservers"
  resource_group_name = azurerm_resource_group.rg.name
  is_windows_image    = true
  admin_password      = var.vm_password
  admin_username      = var.vm_username
  nsg_name            = "nsg_dev${local.dev_env_number}"
  vnet_subnet_id      = module.terraform-azurerm-ecb-test-network.vnet_subnets[0]
  vm_hostname = {
    "myvm" = {
      hostname = "myvm",
      vm_os_id = local.image
    },
  }
  tags = {
    environment = "dev${local.dev_env_number}"
    costcenter  = "it"
  }

  depends_on = [azurerm_resource_group.rg, module.terraform-azurerm-ecb-test-network]
}

module "terraform-azurerm-ecb-test-network" {
  source              = "../modules/network"
  resource_group_name = azurerm_resource_group.rg.name
  address_space       = "x.x.x.x/26"
  subnet_prefixes     = ["x.x.x.x/27", "y.y.y.y/28", "z.z.z.z/28"]
  subnet_names        = ["subnet1", "AzureBastionSubnet", "subnet2"]
  vnet_name           = "vnet_dev${local.dev_env_number}"
  nsg_name            = "nsg_dev${local.dev_env_number}"
  rt_name             = "rt_dev${local.dev_env_number}"

  tags = {
    environment = "dev${local.dev_env_number}"
    costcenter  = "it"
  }

  depends_on = [azurerm_resource_group.rg]
}

variable.tf for same:

variable "location" {
  default     = "westeurope"
  description = ""
}

module definition for terraform-azurerm-ecb-test-windows-vm:

data "azurerm_resource_group" "vm" {
  name = var.resource_group_name
}

resource "random_id" "vm-sa" {
  for_each = var.vm_hostname
  keepers = {
    vm_hostname = each.value.hostname
  }

  byte_length = 6
}

resource "azurerm_storage_account" "vm-sa" {
  for_each                 = var.vm_hostname
  name                     = "bootdiag${lower(random_id.vm-sa[each.key].hex)}"
  resource_group_name      = data.azurerm_resource_group.vm.name
  location                 = coalesce(var.location, data.azurerm_resource_group.vm.location)
  account_tier             = element(split("_", var.boot_diagnostics_sa_type), 0)
  account_replication_type = element(split("_", var.boot_diagnostics_sa_type), 1)
  tags                     = var.tags
}

resource "azurerm_virtual_machine" "vm-windows" {
  for_each                         = var.vm_hostname
  name                             = each.value.hostname
  resource_group_name              = data.azurerm_resource_group.vm.name
  location                         = coalesce(var.location, data.azurerm_resource_group.vm.location)
  vm_size                          = var.vm_size
  network_interface_ids            = [azurerm_network_interface.vm[each.key].id]
  depends_on                       = [azurerm_network_interface_security_group_association.test]
  delete_os_disk_on_termination    = var.delete_os_disk_on_termination
  delete_data_disks_on_termination = var.delete_data_disks_on_termination
  license_type                     = var.license_type

  dynamic "identity" {
    for_each = length(var.identity_ids) == 0 && var.identity_type == "SystemAssigned" ? [var.identity_type] : []
    content {
      type = var.identity_type
    }
  }

  dynamic "identity" {
    for_each = length(var.identity_ids) > 0 || var.identity_type == "UserAssigned" ? [var.identity_type] : [ ]
    content {
      type         = var.identity_type
      identity_ids = length(var.identity_ids) > 0 ? var.identity_ids : []
    }
  }

  storage_image_reference {
    id        = each.value.vm_os_id
    publisher = each.value.vm_os_id == "" ? coalesce(var.vm_os_publisher, module.os.calculated_value_os_publisher) : ""
    offer     = each.value.vm_os_id == "" ? coalesce(var.vm_os_offer, module.os.calculated_value_os_offer) : ""
    sku       = each.value.vm_os_id == "" ? coalesce(var.vm_os_sku, module.os.calculated_value_os_sku) : ""
    version   = each.value.vm_os_id == "" ? var.vm_os_version : ""
  }

  storage_os_disk {
    name              = "${each.value.hostname}-osdisk"
    create_option     = "FromImage"
    caching           = "ReadWrite"
    managed_disk_type = var.storage_account_type
  }

  dynamic "storage_data_disk" {
    for_each = range(var.nb_data_disk)
    content {
      name              = "${each.value.hostname}-datadisk-${storage_data_disk.value}"
      create_option     = "Empty"
      lun               = storage_data_disk.value
      disk_size_gb      = var.data_disk_size_gb
      managed_disk_type = var.data_sa_type
    }
  }

  os_profile {
    computer_name  = each.value.hostname
    admin_username = var.admin_username
    admin_password = var.admin_password
  }

  tags = var.tags

  os_profile_windows_config {
    provision_vm_agent = true
  }

  boot_diagnostics {
    enabled     = var.boot_diagnostics
    storage_uri = var.boot_diagnostics ? join(",", azurerm_storage_account.vm-sa.*.primary_blob_endpoint) : ""
  }
}

data "azurerm_network_security_group" "vm" {
  name                = var.nsg_name
  resource_group_name = data.azurerm_resource_group.vm.name
}

resource "azurerm_network_interface" "vm" {
  for_each                      = var.vm_hostname
  name                          = "${each.value.hostname}-nic"
  resource_group_name           = data.azurerm_resource_group.vm.name
  location                      = coalesce(var.location, data.azurerm_resource_group.vm.location)
  enable_accelerated_networking = var.enable_accelerated_networking

  ip_configuration {
    name                          = "${each.value.hostname}-ip"
    subnet_id                     = var.vnet_subnet_id
    private_ip_address_allocation = "Dynamic"
  }

  tags = var.tags
}

resource "azurerm_network_interface_security_group_association" "test" {
  for_each                  = var.vm_hostname
  network_interface_id      = azurerm_network_interface.vm[each.key].id
  network_security_group_id = data.azurerm_network_security_group.vm.id

  depends_on = [azurerm_network_interface.vm]
}

Plan file is showing recreate for all resources like below:

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create
  - destroy
-/+ destroy and then create replacement
 <= read (data resources)

Terraform will perform the following actions:

  # module.terraform-azurerm-ecb-qa-windows-vm.data.azurerm_network_security_group.vm will be read during apply
  # (config refers to values not yet known)
 <= data "azurerm_network_security_group" "vm"  {
      ~ id                  = "xx/nsg_dev02" -> (known after apply)
      ~ location            = "westeurope" -> (known after apply)
        name                = "nsg_dev02"
        resource_group_name = "rg-spoke-dev02"
      ~ security_rule       = [] -> (known after apply)
      ~ tags                = {
          - "costcenter"  = "it"
          - "environment" = "dev02"
        } -> (known after apply)

      + timeouts {
          + read = (known after apply)
        }
    }

  # module.terraform-azurerm-ecb-qa-windows-vm.data.azurerm_resource_group.vm will be read during apply
  # (config refers to values not yet known)
 <= data "azurerm_resource_group" "vm"  {
      ~ id       = "xx/rg-spoke-dev02" -> (known after apply)
      ~ location = "westeurope" -> (known after apply)
        name     = "rg-spoke-dev02"
      ~ tags     = {} -> (known after apply)

      + timeouts {
          + read = (known after apply)
        }
    }

  # module.terraform-azurerm-ecb-qa-windows-vm.azurerm_network_interface.vm["kamelia"] must be replaced
-/+ resource "azurerm_network_interface" "vm" {
      ~ applied_dns_servers           = [] -> (known after apply)
      ~ dns_servers                   = [] -> (known after apply)
        enable_accelerated_networking = false
        enable_ip_forwarding          = false
      ~ id                            = "xx/resourceGroups/rg-spoke-dev02/providers/Microsoft.Network/networkInterfaces/xx-nic" -> (known after apply)
      + internal_dns_name_label       = (known after apply)
      ~ internal_domain_name_suffix   = "hus0d4jfoexezht0x4bml4qlxh.ax.internal.cloudapp.net" -> (known after apply)
      ~ location                      = "westeurope" -> (known after apply) # forces replacement
      ~ mac_address                   = "xx" -> (known after apply)
        name                          = "xx-nic"
      ~ private_ip_address            = "xx" -> (known after apply)
      ~ private_ip_addresses          = [
          - "xx",
        ] -> (known after apply)
        resource_group_name           = "rg-spoke-dev02"
        tags                          = {
            "costcenter"  = "it"
            "environment" = "dev02"
        }
      ~ virtual_machine_id            = "xx/resourceGroups/rg-spoke-dev02/providers/Microsoft.Compute/virtualMachines/xx" -> (known after apply)

      ~ ip_configuration {
            name                          = "xx-ip"
          ~ primary                       = true -> (known after apply)
          ~ private_ip_address            = "xx" -> (known after apply)
          ~ private_ip_address_allocation = "Dynamic" -> "dynamic"
            private_ip_address_version    = "IPv4"
            subnet_id                     = "xx/resourceGroups/rg-spoke-dev02/providers/Microsoft.Network/virtualNetworks/vnet_dev02/subnets/subnet1"
        }
    }

Hi @indm03,

I think what’s happening here is that Terraform is reacting to the depends_on argument you wrote in the module definition:

  depends_on = [azurerm_resource_group.rg, module.terraform-azurerm-ecb-test-network]

When you use depends_on like this you force Terraform to be quite pessimistic in how it resolves dependencies, because now everything inside that module must depend on these two objects, and one of them is itself a module and so implies depending on many other objects too.

It doesn’t seem like either of these depends_on in your module are actually needed, because all of the necessary dependencies seem to already be described by the references in other arguments. For that reason, I’d suggest trying to remove depends_on from both of your module blocks to see if that makes the situation better. It should be very rare to use depends_on in a module block because in most cases either Terraform can infer the dependencies automatically itself (as I think is the case here) or there are better ways to describe the dependencies more precisely inside your module’s output blocks.

This also seems like a situation where we’ve made Terraform’s dependency resolver more precise in later versions of Terraform and so if you are not on the latest release then upgrading may cause Terraform to handle the depends_on arguments better, although I would still suggest removing them unless there is a specific reason why you included them.

Hi @apparentlymart ,
This actually solved my issue here :slight_smile: thanks a lot. I remember that I might was facing some dependency issue while calling VM module where it was searching ASG information first to bind with NIC before ASG was actually created etc. That’s why I had put all depends_on. But I need to read now and come up with more accurate strategy to handle that part. Thanks a lot again.

Great! I’m glad to hear that it worked out.

As a starting point for learning about other options for managing dependencies between modules, I’d suggest looking at the depends_on argument for Output Values.

I’m not familiar enough with the VM/ASG/NIC situation to show a real example of that, but another typical situation where this sort of problem can arise is assigning policies to objects in order to make them usable. In many cloud systems, including Azure I believe, there is a distinction between declaring an object, declaring a policy, and attaching the policy to the object, and so with Terraform’s default dependency inference the policy attachment ends up being dependent on the object rather than the other way around, and so any other component referring to the object wouldn’t naturally also depend on the policy attachment.

If you’re writing a module that returns an object with a policy attached to it then, you can properly encapsulate that module’s behavior by adding a depends_on argument to the output value to document those additional dependencies that Terraform can’t automatically see:

resource "azurerm_resource_group" "example" {
  name     = "example"
  location = "West Europe"
}

resource "azurerm_policy_definition" "example" {
  name         = "example"
  # etc...
}

resource "azurerm_policy_assignment" "example" {
  name = "example"

  # These references mean that the policy assignment
  # depends on the resource group and the policy.
  scope                = azurerm_resource_group.example.id
  policy_definition_id = azurerm_policy_definition.example.id
  # etc...
}

output "resource_group_name" {
  # This value expression means that the output
  # automatically depends on the resource group.
  value = azurerm_resource_group.example.name

  # ...but the resource group isn't really complete
  # until the policy is assigned to it, so we can use
  # depends_on to let Terraform know that.
  depends_on = [azurerm_policy_assignment.example]
}

The important advantage of placing the depends_on on the output value rather than on the calling module blocks is that this extra dependency is now encapsulated within the module. If another module calls this module and refers to the resource_group_name attribute then that reference will automatically depend on both the resource group and the policy assignment, without the module user needing to know the details of how the objects are connected together.

This is what I meant by “there are better ways to describe the dependencies more precisely inside your module’s output blocks” in my previous comment.