Skip to content

[Bug Report] vmss module causes unintended force replacement when used with moved block #157

Closed

Description

@jinkang23

Describe the bug

When a Virtual Machine Scale Set (VMSS) is created with create_public_ip = false, and instead pip_prefix_name and pip_prefix_resource_group_name are provided via the var.interfaces variable, the public IP prefix is retrieved using the data.azurerm_public_ip_prefix.allocate data source.

If the Terraform configuration is later refactored using a moved block, this can unintentionally trigger a forced replacement of the azurerm_linux_virtual_machine_scale_set.this resource. This happens because the public IP prefix ID, retrieved via the data source, is not known until the next terraform apply. As a result, Terraform treats the value as potentially changed and marks the VMSS for replacement.

To avoid this unintended replacement, we can run the following command first:
terraform apply -target=data.azurerm_public_ip_prefix.allocate

This ensures the public IP prefix ID is resolved before the full apply, preventing unnecessary resource recreation.

Module Version

3.3.8

Terraform version

1.12.2

Expected behavior

Using moved block to refactor Terraform template should not cause intended replacement of VMSS if PIP Prefix resource has not been recreated.

I propose that following changes are made to vmss module:

  • Remove data.azurerm_public_ip_prefix.allocate resource from vmss module.
  • Remove pip_prefix_name and pip_prefix_resource_group_name from var.interfaces variable
  • Add pip_prefix_id to var.interfaces variable so that the resource id can be passed directly to the azurerm_linux_virtual_machine_scale_set.this resource without the need to retrieve it via the data source.

Using a moved block to refactor a Terraform configuration should not trigger a replacement of the VMSS resource, provided that the associated Public IP Prefix (PIP Prefix) has not been recreated. However, the current implementation causes unintended replacements due to reliance on a data source that cannot resolve its value until the next terraform apply.

Proposed Changes to the vmss Module
To prevent unnecessary replacements and improve clarity and control, the following changes are recommended:

Remove the data.azurerm_public_ip_prefix.allocate data source from the vmss module.
This eliminates the dependency on a value that is not known during the planning phase.

Remove pip_prefix_name and pip_prefix_resource_group_name from the var.interfaces variable.
These are no longer needed if the full resource ID is passed directly.

Introduce a new variable pip_prefix_id in var.interfaces.
This allows the caller to explicitly pass the full resource ID of the Public IP Prefix, enabling the azurerm_linux_virtual_machine_scale_set.this resource to reference it directly without relying on a data source.

Benefits
Avoids unintended VMSS replacements during refactoring.
Simplifies the module interface by removing indirect lookups.
Improves predictability and stability of Terraform plans and applies.

Current behavior

When creating a Virtual Machine Scale Set (VMSS) with create_public_ip = false, the module expects the Public IP Prefix to be provided indirectly via:

  • pip_prefix_name
  • pip_prefix_resource_group_name

These values are passed through the var.interfaces variable. The module then uses a data source:

This data source retrieves the Public IP Prefix ID, which is then used in the azurerm_linux_virtual_machine_scale_set.this resource.

Problem During Refactoring
When the Terraform configuration is refactored using a moved block, Terraform attempts to preserve resource identity. However, because the data.azurerm_public_ip_prefix.allocate value is not known until apply time, Terraform cannot confirm whether the value has changed. As a result, it marks the VMSS resource for replacement, even if the actual PIP Prefix has not changed.

This behavior is unintended and can lead to unnecessary downtime and resource churn.

Anything else to add?

No response

