How to implement a Static Outbound IP on Azure App Service via Terraform

Recently, a client asked me with a specific request: they needed a fixed outbound IP address for their Azure App Service to ensure consistent communication with external services. While Azure App Service in the Premium V3 tier offers stable IP addresses, I learned that this stability isn’t guaranteed with the newer Premium V4 tier, where outbound IPs are dynamic by design

To future-proof the solution and meet the client’s needs, I opted for a more robust approach using a NAT Gateway with Virtual Network (VNet) integration.

Below, I’ll walk you through the process, the Terraform configuration, and some key lessons learned along the way.

Setting Up a Static Outbound IP with Terraform

To achieve this, I configured a Virtual Network, integrated it with the App Service, and routed outbound traffic through a NAT Gateway with a static public IP. Here’s the Terraform configuration I used, along with explanations of the critical components.

Terraform Implementation

Virtual Network and Subnet Configuration: A VNet is created with a dedicated subnet for the App Service. The subnet is delegated to Microsoft.Web/serverFarms to allow App Service integration, and service endpoints are enabled for Microsoft.Web.

resource "azurerm_virtual_network" "vnet" {
  name                = "vnet"
  resource_group_name = azurerm_resource_group.rg.name
  location            = "West Europe"
  address_space       = ["10.0.0.0/16"]
}

resource "azurerm_subnet" "appsrv_subnet" {
  name                 = appsrv-subnet"
  resource_group_name  = azurerm_resource_group.rg.name
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefixes     = ["10.0.1.0/24"]

  # Allow App Service into this subnet
  service_endpoints = ["Microsoft.Web"]
  delegation {
    name = "delegation"
    service_delegation {
      name    = "Microsoft.Web/serverFarms"
      actions = ["Microsoft.Network/virtualNetworks/subnets/action"]
    }
  }
}

Static Public IP and NAT Gateway: A static public IP is allocated and associated with a NAT Gateway. The NAT Gateway is then linked to the subnet to route outbound traffic through the static IP.

resource "azurerm_public_ip" "pip" {
name = "pip"
resource_group_name = azurerm_resource_group.rg.name
location = "West Europe"
allocation_method = "Static"
sku = "Standard"
}

resource "azurerm_nat_gateway" "ng" {
name = "nat-gateway"
resource_group_name = azurerm_resource_group.rg.name
location = "West Europe"
}

resource "azurerm_nat_gateway_public_ip_association" "ng_pip_association" {
nat_gateway_id = azurerm_nat_gateway.ng.id
public_ip_address_id = azurerm_public_ip.pip.id
}

resource "azurerm_subnet_nat_gateway_association" "subnet_ng_association" {
subnet_id = azurerm_subnet.appsrv_subnet.id
nat_gateway_id = azurerm_nat_gateway.ng.id
}

App Service with VNet Integration: The App Service is configured with vnet_route_all_enabled = true to ensure all outbound traffic is routed through the VNet. The integration is established using the azurerm_app_service_virtual_network_swift_connection resource.

resource "azurerm_linux_web_app" "appsrv" {
  name                = "appsrv"
  resource_group_name = azurerm_resource_group.rg.name
  location            = "West Europe"
  service_plan_id     = azurerm_service_plan.appplan.id
  https_only          = true
  site_config {
    application_stack {
      dotnet_version = "8.0"
    }
    http2_enabled                     = true
    vnet_route_all_enabled            = true
  }
}

resource "azurerm_app_service_virtual_network_swift_connection" "vnet_swift" {
  app_service_id = azurerm_linux_web_app.appsrv.id
  subnet_id      = azurerm_subnet.appsrv_subnet.id
}

Output the Static IP: Finally, the static outbound IP is outputted for reference, which can be used for whitelisting with external services.

output "static_outbound_ip" {
description = "Static outbound IP for App Service"
value = azurerm_public_ip.pip.ip_address
}

Critical Configuration Notes

  • VNet Routing: Setting vnet_route_all_enabled = true in the App Service’s site_config is essential to ensure all outbound traffic goes through the VNet and, consequently, the NAT Gateway.
  • Subnet Delegation: The subnet must be delegated to Microsoft.Web/serverFarms to support App Service integration.
  • NAT Gateway Support: This setup works on Basic, Standard, Premium, PremiumV2, and PremiumV3 App Service plans, making it compatible with most tiers.

Challenges with Terraform and Swift Connection

One recurring issue I encountered was with the azurerm_app_service_virtual_network_swift_connection resource. Occasionally, Terraform would attempt to recreate or update the VNet integration unexpectedly, even when no changes were made to the configuration. This behavior has been noted in community discussions, where the virtual_network_subnet_id attribute can be reset or cleared during subsequent Terraform runs. To mitigate this, I added a lifecycle block to ignore changes to certain attributes.

resource "azurerm_linux_web_app" "appsrv" {
  # ... existing config ...
  lifecycle {
    ignore_changes = [
      virtual_network_subnet_id,
    ]
  }

Verifying the Setup

After deployment, it’s crucial to confirm that the VNet integration is active and the static IP is applied to outbound traffic. In the Azure Portal, navigate to the App Service’s Networking section under Properties. If the VNet integration is listed and active, and the outbound IP matches the static IP assigned to the NAT Gateway, the setup is working as intended. If the integration status indicates an issue, the static IP won’t apply to outbound requests, so double-check the configuration.

Final Thoughts

With the Premium V4 tier not providing stable outbound IPs, this NAT Gateway solution ensures a consistent outbound IP regardless of the App Service plan tier. Whether you upgrade to V4 or stays on V3, your outbound traffic will remain predictable, avoiding potential disruptions with external services.

While the Terraform setup can be complex and occasionally finicky, especially with the swift connection resource; the result is a robust, future-proof solution. If you’re facing similar challenges or need to whitelist a static IP for third-party integrations, this approach is worth considering.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top