hashicorp / terraform-provider-azurerm

Terraform provider for Azure Resource Manager
https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs
Mozilla Public License 2.0
4.52k stars 4.6k forks source link

Subnet Resource Recreation #27251

Open ZarakiKenpachi7 opened 2 weeks ago

ZarakiKenpachi7 commented 2 weeks ago

Is there an existing issue for this?

Community Note

Terraform Version

1.9.5

AzureRM Provider Version

4.0.1

Affected Resource(s)/Data Source(s)

azurerm_virtual_network

Terraform Configuration Files

1. ./modules/network/vnet.tf :

resource "azurerm_virtual_network" "vnet" {
  for_each            = var.networks
  name                = "vnet-${each.key}"
  resource_group_name = var.resource_group_name
  location            = var.rg_location
  address_space       = each.value.address_space
  dns_servers         = each.value.dns_servers

  dynamic "subnet" {
    for_each = each.value.subnets
    content {
      name                                          = subnet.key != "AzureBastionSubnet" && subnet.key != "GatewaySubnet" ? "snet-${subnet.key}" : subnet.key
      address_prefixes                              = subnet.value.address_prefixes
      security_group                                = subnet.key != "GatewaySubnet" ? azurerm_network_security_group.nsg["${each.key}-${subnet.key}"].id : null
      delegation                                    = subnet.value.delegation
      private_endpoint_network_policies             = subnet.value.private_endpoint_network_policies
      private_link_service_network_policies_enabled = subnet.value.private_link_service_network_policies_enabled
      service_endpoints                             = subnet.value.service_endpoints
    }
  }

  tags = var.common_tags
  depends_on = [
    azurerm_network_security_group.nsg
  ]
}

2. ./modules/network/providers.tf :

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "4.0.1"
    }
  }
}

3. ./modules/network/variables.tf :

variable "opgroup" {
  description = "Name of the Group"
  type        = string
}

variable "location" {
  type        = string
  description = "The Azure region where this component is to be deployed, i.e. uksouth"
}

variable "client_name" {
  type = string
}

variable "environment" {
  description = "Environment used for naming purposes and tags"
  type        = string
}

variable "subscription_id" {
  type = string
}

variable "resource_group_name" {
  type = string
}

variable "rg_location" {
  type = string
}

variable "networks" {
  description = "Definition for each virtual network we want to create"
  type = map(
    object({
      address_space = list(string)
      dns_servers   = optional(list(string), [])
      subnets = map(
        object({
          address_prefixes  = list(string)
          service_endpoints = optional(list(string))
          delegation = optional(list(object({
            name = string
            service_delegation = list(object({
              name    = string
              actions = list(string)
            }))
          })))
          private_endpoint_network_policies             = optional(string)
          private_link_service_network_policies_enabled = optional(string)
        })
      )
    })
  )
}

Now, at the top level :

1. ./main.tf :

module "network" {
  source                                   = "./modules/network"
  for_each                                 = var.networks
  opgroup                                  = var.opgroup
  location                                 = each.key
  environment                              = var.environment
  resource_group_name                      = "network"
  rg_location                              = "southeastasia"
  networks                                 = each.value
  subscription_id                          = var.subscription_id
  client_name                              = var.client_name
  providers = {
    azurerm = azurerm
  }
}

2. ./variables.tf :

variable "networks" {
  type = map(
    map( 
      object({
        address_space = list(string)
        dns_servers   = optional(list(string), [])
        subnets = map(
          object({
            address_prefixes  = list(string)
            service_endpoints = optional(list(string))
            delegation = optional(list(object({
              name = string
              service_delegation = list(object({
                name    = string
                actions = optional(list(string))
              }))
            })))
            private_endpoint_network_policies             = optional(string)
            private_link_service_network_policies_enabled = optional(string)
          })
        )
      })
    )
  )
  default = null
}

variable "subscription_id" {
  type = string
}

variable "client_name" {
  type = string
}

variable "region" {
  type = string
}

variable "environment" {
  description = "Environment used for naming purposes and tags"
  type        = string
  validation {
    condition     = contains(["p", "x", "d", "s"], var.environment)
    error_message = "Environment not found."
  }
}

variable "opgroup" {
  type        = string
  description = "Name of the Group"
}

3. ./terraform.auto.tfvars :

subscription_id = "XXXXXXXXXXXXXXXXXXXXXXXXXXX"
environment     = "p"
region          = "southeastasia"
opgroup         = "xxxx"
client_name     = "xxxxxxx"

