Skip to content

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

☁️ Azure Virtual Hub Terraform Module

A standalone module for a single Azure Virtual Hub — the regional routing core of a Virtual WAN — targeting hashicorp/azurerm ~> 4.0. Secure-by-default: the empty call selects the full-featured Standard SKU and leaves branch-to-branch transit off until you ask for it.

Terraform Provider: azurerm Module Version Type: standalone Resources


🧩 Overview

This module manages one azurerm_virtual_hub and the static route blocks that live directly on it:

  • 🌐 Creates a Virtual Hub — the managed routing core that a Virtual WAN uses to connect VNets and branch sites in a region.
  • 🧭 Sets a routing preference for the hub's virtual router (ExpressRoute by default) and exposes the router's autoscale minimum capacity.
  • 🛣️ Carries optional inline static routes as a keyed map, so adding or removing one route never re-indexes the rest.
  • 🔒 Defaults to the Standard SKU — the SKU that unlocks hub routing features and the Secured Virtual Hub path — and leaves branch-to-branch transit disabled until a caller opts in.
  • 🧱 Emits the hub id, its default_route_table_id, and its virtual-router BGP identity so sibling modules (connections, route tables, gateways, firewall) can wire against it.

💡 Why it matters: the Virtual Hub is the pivot point of a Virtual WAN topology. Getting its SKU, address space, and routing preference right at creation time avoids a force-new replacement later that would disrupt every gateway and connection attached to it.

❤️ Support this project

If this module saves you time, please consider supporting the work:


🗺️ Where this fits in the family

The hub sits inside a Virtual WAN and is the anchor that connections, route tables, routing intent, gateways, and (for a Secured Virtual Hub) Azure Firewall attach to — each owned by its own sibling module and wired against this hub's id.

flowchart LR
  rg["terraform-azurerm-resource-group"] -->|"resource_group_name"| hub
  wan["terraform-azurerm-virtual-wan"] -->|"virtual_wan_id"| hub
  hub["terraform-azurerm-virtual-hub<br/>(this module)"]
  hub -->|"hub id"| conn["terraform-azurerm-virtual-hub-connection<br/>(VNets and branches)"]
  hub -->|"hub id"| rt["terraform-azurerm-virtual-hub-route-table"]
  hub -->|"hub id"| ri["terraform-azurerm-virtual-hub-routing-intent"]
  hub -->|"hub id"| gw["terraform-azurerm-vpn-gateway<br/>express-route gateway"]
  hub -->|"attach for a Secured Virtual Hub"| fw["Azure Firewall (sibling module)"]
  classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
  classDef target fill:#004578,stroke:#001f3f,color:#ffffff;
  classDef sib fill:#eef3f8,stroke:#b0bec5,color:#1a1a1a;
  class hub me;
  class wan target;
  class rg,conn,rt,ri,gw,fw sib;
Loading

🧬 What this module builds

A single keystone azurerm_virtual_hub.this with inline route blocks, fed by typed inputs and emitting the hub's identity and its virtual-router facts.

flowchart LR
  subgraph inputs["Inputs"]
    i1["name / resource_group_name / location"]
    i2["virtual_wan_id / address_prefix"]
    i3["sku / hub_routing_preference / branch_to_branch_traffic_enabled"]
    i4["routes: address_prefixes + next_hop_ip_address"]
  end
  hub["azurerm_virtual_hub.this<br/>keystone + inline route blocks"]
  subgraph outputs["Outputs"]
    o1["id"]
    o2["name"]
    o3["default_route_table_id"]
    o4["virtual_router_asn / virtual_router_ips"]
  end
  i1 --> hub
  i2 --> hub
  i3 --> hub
  i4 --> hub
  hub --> o1
  hub --> o2
  hub --> o3
  hub --> o4
  classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
  classDef target fill:#004578,stroke:#001f3f,color:#ffffff;
  classDef sib fill:#eef3f8,stroke:#b0bec5,color:#1a1a1a;
  class hub target;
  class i1,i2,i3,i4,o1,o2,o3,o4 sib;
Loading

Resource inventory

Resource Cardinality Role
azurerm_virtual_hub.this 1 (keystone) The Virtual Hub
route { … } 0..N inline (from var.routes) Static routes carried on the hub

