diff --git a/internal/services/network/virtual_network_gateway_resource.go b/internal/services/network/virtual_network_gateway_resource.go index dd47f91ff4ef..cfa51f80bef2 100644 --- a/internal/services/network/virtual_network_gateway_resource.go +++ b/internal/services/network/virtual_network_gateway_resource.go @@ -5,7 +5,6 @@ package network import ( "bytes" - "context" "fmt" "log" "math" @@ -49,8 +48,6 @@ func resourceVirtualNetworkGateway() *pluginsdk.Resource { Delete: pluginsdk.DefaultTimeout(120 * time.Minute), }, - CustomizeDiff: pluginsdk.CustomizeDiffShim(resourceVirtualNetworkGatewayCustomizeDiff), - Schema: map[string]*pluginsdk.Schema{ "name": { Type: pluginsdk.TypeString, @@ -674,23 +671,6 @@ func resourceVirtualNetworkGateway() *pluginsdk.Resource { return resource } -func resourceVirtualNetworkGatewayCustomizeDiff(ctx context.Context, d *pluginsdk.ResourceDiff, _ interface{}) error { - gatewayType := d.Get("type").(string) - - // Validate that public_ip_address_id is not set for ExpressRoute gateways - if gatewayType == string(virtualnetworkgateways.VirtualNetworkGatewayTypeExpressRoute) { - ipConfigs := d.Get("ip_configuration").([]interface{}) - for i, ipConfigRaw := range ipConfigs { - ipConfig := ipConfigRaw.(map[string]interface{}) - if publicIPID, ok := ipConfig["public_ip_address_id"].(string); ok && publicIPID != "" { - return fmt.Errorf("`ip_configuration.%d.public_ip_address_id` cannot be set when `type` is set to `ExpressRoute`", i) - } - } - } - - return nil -} - func resourceVirtualNetworkGatewayCreate(d *pluginsdk.ResourceData, meta interface{}) error { client := meta.(*clients.Client).Network.VirtualNetworkGateways ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d) @@ -789,8 +769,7 @@ func resourceVirtualNetworkGatewayRead(d *pluginsdk.ResourceData, meta interface d.Set("sku", string(pointer.From(props.Sku.Name))) } - gatewayType := pointer.From(props.GatewayType) - if err := d.Set("ip_configuration", flattenVirtualNetworkGatewayIPConfigurations(props.IPConfigurations, gatewayType)); err != nil { + if err := d.Set("ip_configuration", flattenVirtualNetworkGatewayIPConfigurations(props.IPConfigurations)); err != nil { return fmt.Errorf("setting `ip_configuration`: %+v", err) } @@ -1427,7 +1406,7 @@ func flattenVirtualNetworkGatewayBgpPeeringAddresses(input *[]virtualnetworkgate return output, nil } -func flattenVirtualNetworkGatewayIPConfigurations(ipConfigs *[]virtualnetworkgateways.VirtualNetworkGatewayIPConfiguration, gatewayType virtualnetworkgateways.VirtualNetworkGatewayType) []interface{} { +func flattenVirtualNetworkGatewayIPConfigurations(ipConfigs *[]virtualnetworkgateways.VirtualNetworkGatewayIPConfiguration) []interface{} { flat := make([]interface{}, 0) if ipConfigs != nil { @@ -1446,12 +1425,9 @@ func flattenVirtualNetworkGatewayIPConfigurations(ipConfigs *[]virtualnetworkgat } } - // Do not include public_ip_address_id for ExpressRoute gateways - if gatewayType != virtualnetworkgateways.VirtualNetworkGatewayTypeExpressRoute { - if pip := props.PublicIPAddress; pip != nil { - if id := pip.Id; id != nil { - v["public_ip_address_id"] = *id - } + if pip := props.PublicIPAddress; pip != nil { + if id := pip.Id; id != nil { + v["public_ip_address_id"] = *id } } diff --git a/internal/services/network/virtual_network_gateway_resource_test.go b/internal/services/network/virtual_network_gateway_resource_test.go index 3ee72815ecec..1428887fbce8 100644 --- a/internal/services/network/virtual_network_gateway_resource_test.go +++ b/internal/services/network/virtual_network_gateway_resource_test.go @@ -6,6 +6,7 @@ package network_test import ( "context" "fmt" + "os" "testing" "github.com/hashicorp/go-azure-helpers/lang/pointer" @@ -355,6 +356,38 @@ func TestAccVirtualNetworkGateway_expressRoute(t *testing.T) { }) } +func TestAccVirtualNetworkGateway_expressRouteWithPublicIPAddressId(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_virtual_network_gateway", "test") + r := VirtualNetworkGatewayResource{} + // Brownfield-only: this must be an existing legacy ExpressRoute gateway that still + // returns `ip_configuration.0.public_ip_address_id`. Microsoft made auto-assigned + // Public IPs generally available in July 2025; gateways created since then use the + // HOBO model and do not expose that field. + legacyGatewayID := os.Getenv("ARM_TEST_LEGACY_EXPRESSROUTE_GATEWAY_ID") + + if legacyGatewayID == "" { + t.Skip("Skipping as `ARM_TEST_LEGACY_EXPRESSROUTE_GATEWAY_ID` was not specified") + } + + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.expressRouteWithPublicIPAddressIdBrownfield(legacyGatewayID), + ResourceName: data.ResourceName, + ImportState: true, + ImportStateId: legacyGatewayID, + ImportStateVerify: true, + }, + { + Config: r.expressRouteWithPublicIPAddressIdBrownfield(legacyGatewayID), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).Key("type").HasValue("ExpressRoute"), + check.That(data.ResourceName).Key("ip_configuration.0.public_ip_address_id").Exists(), + ), + }, + data.ImportStep(), + }) +} + func TestAccVirtualNetworkGateway_expressRouteErGwScale(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_virtual_network_gateway", "test") r := VirtualNetworkGatewayResource{} @@ -1420,6 +1453,43 @@ resource "azurerm_virtual_network_gateway" "test" { `, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger, data.RandomInteger) } +func (VirtualNetworkGatewayResource) expressRouteWithPublicIPAddressIdBrownfield(existingGatewayID string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +locals { + gateway_id = "%s" + gateway_name = split("/", local.gateway_id)[8] + resource_group_name = split("/", local.gateway_id)[4] +} + +data "azurerm_virtual_network_gateway" "existing" { + name = local.gateway_name + resource_group_name = local.resource_group_name +} + +resource "azurerm_virtual_network_gateway" "test" { + name = local.gateway_name + location = data.azurerm_virtual_network_gateway.existing.location + resource_group_name = local.resource_group_name + + type = "ExpressRoute" + vpn_type = data.azurerm_virtual_network_gateway.existing.vpn_type + sku = data.azurerm_virtual_network_gateway.existing.sku + generation = data.azurerm_virtual_network_gateway.existing.generation + + ip_configuration { + name = data.azurerm_virtual_network_gateway.existing.ip_configuration[0].name + public_ip_address_id = data.azurerm_virtual_network_gateway.existing.ip_configuration[0].public_ip_address_id + private_ip_address_allocation = data.azurerm_virtual_network_gateway.existing.ip_configuration[0].private_ip_address_allocation + subnet_id = data.azurerm_virtual_network_gateway.existing.ip_configuration[0].subnet_id + } +} +`, existingGatewayID) +} + func (VirtualNetworkGatewayResource) expressRouteErGwScale(data acceptance.TestData) string { return fmt.Sprintf(` provider "azurerm" { diff --git a/website/docs/r/virtual_network_gateway.html.markdown b/website/docs/r/virtual_network_gateway.html.markdown index 5795faa9a7ba..d68f227c95cc 100644 --- a/website/docs/r/virtual_network_gateway.html.markdown +++ b/website/docs/r/virtual_network_gateway.html.markdown @@ -170,7 +170,7 @@ The `ip_configuration` block supports: * `public_ip_address_id` - (Optional) The ID of the public IP address to associate with the Virtual Network Gateway. -~> **Note:** `public_ip_address_id` should not be specified when `type` is set to `ExpressRoute`. +~> **Note:** For `ExpressRoute` gateways, Azure may use auto-assigned Hosted-On-Behalf-Of (HOBO) public IPs for newer deployments. Existing gateways can still expose a `public_ip_address_id`. For more information, see [Auto-assigned public IP](https://learn.microsoft.com/en-us/azure/expressroute/expressroute-about-virtual-network-gateways#auto-assigned-public-ip). ---