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-featuredStandardSKU and leaves branch-to-branch transit off until you ask for it.
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 (
ExpressRouteby 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
StandardSKU — 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, itsdefault_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.
If this module saves you time, please consider supporting the work:
- ⭐ Star the repository on GitHub.
- 🤝 Connect on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
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;
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;
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 |
| 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, andskuare each force-new — changing any one replaces the hub. - 📏
address_prefixcannot be smaller than/24; Azure recommends/23or larger. The module validates the mask is/24or larger. - 🧮 The provider's CIDR validator counts dots and digits and checks nothing else. Its regex accepts
999.999.999.999, and its/nnpart 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
routesthe two fields are validated by different rules.address_prefixesentries go through that CIDR validator, so a bare IP is accepted;next_hop_ip_addressgoes through an ADDRESS validator, which refuses a/nnoutright. - 🪧
Basicsupports 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 omittingskugets you whatever the WAN is, notStandard. ⚠️ 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. Becauseskuis 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, androuteblocks are updatable in place. - 🧮
virtual_router_asn,virtual_router_ips, anddefault_route_table_idare computed — they are known only after apply.
Network Contributoron the target resource group, or a custom role grantingMicrosoft.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.
- An existing resource group in a supported US Azure region.
- The
Microsoft.Networkresource provider registered on the target subscription. - A Virtual WAN whose
idis supplied asvirtual_wan_id(for the normal WAN-attached topology). - A planned, non-overlapping
address_prefix(mask no smaller than/24;/23or larger recommended). - The caller configures the
provider "azurerm" { features {} }block, auth, and subscription; the module declares none of these.
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
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.
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 |
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"
}
⚠️ Basicsupports 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).skuis force-new — you cannot upgrade aBasichub toStandardin 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"
}ℹ️
ASPathfavors the route with the shortest BGP AS path. Legal values areExpressRoute(default),VpnGateway, andASPath.
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 stablefor_eachidentity, 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
routeblock.address_prefixesaccepts 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. TheStandardSKU 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; omitvirtual_wan_idonly for a deliberate standalone-router topology.virtual_wan_idis 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
skuandaddress_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/locationand the Virtual WAN'sidflow straight into the hub. Downstream connection, route-table, and gateway modules then consumemodule.virtual_hub.idandmodule.virtual_hub.default_route_table_id.
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. |
| 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 |
- Force-new fields bite hard.
name,resource_group_name,location,virtual_wan_id,address_prefix, andskuare 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/23or 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. StandardvsBasic. OnlyStandardsupports hub routing features and the Secured Virtual Hub path. Becauseskuis force-new, aBasichub cannot be upgraded in place — this is why the module defaults toStandard.for_eachkey 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, andvirtual_router_ipsare 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-sideprovider "azurerm" { features {} }block. That block belongs to the root module, never here.
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 | — |
# From the module folder (plan-only; a human applies from CI):
terraform init -backend=false
terraform validate
terraform fmt -checkPin 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.
The offline proof gate is the quality bar this module ships against:
terraform init -backend=falseinstalls the pinnedazurerm ~> 4.0provider with no backend.terraform validateproves the configuration is internally consistent and type-correct against the pinned provider schema — it catches every typing mistake theobject()schemas are designed to surface, and exercises thesku/hub_routing_preferenceenum validations and theaddress_prefixmask validation.terraform fmt -checkenforces 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.
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",
]
| 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. |
azurerm_virtual_hubresourceazurerm_virtual_wanresource- Azure Virtual WAN documentation
- What is a Secured Virtual Hub?
- Sibling modules:
terraform-azurerm-virtual-wan,terraform-azurerm-virtual-hub-connection,terraform-azurerm-vpn-gateway,terraform-azurerm-resource-group. - This module's
SCOPE.mdfor the full cross-module contract.
💙 "Infrastructure as Code should be standardized, consistent, and secure."