✅ Provider / Versions

Requirement Value
Terraform >= 1.12.0
Provider hashicorp/azurerm ~> 4.0
Provider block None in this module — the caller configures provider "azurerm" { features {} }, auth, and subscription

Schema notes that bite (verified against the live provider schema):

  • 🧨 name, resource_group_name, location, virtual_wan_id, address_prefix, and sku are each force-new — changing any one replaces the hub.
  • 📏 address_prefix cannot be smaller than /24; Azure recommends /23 or larger. The module validates the mask is /24 or larger.
  • 🧮 The provider's CIDR validator counts dots and digits and checks nothing else. Its regex accepts 999.999.999.999, and its /nn part is OPTIONAL — so a bare IP address passes an address-prefix field. This module checks the octet ranges and requires the prefix length; both refuse only values Azure would refuse anyway.
  • ⚖️ Inside routes the two fields are validated by different rules. address_prefixes entries go through that CIDR validator, so a bare IP is accepted; next_hop_ip_address goes through an ADDRESS validator, which refuses a /nn outright.
  • 🪧 Basic supports site-to-site VPN and ONLY that — it terminates branch tunnels, and excludes point-to-site (User VPN), ExpressRoute, VNet-to-VNet and inter-hub transit, Azure Firewall and NVAs, with no adjustable gateway scale units. Neither this module nor the provider defaults this argument — the hub's type is configured on the Virtual WAN object and every hub in a Basic WAN is Basic, so omitting sku gets you whatever the WAN is, not Standard.
  • ⚠️ The real upgrade path is on the WAN and is one-way. Upgrading a WAN upgrades every hub in it, and Standard cannot be reverted to Basic. Because sku is force-new here, changing it in Terraform replaces the hub and everything attached to it, which the WAN-level upgrade does not.
  • 🔁 hub_routing_preference, branch_to_branch_traffic_enabled, virtual_router_auto_scale_min_capacity, and route blocks are updatable in place.
  • 🧮 virtual_router_asn, virtual_router_ips, and default_route_table_id are computed — they are known only after apply.