Activity

  1. added
    bugSomething isn't working
    and removed
    bugSomething isn't working
    on Jul 9, 2025
  2. jinkang23 commented on Jul 15, 2025

    @jinkang23
    Author

    @acelebanski -I respecfully request that you reconsider this decision. I have observed similar behavior during a straightforward dns_servers modification within the vnet module, even in the absence of any moved or import blocks. Thank you.

    Here is an example of terraform plan with a dns_servers change:

    Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
      ~ update in-place
    -/+ destroy and then create replacement
     <= read (data resources)
    
    Terraform will perform the following actions:
    
    
      # module.vmss["region1_001_vmseries"].data.azurerm_public_ip_prefix.allocate["public"] will be read during apply
     <= data "azurerm_public_ip_prefix" "allocate" {
          + id                  = (known after apply)
          + ip_prefix           = (known after apply)
          + location            = (known after apply)
          + name                = "ippre-fw-example-ipam-region1-001"
          + prefix_length       = (known after apply)
          + resource_group_name = "rg-example-ipam-region1-001"
          + sku                 = (known after apply)
          + sku_tier            = (known after apply)
          + tags                = (known after apply)
          + zones               = (known after apply)
        }
    
      # module.vmss["region1_001_vmseries"].azurerm_linux_virtual_machine_scale_set.this must be replaced
    -/+ resource "azurerm_linux_virtual_machine_scale_set" "this" {
          ~ computer_name_prefix                              = "vmss-example-fw-region1-001" -> (known after apply)
          - encryption_at_host_enabled                        = false -> null
          ~ id                                                = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-example-fw-region1-001/providers/Microsoft.Compute/virtualMachineScaleSets/vmss-example-fw-region1-001" -> (known after apply)
            name                                              = "vmss-example-fw-region1-001"
          - secure_boot_enabled                               = false -> null
            tags                                              = {
            }
          ~ unique_id                                         = "00000000-0000-0000-0000-000000000000" -> (known after apply)
          - vtpm_enabled                                      = false -> null
            # (26 unchanged attributes hidden)
    
          ~ automatic_instance_repair (known after apply)
    
          ~ extension (known after apply)
    
          ~ identity {
              - identity_ids = [] -> null
              ~ principal_id = "00000000-0000-0000-0000-000000000000" -> (known after apply)
              ~ tenant_id    = "00000000-0000-0000-0000-000000000000" -> (known after apply)
                # (1 unchanged attribute hidden)
            }
    
          ~ network_interface {
              - dns_servers                   = [] -> null
                name                          = "management"
                # (4 unchanged attributes hidden)
    
              ~ ip_configuration {
                  - application_gateway_backend_address_pool_ids = [] -> null
                  - application_security_group_ids               = [] -> null
                  - load_balancer_backend_address_pool_ids       = [] -> null
                  - load_balancer_inbound_nat_rules_ids          = [] -> null
                    name                                         = "primary"
                    # (3 unchanged attributes hidden)
                }
            }
          ~ network_interface {
              - dns_servers                   = [] -> null
                name                          = "public"
                # (4 unchanged attributes hidden)
    
              ~ ip_configuration {
                  - application_gateway_backend_address_pool_ids = [] -> null
                  - application_security_group_ids               = [] -> null
                  - load_balancer_inbound_nat_rules_ids          = [] -> null
                    name                                         = "primary"
                    # (4 unchanged attributes hidden)
    
                  ~ public_ip_address {
                      ~ idle_timeout_in_minutes = 4 -> (known after apply)
                        name                    = "public"
                      ~ public_ip_prefix_id     = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-example-ipam-region1-001/providers/Microsoft.Network/publicIPPrefixes/ippre-fw-example-ipam-region1-001" -> (known after apply) # forces replacement
                        # (2 unchanged attributes hidden)
                    }
                }
            }
          ~ network_interface {
              - dns_servers                   = [] -> null
                name                          = "private"
                # (4 unchanged attributes hidden)
    
              ~ ip_configuration {
                  - application_gateway_backend_address_pool_ids = [] -> null
                  - application_security_group_ids               = [] -> null
                  - load_balancer_inbound_nat_rules_ids          = [] -> null
                    name                                         = "primary"
                    # (4 unchanged attributes hidden)
                }
            }
          ~ network_interface {
              - dns_servers                   = [] -> null
                name                          = "dmz"
                # (4 unchanged attributes hidden)
    
              ~ ip_configuration {
                  - application_gateway_backend_address_pool_ids = [] -> null
                  - application_security_group_ids               = [] -> null
                  - load_balancer_inbound_nat_rules_ids          = [] -> null
                    name                                         = "primary"
                    # (4 unchanged attributes hidden)
                }
            }
    
          ~ os_disk {
              ~ disk_size_gb                     = 60 -> (known after apply)
                # (6 unchanged attributes hidden)
            }
    
          ~ spot_restore (known after apply)
    
          ~ termination_notification (known after apply)
    
            # (5 unchanged blocks hidden)
        }
    
      # module.vmss["region1_001_vmseries"].azurerm_monitor_autoscale_setting.this[0] must be replaced
    -/+ resource "azurerm_monitor_autoscale_setting" "this" {
          ~ id                  = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-example-fw-region1-001/providers/Microsoft.Insights/autoScaleSettings/vmss-example-fw-region1-001" -> (known after apply)
            name                = "vmss-example-fw-region1-001"
            tags                = {
            }
          ~ target_resource_id  = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-example-fw-region1-001/providers/Microsoft.Compute/virtualMachineScaleSets/vmss-example-fw-region1-001" -> (known after apply) # forces replacement
            # (3 unchanged attributes hidden)
    
            # (2 unchanged blocks hidden)
        }
    
      # module.vmss["region2_001_vmseries"].data.azurerm_public_ip_prefix.allocate["public"] will be read during apply
     <= data "azurerm_public_ip_prefix" "allocate" {
          + id                  = (known after apply)
          + ip_prefix           = (known after apply)
          + location            = (known after apply)
          + name                = "ippre-fw-example-ipam-region2-001"
          + prefix_length       = (known after apply)
          + resource_group_name = "rg-example-ipam-region2-001"
          + sku                 = (known after apply)
          + sku_tier            = (known after apply)
          + tags                = (known after apply)
          + zones               = (known after apply)
        }
    
      # module.vmss["region2_001_vmseries"].azurerm_linux_virtual_machine_scale_set.this must be replaced
    -/+ resource "azurerm_linux_virtual_machine_scale_set" "this" {
          ~ computer_name_prefix                              = "vmss-example-fw-region2-001" -> (known after apply)
          - encryption_at_host_enabled                        = false -> null
          ~ id                                                = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-example-fw-region2-001/providers/Microsoft.Compute/virtualMachineScaleSets/vmss-example-fw-region2-001" -> (known after apply)
            name                                              = "vmss-example-fw-region2-001"
          - secure_boot_enabled                               = false -> null
            tags                                              = {
            }
          ~ unique_id                                         = "cee69998-3590-4983-a13c-839d14d94c84" -> (known after apply)
          - vtpm_enabled                                      = false -> null
            # (26 unchanged attributes hidden)
    
          ~ automatic_instance_repair (known after apply)
    
          ~ extension (known after apply)
    
          ~ identity {
              - identity_ids = [] -> null
              ~ principal_id = "00000000-0000-0000-0000-000000000000" -> (known after apply)
              ~ tenant_id    = "00000000-0000-0000-0000-000000000000" -> (known after apply)
                # (1 unchanged attribute hidden)
            }
    
          ~ network_interface {
              - dns_servers                   = [] -> null
                name                          = "management"
                # (4 unchanged attributes hidden)
    
              ~ ip_configuration {
                  - application_gateway_backend_address_pool_ids = [] -> null
                  - application_security_group_ids               = [] -> null
                  - load_balancer_backend_address_pool_ids       = [] -> null
                  - load_balancer_inbound_nat_rules_ids          = [] -> null
                    name                                         = "primary"
                    # (3 unchanged attributes hidden)
                }
            }
          ~ network_interface {
              - dns_servers                   = [] -> null
                name                          = "public"
                # (4 unchanged attributes hidden)
    
              ~ ip_configuration {
                  - application_gateway_backend_address_pool_ids = [] -> null
                  - application_security_group_ids               = [] -> null
                  - load_balancer_inbound_nat_rules_ids          = [] -> null
                    name                                         = "primary"
                    # (4 unchanged attributes hidden)
    
                  ~ public_ip_address {
                      ~ idle_timeout_in_minutes = 4 -> (known after apply)
                        name                    = "public"
                      ~ public_ip_prefix_id     = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-example-ipam-region2-001/providers/Microsoft.Network/publicIPPrefixes/ippre-fw-example-ipam-region2-001" -> (known after apply) # forces replacement
                        # (2 unchanged attributes hidden)
                    }
                }
            }
          ~ network_interface {
              - dns_servers                   = [] -> null
                name                          = "private"
                # (4 unchanged attributes hidden)
    
              ~ ip_configuration {
                  - application_gateway_backend_address_pool_ids = [] -> null
                  - application_security_group_ids               = [] -> null
                  - load_balancer_inbound_nat_rules_ids          = [] -> null
                    name                                         = "primary"
                    # (4 unchanged attributes hidden)
                }
            }
          ~ network_interface {
              - dns_servers                   = [] -> null
                name                          = "dmz"
                # (4 unchanged attributes hidden)
    
              ~ ip_configuration {
                  - application_gateway_backend_address_pool_ids = [] -> null
                  - application_security_group_ids               = [] -> null
                  - load_balancer_inbound_nat_rules_ids          = [] -> null
                    name                                         = "primary"
                    # (4 unchanged attributes hidden)
                }
            }
    
          ~ os_disk {
              ~ disk_size_gb                     = 60 -> (known after apply)
                # (6 unchanged attributes hidden)
            }
    
          ~ spot_restore (known after apply)
    
          ~ termination_notification (known after apply)
    
            # (5 unchanged blocks hidden)
        }
    
      # module.vmss["region2_001_vmseries"].azurerm_monitor_autoscale_setting.this[0] must be replaced
    -/+ resource "azurerm_monitor_autoscale_setting" "this" {
          ~ id                  = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-example-fw-region2-001/providers/Microsoft.Insights/autoScaleSettings/vmss-example-fw-region2-001" -> (known after apply)
            name                = "vmss-example-fw-region2-001"
            tags                = {
            }
          ~ target_resource_id  = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-example-fw-region2-001/providers/Microsoft.Compute/virtualMachineScaleSets/vmss-example-fw-region2-001" -> (known after apply) # forces replacement
            # (3 unchanged attributes hidden)
    
            # (2 unchanged blocks hidden)
        }
    
      # module.vnet["region1_001"].azurerm_virtual_network.this[0] will be updated in-place
      ~ resource "azurerm_virtual_network" "this" {
          ~ dns_servers                    = [
              ~ "10.1.1.1" -> "10.0.0.1",
            ]
            id                             = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-example-fw-region1-001/providers/Microsoft.Network/virtualNetworks/vnet-example-fw-region1-001"
            name                           = "vnet-example-fw-region1-001"
            tags                           = {
            }
            # (9 unchanged attributes hidden)
    
            # (2 unchanged blocks hidden)
        }
    
      # module.vnet["region2_001"].azurerm_virtual_network.this[0] will be updated in-place
      ~ resource "azurerm_virtual_network" "this" {
          ~ dns_servers                    = [
              ~ "10.1.1.1" -> "10.0.0.1",
            ]
            id                             = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-example-fw-region2-001/providers/Microsoft.Network/virtualNetworks/vnet-example-fw-region2-001"
            name                           = "vnet-example-fw-region2-001"
            tags                           = {
            }
            # (9 unchanged attributes hidden)
    
            # (2 unchanged blocks hidden)
        }
    
    Plan: 7 to add, 2 to change, 7 to destroy.
    
    ------------------------------------------------------------------------
    
  3. added
    enhancementNew feature or request
    and removed
    wontfixThis will not be worked on
    on Jul 16, 2025
  4. acelebanski commented on Jul 16, 2025

    @acelebanski
    Contributor

    Hello @jinkang23, I don't understand the dns_servers example, it attempts to update the vnets in place which is good.

    However, we decided to support specifying the Public IP Prefix by ID in parallel to supporting name & resource group name. We treat this as a one-off just to help in your specific use case. At the moment we're not planning to refactor the entire codebase to get rid of using data sources as it's still our preference.

  5. jinkang23 commented on Jul 16, 2025

    @jinkang23
    Author

    Hello @jinkang23, I don't understand the dns_servers example, it attempts to update the vnets in place which is good.

    However, we decided to support specifying the Public IP Prefix by ID in parallel to supporting name & resource group name. We treat this as a one-off just to help in your specific use case. At the moment we're not planning to refactor the entire codebase to get rid of using data sources as it's still our preference.

    Hi @acelebanski - Thank you for reconsidering and accepting my request.

    To clarify, my concern isn't about updating DNS servers on the vnet resource itself, but how this update triggers Terraform to mark resource "azurerm_linux_virtual_machine_scale_set" "this" for force replacement. Notably, passing the Public IP Prefix by ID (instead of via resource group/name using data sources) avoids this issue, which is my main concern.

  6. acelebanski commented on Jul 17, 2025

    @acelebanski
    Contributor

    @jinkang23 I cannot replicate your problem with DNS update on a VNET replacing VMSS. Probably something specific to your other code changes. Nevertheless, I'm going to implement a solution for you as described in my previous comment.

  7. self-assigned this
    on Oct 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions