DEV Community

Cover image for Breaking the Loop: Solving Circular Dependencies in Azure Firewall Routing with Terraform
david
david

Posted on • Originally published at woitzik.dev

Breaking the Loop: Solving Circular Dependencies in Azure Firewall Routing with Terraform

You add a Route Table to force all internet-bound traffic (0.0.0.0/0) from your Spoke VNets into an Azure Firewall. You run terraform plan.

Error: Cycle: azurerm_subnet_route_table_association.spoke_binding,
azurerm_route_table.spoke_udr, azurerm_firewall.fw ...
Enter fullscreen mode Exit fullscreen mode

Terraform has deadlocked. And even if you fix the cycle — a plain 0.0.0.0/0 route will silently break Windows VM activation and Managed Identity authentication three days later.

Here's why both happen and how to fix them cleanly.

The Cycle Error

Terraform can't resolve the dependency graph:

  • The Route Table needs the Firewall's private_ip_address
  • The Firewall needs AzureFirewallSubnet to exist first
  • The Subnet Association tries to bind everything simultaneously

The fix: directly reference azurerm_firewall.fw.ip_configuration[0].private_ip_address in the Route Table. Terraform can now unambiguously resolve the order:

Firewall → (has IP) → Route Table → Subnet Association
Enter fullscreen mode Exit fullscreen mode

No workarounds. Just correct resource ordering.

The PaaS Trap

Once the cycle is fixed, most engineers celebrate and move on. Three days later:

  • Windows VMs lose activation — Azure KMS traffic is trapped by 0.0.0.0/0
  • Managed Identities stop authenticating — Azure AD traffic hits the unconfigured firewall

The fix is two explicit bypass routes that must exist before the Route Table is attached to any subnet:

# KMS Bypass — required for Windows VM activation
# 23.102.135.246/32 is Azure's global KMS endpoint (officially documented)
route {
  name           = "bypass-azure-kms"
  address_prefix = "23.102.135.246/32"
  next_hop_type  = "Internet"
}

# Azure AD Bypass — prevents Managed Identity auth lockouts
route {
  name           = "bypass-azure-ad"
  address_prefix = "AzureActiveDirectory"
  next_hop_type  = "Internet"
}
Enter fullscreen mode Exit fullscreen mode

Note: next_hop_type = "Internet" for Azure-owned IP ranges routes traffic over Azure's internal backbone — it never leaves Microsoft's network.

Scaling to Multiple Spokes

Instead of duplicating the association resource for every Spoke, use for_each:

resource "azurerm_subnet_route_table_association" "spoke_routing" {
  for_each       = var.spoke_subnet_ids
  subnet_id      = each.value
  route_table_id = azurerm_route_table.spoke_udr.id
}
Enter fullscreen mode Exit fullscreen mode

Add a new Spoke to the variable map, run terraform apply — done.


The free baseline (cycle fix + route table structure) is on GitHub. The full article with complete code, IP Group scaling, and FQDN baseline policies is on my blog.

👉 Full article on woitzik.dev

👉 Free GitHub repo

Top comments (0)