🔑 Required Azure RBAC Roles / Permissions

  • Network Contributor on the target resource group, or a custom role granting Microsoft.Network/virtualHubs/* at the resource-group scope, plus read on the referenced Virtual WAN (Microsoft.Network/virtualWans/read). Least privilege, at the smallest scope that works.

Azure Prerequisites

  • An existing resource group in a supported US Azure region.
  • The Microsoft.Network resource provider registered on the target subscription.
  • A Virtual WAN whose id is supplied as virtual_wan_id (for the normal WAN-attached topology).
  • A planned, non-overlapping address_prefix (mask no smaller than /24; /23 or larger recommended).
  • The caller configures the provider "azurerm" { features {} } block, auth, and subscription; the module declares none of these.

📁 Module Structure

terraform-azurerm-virtual-hub/
├── providers.tf   # required_version >= 1.12.0; azurerm ~> 4.0; no provider block
├── variables.tf   # deeply-typed inputs; secure defaults; tags + timeouts tail
├── main.tf        # azurerm_virtual_hub.this keystone; dynamic route + timeouts
├── outputs.tf     # id first, then name, default_route_table_id, virtual_router_*
├── README.md      # this file
├── SCOPE.md       # the cross-module contract
├── LICENSE        # MIT
└── .gitignore     # canonical library ignore set

⚙️ Quick Start

The caller configures the provider (including the mandatory features {} block) and authentication; this module never does.

provider "azurerm" {
  features {}
}

module "virtual_hub" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-hub.git?ref=v1.0.0"

  name                = "vhub-eastus-prod"
  resource_group_name = "rg-network-prod"
  location            = "eastus"
  virtual_wan_id      = azurerm_virtual_wan.this.id
  address_prefix      = "10.100.0.0/23"

  tags = {
    environment = "prod"
    workload    = "connectivity"
  }
}

ℹ️ Pin ?ref=v1.0.0 (never a branch) so the source is immutable. This library is plan-only; a human applies from CI.

🔌 Cross-Module Contract

Consumes

Input Type From
resource_group_name string terraform-azurerm-resource-group output name
location string caller / resource group location
virtual_wan_id string terraform-azurerm-virtual-wan output id

Emits

Output Description Consumed by
id Virtual Hub Resource ID (first) hub connections, route tables, route-map, gateways, firewall, diagnostics
name Virtual Hub name diagnostics / tagging
default_route_table_id ID of the hub's default route table route-table and connection modules
virtual_router_asn ASN of the hub's virtual router (BGP) BGP peering configuration
virtual_router_ips IP addresses of the hub's virtual router (BGP) BGP peering configuration

📚 Example Library

1 · Minimal Standard hub with a /23 prefix
module "virtual_hub" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-hub.git?ref=v1.0.0"

  name                = "vhub-eastus"
  resource_group_name = "rg-network"
  location            = "eastus"
  virtual_wan_id      = module.virtual_wan.id
  address_prefix      = "10.100.0.0/23"
}

💡 The empty call already picks sku = "Standard" and leaves branch-to-branch transit disabled — this is the recommended baseline for a routed Virtual WAN hub.

2 · Basic SKU hub (documented trade-off)
module "virtual_hub" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-hub.git?ref=v1.0.0"

  name                = "vhub-basic"
  resource_group_name = "rg-network"
  location            = "eastus2"
  virtual_wan_id      = module.virtual_wan.id
  address_prefix      = "10.101.0.0/23"
  sku                 = "Basic"
}

⚠️ Basic supports site-to-site VPN and ONLY that — it terminates branch tunnels, and excludes point-to-site (User VPN), ExpressRoute, VNet-to-VNet and inter-hub transit, Azure Firewall and NVAs, with no adjustable gateway scale units. It does not support hub routing features (routing intent, the Secured Virtual Hub path). sku is force-new — you cannot upgrade a Basic hub to Standard in place.

3 · Routing preference — VpnGateway
module "virtual_hub" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-hub.git?ref=v1.0.0"

  name                   = "vhub-vpnpref"
  resource_group_name    = "rg-network"
  location               = "eastus"
  virtual_wan_id         = module.virtual_wan.id
  address_prefix         = "10.102.0.0/23"
  hub_routing_preference = "VpnGateway"
}

ℹ️ Prefer routes learned via the VPN gateway when the hub's virtual router has several paths to the same destination. Updatable in place.

4 · Routing preference — ASPath
module "virtual_hub" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-hub.git?ref=v1.0.0"

  name                   = "vhub-aspath"
  resource_group_name    = "rg-network"
  location               = "centralus"
  virtual_wan_id         = module.virtual_wan.id
  address_prefix         = "10.103.0.0/23"
  hub_routing_preference = "ASPath"
}

ℹ️ ASPath favors the route with the shortest BGP AS path. Legal values are ExpressRoute (default), VpnGateway, and ASPath.

5 · A single custom static route
module "virtual_hub" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-hub.git?ref=v1.0.0"

  name                = "vhub-routed"
  resource_group_name = "rg-network"
  location            = "eastus"
  virtual_wan_id      = module.virtual_wan.id
  address_prefix      = "10.104.0.0/23"

  routes = {
    to_onprem = {
      address_prefixes    = ["192.168.0.0/16"]
      next_hop_ip_address = "10.104.0.68"
    }
  }
}

💡 Routes are a keyed map — the key (to_onprem) is the stable for_each identity, so renaming or removing one route never disturbs the others.

6 · Multiple static routes at scale
module "virtual_hub" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-hub.git?ref=v1.0.0"

  name                = "vhub-multiroute"
  resource_group_name = "rg-network"
  location            = "eastus"
  virtual_wan_id      = module.virtual_wan.id
  address_prefix      = "10.105.0.0/23"

  routes = {
    to_datacenter_a = {
      address_prefixes    = ["172.16.0.0/16"]
      next_hop_ip_address = "10.105.0.68"
    }
    to_datacenter_b = {
      address_prefixes    = ["172.17.0.0/16"]
      next_hop_ip_address = "10.105.0.69"
    }
    to_partner = {
      address_prefixes    = ["203.0.113.0/24", "198.51.100.0/24"]
      next_hop_ip_address = "10.105.0.70"
    }
  }
}

ℹ️ Each map entry renders one inline route block. address_prefixes accepts a list, so one route can cover several destination CIDRs.

7 · Raising the virtual-router autoscale capacity
module "virtual_hub" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-hub.git?ref=v1.0.0"

  name                                   = "vhub-scaled"
  resource_group_name                    = "rg-network"
  location                               = "eastus"
  virtual_wan_id                         = module.virtual_wan.id
  address_prefix                         = "10.106.0.0/23"
  virtual_router_auto_scale_min_capacity = 5
}

💡 The minimum is 2 (the default). Raise it to pre-provision routing throughput for high-traffic hubs. Updatable in place.

8 · Enabling branch-to-branch transit
module "virtual_hub" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-hub.git?ref=v1.0.0"

  name                             = "vhub-transit"
  resource_group_name              = "rg-network"
  location                         = "eastus"
  virtual_wan_id                   = module.virtual_wan.id
  address_prefix                   = "10.107.0.0/23"
  branch_to_branch_traffic_enabled = true
}

🔒 Defaults to false, so branch-to-branch transit is off until you explicitly enable it. Turn it on only when your topology requires branch sites to route to one another through the hub.

9 · Tags and custom timeouts
module "virtual_hub" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-hub.git?ref=v1.0.0"

  name                = "vhub-tagged"
  resource_group_name = "rg-network"
  location            = "eastus"
  virtual_wan_id      = module.virtual_wan.id
  address_prefix      = "10.108.0.0/23"

  tags = {
    environment = "prod"
    cost_center = "networking"
    owner       = "platform-team"
  }

  timeouts = {
    create = "30m"
    delete = "30m"
  }
}

ℹ️ Hub create and delete operations can take many minutes; longer timeouts avoid spurious failures on slow control-plane operations.

10 · Secured Virtual Hub baseline (attach Azure Firewall — sibling)
# This module creates the Standard hub; the firewall is a sibling module wired to the hub id.
module "virtual_hub" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-hub.git?ref=v1.0.0"

  name                = "vhub-secured"
  resource_group_name = "rg-network"
  location            = "eastus"
  virtual_wan_id      = module.virtual_wan.id
  address_prefix      = "10.109.0.0/23"
  sku                 = "Standard" # required for the Secured Virtual Hub path
}

# module "hub_firewall" {
#   source          = "git::https://github.com/microsoftexpert/terraform-azurerm-firewall.git?ref=v1.0.0"
#   virtual_hub_id  = module.virtual_hub.id
#   ...
# }

🔒 A Secured Virtual Hub is a hub with an Azure Firewall attached. This module owns only the hub; the firewall (and the routing intent that steers traffic through it) are separate sibling modules composed against module.virtual_hub.id. The Standard SKU is required for this path.

11 · Standalone router topology (no Virtual WAN)
module "virtual_hub" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-hub.git?ref=v1.0.0"

  name                = "vhub-standalone"
  resource_group_name = "rg-network"
  location            = "eastus"
  address_prefix      = "10.110.0.0/23"
  # virtual_wan_id intentionally omitted
}

⚠️ Rare. A hub is normally created inside a Virtual WAN; omit virtual_wan_id only for a deliberate standalone-router topology. virtual_wan_id is force-new, so you cannot attach a WAN later without replacing the hub.

12 · Full-featured hub (all knobs set)
module "virtual_hub" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-hub.git?ref=v1.0.0"

  name                                   = "vhub-full"
  resource_group_name                    = "rg-network"
  location                               = "eastus"
  virtual_wan_id                         = module.virtual_wan.id
  address_prefix                         = "10.111.0.0/23"
  sku                                    = "Standard"
  hub_routing_preference                 = "ASPath"
  branch_to_branch_traffic_enabled       = true
  virtual_router_auto_scale_min_capacity = 4

  routes = {
    to_onprem = {
      address_prefixes    = ["192.168.0.0/16"]
      next_hop_ip_address = "10.111.0.68"
    }
  }

  tags = { environment = "prod" }
}

💡 Every non-identity field here is updatable in place except sku and address_prefix, which are force-new.

13 · Wiring the hub id into a sibling connection module
module "virtual_hub" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-hub.git?ref=v1.0.0"

  name                = "vhub-eastus"
  resource_group_name = "rg-network"
  location            = "eastus"
  virtual_wan_id      = module.virtual_wan.id
  address_prefix      = "10.112.0.0/23"
}

resource "azurerm_virtual_hub_connection" "hub_connection" {
  name                         = "conn-spoke-a"
  remote_virtual_network_id    = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-example/providers/Microsoft.Example/example/example"
  virtual_hub_id               = module.virtual_hub.id
}

ℹ️ Hub connections are owned by their own module and consume this hub's id. The same pattern applies to route tables, routing intent, and gateways.

14 · 🏗️ End-to-end composition (resource group + Virtual WAN + hub)
provider "azurerm" {
  features {}
}

module "resource_group" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"

  name     = "rg-network-prod"
  location = "eastus"
}

module "virtual_wan" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-wan.git?ref=v1.0.0"

  name                = "vwan-prod"
  resource_group_name = module.resource_group.name
  location            = module.resource_group.location
}

module "virtual_hub" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-hub.git?ref=v1.0.0"

  name                = "vhub-eastus-prod"
  resource_group_name = module.resource_group.name
  location            = module.resource_group.location
  virtual_wan_id      = module.virtual_wan.id
  address_prefix      = "10.100.0.0/23"

  hub_routing_preference = "ExpressRoute"

  tags = {
    environment = "prod"
    workload    = "connectivity"
  }
}

output "hub_id" {
  value = module.virtual_hub.id
}

output "hub_default_route_table_id" {
  value = module.virtual_hub.default_route_table_id
}

💡 The resource group's name/location and the Virtual WAN's id flow straight into the hub. Downstream connection, route-table, and gateway modules then consume module.virtual_hub.id and module.virtual_hub.default_route_table_id.

📥 Inputs

Identity (required, all force-new): name, resource_group_name, location.

WAN + address space (optional, force-new): virtual_wan_id, address_prefix.

SKU + routing (optional, secure defaults): sku ("Standard"), hub_routing_preference ("ExpressRoute"), branch_to_branch_traffic_enabled (false), virtual_router_auto_scale_min_capacity (2).

Static routes + tail: routes ({}), tags ({}), timeouts (null).

Full input schemas
Name Type Default Required Notes
name string — yes Force-new.
resource_group_name string — yes Force-new; RG not created here.
location string — yes Force-new; US regions targeted.
virtual_wan_id string null no Force-new; supply the Virtual WAN id.
address_prefix string null no Force-new; mask /24 or larger, /23 recommended.
sku string "Standard" no Standard | Basic; force-new.
hub_routing_preference string "ExpressRoute" no ExpressRoute | VpnGateway | ASPath.
branch_to_branch_traffic_enabled bool false no Cross-branch transit off by default.
virtual_router_auto_scale_min_capacity number 2 no Minimum 2.
routes map(object({ address_prefixes = list(string), next_hop_ip_address = string })) {} no Inline static routes, keyed.
tags map(string) {} no Applied to the hub.
timeouts object({ create, read, update, delete = optional(string) }) null no Per-operation timeouts.

🧾 Outputs

Output Description Kind
id The Azure Resource ID of the Virtual Hub Passthrough
name The name of the Virtual Hub Passthrough
location Azure region the resource is deployed in, in the canonical form Azure uses Passthrough
default_route_table_id The Resource ID of the hub's default route table Passthrough
virtual_router_asn The Autonomous System Number (ASN) of the hub's virtual router (BGP) Passthrough
virtual_router_ips The IP addresses of the hub's virtual router (BGP) Passthrough
almost_everything_here_is_force_new Always true, and it shapes how this resource should be planned Constant
basic_sku_supports_site_to_site_vpn_only True when the hub is Basic — site-to-site VPN only, no P2S / ExpressRoute / transit / Firewall / NVA. A capability gate, not a performance tier Derived
address_prefix_size_is_not_enforced_by_the_provider Always true Constant
routing_preference_only_matters_for_dual_learned_prefixes True when a routing preference is set explicitly Derived
auto_scale_minimum_is_a_billed_floor_not_a_ceiling Always true, and the name invites the wrong reading Constant

🧠 Architecture Notes

  • Force-new fields bite hard. name, resource_group_name, location, virtual_wan_id, address_prefix, and sku are each immutable. In a live Virtual WAN, replacing the hub disrupts every gateway and connection attached to it — plan the address space and SKU up front.
  • Address prefix sizing. The provider rejects a mask smaller than /24; Azure recommends /23 or larger so the hub's routers and future gateways have room. The module validates the mask at plan time to surface the mistake before an apply.
  • Standard vs Basic. Only Standard supports hub routing features and the Secured Virtual Hub path. Because sku is force-new, a Basic hub cannot be upgraded in place — this is why the module defaults to Standard.
  • for_each key stability. Inline routes are keyed by their map key, not by list position, so adding or removing one route never re-indexes the rest and never forces an unrelated route to churn.
  • Computed values. default_route_table_id, virtual_router_asn, and virtual_router_ips are known only after apply; downstream modules that consume them create an implicit dependency on the hub.
  • What this module does not own. Hub connections, hub route tables, routing intent, VPN / ExpressRoute gateways, and the Azure Firewall of a Secured Virtual Hub are separate sibling modules, each composed against this hub's id. This keeps the hub's lifecycle independent of the things attached to it.
  • features {} dependence. The provider will not initialize without a caller-side provider "azurerm" { features {} } block. That block belongs to the root module, never here.

🧱 Design Principles

The empty call produces the hardened, full-featured hub; each relaxation is an explicit opt-out the caller must type.

Concern Secure default (empty call) Opt-out (caller must type it)
SKU / feature surface sku = "Standard" (enables routing features + Secured Hub path) sku = "Basic"
Branch-to-branch transit branch_to_branch_traffic_enabled = false set to true
Address space validated /24-or-larger; /23 recommended supply a valid larger mask
Routing preference hub_routing_preference = "ExpressRoute" (Azure default) "VpnGateway" | "ASPath"
Secrets none accepted or emitted —

🚀 Runbook

# From the module folder (plan-only; a human applies from CI):
terraform init -backend=false
terraform validate
terraform fmt -check

Pin the module source to ?ref=v1.0.0 — never a branch. This library performs no cloud apply during authoring; terraform apply is run by a human from CI against real credentials.

🧪 Testing

The offline proof gate is the quality bar this module ships against:

  • terraform init -backend=false installs the pinned azurerm ~> 4.0 provider with no backend.
  • terraform validate proves the configuration is internally consistent and type-correct against the pinned provider schema — it catches every typing mistake the object() schemas are designed to surface, and exercises the sku / hub_routing_preference enum validations and the address_prefix mask validation.
  • terraform fmt -check enforces canonical formatting.

None of these calls Azure. Only terraform plan (run by a human, from CI, against real credentials) exercises the ARM API — for example the actual /24-floor enforcement and address-space overlap checks.

💬 Example Output

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

hub_id                     = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-network-prod/providers/Microsoft.Network/virtualHubs/vhub-eastus-prod"
hub_default_route_table_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-network-prod/providers/Microsoft.Network/virtualHubs/vhub-eastus-prod/hubRouteTables/defaultRouteTable"
name                       = "vhub-eastus-prod"
virtual_router_asn         = 65515
virtual_router_ips         = [
  "10.100.0.68",
  "10.100.0.69",
]

🔍 Troubleshooting

Symptom Cause Fix
Error: expected sku to be one of [...] at plan sku set to something other than Standard / Basic Use Standard (default) or Basic.
Plan wants to replace the hub after a small edit You changed a force-new field (sku, address_prefix, virtual_wan_id, name, location, resource_group_name) Revert the change, or accept the replacement in a maintenance window.
address_prefix must be a valid CIDR whose mask is /24 or larger Prefix omitted, malformed, or smaller than /24 Supply a valid CIDR with mask /24 or larger; /23 recommended, e.g. "10.0.0.0/23".
Hub routing features (routing intent, Secured Hub) unavailable Hub created with sku = "Basic" Recreate with sku = "Standard" (force-new — cannot upgrade in place).
Error: features / provider fails to initialize No caller-side provider "azurerm" { features {} } block Add the features {} block to the root module.
virtual_router_asn / virtual_router_ips show as (known after apply) These are computed attributes Expected — they resolve during apply.

🔗 Related Docs


💙 "Infrastructure as Code should be standardized, consistent, and secure."