networks = {
  "southeastasia" = {
    "dmz-01" = {
      address_space = ["10.37.1.0/24"]
      subnets = {
        "frontend" = {
          address_prefixes  = ["10.37.1.0/27"]
          service_endpoints = ["Microsoft.Storage", "Microsoft.KeyVault", "Microsoft.EventHub"]
          delegation = [{
            name = "shubhdelegation"
            service_delegation = [{
              name = "Microsoft.Web/serverFarms"
            }]
          }]
        }
      }
    }
  }
}

Debug Output/Panic Output

The apply succeeds in the first instance and applies and creates the vnet and the subnets and the subnet properties. The issue is when I, right after this run a Terraform plan :

The plan everytime tries to delete and recreate the subnet :

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # module.network["southeastasia"].azurerm_virtual_network.vnet["dmz-01"] will be updated in-place
  ~ resource "azurerm_virtual_network" "vnet" {
        id                      = "/subscriptions/10e0ad56-8242-45e7-b95a-a64f4eb4542f/resourceGroups/network/providers/Microsoft.Network/virtualNetworks/vnet-dmz-01"      
        name                    = "vnet-dmz-01"
      ~ subnet                  = [
          - {
              - address_prefixes                              = [
                  - "10.37.1.0/27",
                ]
              - default_outbound_access_enabled               = true
              - delegation                                    = [
                  - {
                      - name               = "shubhdelegation"
                      - service_delegation = [
                          - {
                              - actions = [
                                  - "Microsoft.Network/virtualNetworks/subnets/action",
                                ]
                              - name    = "Microsoft.Web/serverFarms"
                            },
                        ]
                    },
                ]
              - id                                            = "/subscriptions/10e0ad56-8242-45e7-b95a-a64f4eb4542f/resourceGroups/network/providers/Microsoft.Network/virtualNetworks/vnet-dmz-01/subnets/snet-frontend"
              - name                                          = "snet-frontend"
              - private_endpoint_network_policies             = "Disabled"
              - private_link_service_network_policies_enabled = true
              - security_group                                = "/subscriptions/10e0ad56-8242-45e7-b95a-a64f4eb4542f/resourceGroups/network/providers/Microsoft.Network/networkSecurityGroups/nsg-vnet-frontend"
              - service_endpoint_policy_ids                   = []
              - service_endpoints                             = [
                  - "Microsoft.EventHub",
                  - "Microsoft.KeyVault",
                  - "Microsoft.Storage",
                ]
                # (1 unchanged attribute hidden)
            },
          + {
              + address_prefixes                              = [
                  + "10.37.1.0/27",
                ]
              + default_outbound_access_enabled               = true
              + delegation                                    = [
                  + {
                      + name               = "shubhdelegation"
                      + service_delegation = [
                          + {
                              + actions = []
                              + name    = "Microsoft.Web/serverFarms"
                            },
                        ]
                    },
                ]
              + id                                            = (known after apply)
              + name                                          = "snet-frontend"
              + private_endpoint_network_policies             = "Disabled"
              + private_link_service_network_policies_enabled = true
              + security_group                                = "/subscriptions/10e0ad56-8242-45e7-b95a-a64f4eb4542f/resourceGroups/network/providers/Microsoft.Network/networkSecurityGroups/nsg-vnet-frontend"
              + service_endpoint_policy_ids                   = []
              + service_endpoints                             = [
                  + "Microsoft.EventHub",
                  + "Microsoft.KeyVault",
                  + "Microsoft.Storage",
                ]
                # (1 unchanged attribute hidden)
            },
        ]
        tags                    = {}
        # (8 unchanged attributes hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

Expected Behaviour

No Configuration Changes

Actual Behaviour

It tries to recreate the subnet every time a TF plan runs. Even though there is no change in the configuration.

Steps to Reproduce

terraform init terraform plan terraform apply --auto-approve

Important Factoids

No

References

No response

lonegunmanb commented 1 week ago

Hi @ZarakiKenpachi7 thanks for reporting this issue, I can reproduce this issue on my side. According to the schema, the whole subnet nested block's type is set(object), which means once any element has changed any attribute, the element would be treated as a whole new element, since the hash of object has been changed. Unlike list we can compare element based on index, any change to set's element would result in removing old instance and insert the new one.

Is that possible to use azurerm_subnet resource instead of nested subnet block inside azurerm_virtual_network resource? I think that could solve your issue, or, you can set subnet[0].delegation[0].service_delegation[0].actions to ["Microsoft.Network/virtualNetworks/subnets/action"] as workaround.