Create an Azure Virtual WAN — the global network backbone for virtual hubs, VPN/ExpressRoute gateways, and branch connectivity — with secure, full-featured defaults, on
hashicorp/azurerm ~> 4.0.
- 🌐 Creates one
azurerm_virtual_wan— the top-level backbone that virtual hubs and their gateways attach to. - 🛡️ Secure by default: the full-featured Standard tier, VPN encryption enabled, and no Office 365 local breakout.
- 🧭 Exposes the WAN tier, VPN encryption toggle, branch-to-branch transit, and Office 365 local breakout — all typed and validated.
- 🔗 Emits
id,name,type, andlocationfor downstream hub and gateway wiring.
💡 Why it matters: the Virtual WAN is the root of a hub-and-spoke global network. Choosing the
Standardtier and keeping VPN encryption on at creation avoids a forced replacement later — the tier is immutable, and everything attached to the WAN would be replaced with it. The empty call gives you a production-grade backbone that hubs and gateways can build on immediately.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- ⭐ Star this repository to help others discover this Terraform module.
- 🤝 Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!
flowchart TD
RG["terraform-azurerm-resource-group"]
VWAN["terraform-azurerm-virtual-wan"]
AVWAN["azurerm_virtual_wan"]
HUB["terraform-azurerm-virtual-hub"]
VPNGW["terraform-azurerm-vpn-gateway"]
ERGW["terraform-azurerm-express-route-gateway"]
P2S["terraform-azurerm-point-to-site-vpn-gateway"]
CONN["VPN sites / ExpressRoute / P2S connections"]
RG -->|"resource_group_name + location"| VWAN
VWAN --> AVWAN
VWAN -->|"virtual_wan_id"| HUB
HUB -->|"virtual_hub_id"| VPNGW
HUB -->|"virtual_hub_id"| ERGW
HUB -->|"virtual_hub_id"| P2S
VPNGW -->|"branch / site-to-site"| CONN
ERGW -->|"private circuit"| CONN
P2S -->|"remote users"| CONN
classDef this fill:#0078D4,color:#ffffff,stroke:#004578,stroke-width:2px;
classDef key fill:#004578,color:#ffffff,stroke:#004578;
class VWAN this;
class AVWAN key;
flowchart LR
I1["name / location / resource_group_name"]
I2["type = Standard (default)"]
I3["disable_vpn_encryption = false (default)"]
I4["allow_branch_to_branch_traffic = true (default)"]
I5["office365_local_breakout_category = None (default)"]
VWAN["azurerm_virtual_wan.this"]
O1["id"]
O2["name"]
O3["type"]
O4["location"]
I1 --> VWAN
I2 --> VWAN
I3 --> VWAN
I4 --> VWAN
I5 --> VWAN
VWAN --> O1
VWAN --> O2
VWAN --> O3
VWAN --> O4
classDef this fill:#0078D4,color:#ffffff,stroke:#004578,stroke-width:2px;
class VWAN this;
Resource inventory
| Resource | Cardinality | Role |
|---|---|---|
azurerm_virtual_wan.this |
1 (keystone) | The Virtual WAN backbone. |
| 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
name,resource_group_name, andlocationare immutable — changing any of them forces replacement of the Virtual WAN.typeis NOT force-new, and the trap is the other way round. A tier change is an ordinary in-place update, so both directions plan identically — but Microsoft supports onlyBasic→Standard. AStandard→Basicedit produces a clean one-line plan that Azure refuses at apply. The module defaults toStandard.Basicis feature-limited. ABasicWAN supports site-to-site VPN only — no hub-to-hub transit, no ExpressRoute gateways in its hubs, and no custom routing.Standardunlocks the full secured-hub feature set.allow_branch_to_branch_trafficdefaults totrue. This is the normal WAN behavior and permits lateral traffic between connected branches; set it tofalseto isolate branches.- The Virtual WAN itself has no public data plane to disable — exposure is governed by the gateways and hubs attached to it, each of which is a separate module.
- Network Contributor on the target resource group (or a custom role with
Microsoft.Network/virtualWans/*), scoped as narrowly as the deployment allows.
- The
Microsoft.Networkresource provider registered on the subscription. - An existing resource group in a supported US Azure region.
- The caller configures the
provider "azurerm" { features {} }block, auth, and subscription; the module declares none of these.
terraform-azurerm-virtual-wan/
├── providers.tf # required_version + azurerm ~> 4.0; no provider block
├── variables.tf # name, rg, location, type, encryption, branch transit, O365 breakout, tags, timeouts
├── main.tf # azurerm_virtual_wan.this
├── outputs.tf # id, name, type, location
├── README.md # this document
├── SCOPE.md # cross-module contract
├── LICENSE # MIT
└── .gitignore # canonical Terraform ignore set
provider "azurerm" {
features {}
}
module "vwan" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-wan.git?ref=v1.0.0"
name = "vwan-hub-prod-eastus2"
resource_group_name = "rg-network-prod-eastus2"
location = "eastus2"
}ℹ️ The empty call is a Standard WAN with VPN encryption enabled and no Office 365 local breakout. Pin the module by tag (
?ref=v1.0.0), never a branch. The caller configures the provider, auth, andfeatures {}.
Consumes
| Input | Type | From |
|---|---|---|
resource_group_name |
string |
terraform-azurerm-resource-group (name) |
location |
string |
caller / resource group (location) |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
Virtual WAN Resource ID | virtual hubs (virtual_wan_id), vpn-site, downstream modules |
name |
Virtual WAN name | diagnostics, tagging |
type |
WAN tier in effect (Basic / Standard) |
topology validation |
location |
Region the WAN was created in | region-aware wiring |
1 · Minimal (Standard, encryption on)
module "vwan" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-wan.git?ref=v1.0.0"
name = "vwan-min-eastus2"
resource_group_name = "rg-network-eastus2"
location = "eastus2"
}🔒 Standard tier, VPN encryption enabled, no Office 365 local breakout — the secure baseline out of the box.
2 · Basic tier (feature-limited)
module "vwan" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-wan.git?ref=v1.0.0"
name = "vwan-basic-eastus2"
resource_group_name = "rg-network-eastus2"
location = "eastus2"
type = "Basic"
}
⚠️ ABasicWAN supports site-to-site VPN only — no hub-to-hub transit, no ExpressRoute gateways, no custom routing. Upgrading toStandardlater is a supported in-place change that converts every hub in the WAN; it is the reverse that cannot be done.
3 · Branch-to-branch traffic disabled (branch isolation)
module "vwan" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-wan.git?ref=v1.0.0"
name = "vwan-isolated-eastus2"
resource_group_name = "rg-network-eastus2"
location = "eastus2"
allow_branch_to_branch_traffic = false
}🔒 Set
allow_branch_to_branch_traffic = falseto stop connected branches from reaching one another through the hub — a tighter posture for segmented topologies.
4 · Office 365 local breakout — Optimize
module "vwan" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-wan.git?ref=v1.0.0"
name = "vwan-o365-opt-eastus2"
resource_group_name = "rg-network-eastus2"
location = "eastus2"
office365_local_breakout_category = "Optimize"
}ℹ️ Breaks out only the Office 365 Optimize category locally, keeping latency-sensitive Microsoft 365 traffic off the backbone path.
5 · Office 365 local breakout — OptimizeAndAllow
module "vwan" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-wan.git?ref=v1.0.0"
name = "vwan-o365-oa-eastus2"
resource_group_name = "rg-network-eastus2"
location = "eastus2"
office365_local_breakout_category = "OptimizeAndAllow"
}6 · Office 365 local breakout — All
module "vwan" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-wan.git?ref=v1.0.0"
name = "vwan-o365-all-eastus2"
resource_group_name = "rg-network-eastus2"
location = "eastus2"
office365_local_breakout_category = "All"
}
⚠️ Allbreaks out every Office 365 category locally — confirm this matches your egress and inspection policy before enabling.
7 · VPN encryption disabled (explicit opt-out)
module "vwan" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-wan.git?ref=v1.0.0"
name = "vwan-noenc-eastus2"
resource_group_name = "rg-network-eastus2"
location = "eastus2"
disable_vpn_encryption = true
}
⚠️ disable_vpn_encryption = trueturns off VPN encryption across the WAN. The module defaults this tofalse(encryption on); set it only for a deliberate, reviewed exception.
8 · With tags
module "vwan" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-wan.git?ref=v1.0.0"
name = "vwan-tagged-eastus2"
resource_group_name = "rg-network-eastus2"
location = "eastus2"
tags = {
environment = "prod"
owner = "network-platform"
costCenter = "1234"
}
}9 · Custom timeouts
module "vwan" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-wan.git?ref=v1.0.0"
name = "vwan-timeouts-eastus2"
resource_group_name = "rg-network-eastus2"
location = "eastus2"
timeouts = {
create = "30m"
delete = "30m"
}
}10 · Standard WAN ready for a secured hub
module "vwan" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-wan.git?ref=v1.0.0"
name = "vwan-secured-eastus2"
resource_group_name = "rg-network-eastus2"
location = "eastus2"
type = "Standard"
disable_vpn_encryption = false
allow_branch_to_branch_traffic = true
}💡 A
StandardWAN is the prerequisite for virtual hubs that host ExpressRoute gateways, hub-to-hub transit, and custom routing. Create the WAN here, then attach aterraform-azurerm-virtual-hubbymodule.vwan.id.
11 · for_each — a WAN per region
module "vwan" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-wan.git?ref=v1.0.0"
for_each = toset(["eastus2", "westus2", "centralus"])
name = "vwan-${each.key}"
resource_group_name = "rg-network-${each.key}"
location = each.key
}ℹ️ A stable
for_eachkey (the region) means adding or removing a region never re-indexes the others.
12 · Consuming a resource group's outputs
module "rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-network-prod-eastus2"
location = "eastus2"
}
module "vwan" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-wan.git?ref=v1.0.0"
name = "vwan-prod-eastus2"
resource_group_name = module.rg.name
location = module.rg.location
}💡 Wire
resource_group_nameandlocationfrom the resource-group module so the WAN follows the group's placement.
13 · 🏗️ End-to-end composition
A resource group, a Standard Virtual WAN, and a virtual hub (owned by its sibling module) attached to the WAN by id.
provider "azurerm" {
features {}
}
module "rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-network-prod-eastus2"
location = "eastus2"
}
module "vwan" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-wan.git?ref=v1.0.0"
name = "vwan-prod-eastus2"
resource_group_name = module.rg.name
location = module.rg.location
type = "Standard"
}
# Hubs are a SEPARATE catalog module — this module stays a clean single-resource backbone.
module "hub" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-hub.git?ref=v1.0.0"
name = "vhub-prod-eastus2"
resource_group_name = module.rg.name
location = module.rg.location
virtual_wan_id = module.vwan.id
address_prefix = "10.100.0.0/23"
}💡 The WAN flows into the virtual hub by
module.vwan.id. VPN/ExpressRoute gateways and connections then attach to the hub — each its own sibling module — so no single module owns the whole backbone.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
name |
string |
✅ | — | Virtual WAN name. Immutable. |
resource_group_name |
string |
✅ | — | Containing resource group. Immutable. |
location |
string |
✅ | — | Azure region. Immutable. |
type |
string |
— | "Standard" |
Standard / Basic. Updatable in place; the downgrade to Basic is refused by Azure. |
disable_vpn_encryption |
bool |
— | false |
Disable VPN encryption across the WAN. |
allow_branch_to_branch_traffic |
bool |
— | true |
Allow branch-to-branch transit through the hub. |
office365_local_breakout_category |
string |
— | "None" |
None / Optimize / OptimizeAndAllow / All. |
tags |
map(string) |
— | {} |
Resource tags. |
timeouts |
object |
— | null |
Optional create/read/update/delete timeouts. |
Full variable schemas
variable "type" { type = string, default = "Standard" } # Basic | Standard; in-place, upgrade-only
variable "disable_vpn_encryption" { type = bool, default = false } # encryption ON by default
variable "allow_branch_to_branch_traffic" { type = bool, default = true } # branch transit on (WAN default)
variable "office365_local_breakout_category" { type = string, default = "None" } # None | Optimize | OptimizeAndAllow | All (validated)
variable "timeouts" {
type = object({
create = optional(string)
read = optional(string)
update = optional(string)
delete = optional(string)
})
default = null
}| Output | Description | Notes |
|---|---|---|
id |
Virtual WAN Resource ID | Emitted first; consumed as virtual_wan_id by hubs. |
name |
Virtual WAN name | — |
type |
WAN tier in effect | Basic or Standard. |
location |
Region the WAN was created in | — |
- Single-resource ownership. This module owns exactly one
azurerm_virtual_wan.this. Virtual hubs, VPN and ExpressRoute gateways, VPN sites, and connections are separate catalog modules that consume this WAN'sid— keeping the backbone a clean, independently versioned boundary. - Immutable identity, but a MUTABLE tier with a one-way door.
name,resource_group_nameandlocationare force-new.typeis not: the provider declares noForceNewon it and sends it on update, so a tier change plans as an ordinary in-place edit.Basic→Standardworks and converts every hub in the WAN.Standard→Basicproduces an identical-looking plan and is refused by Azure at apply — the plan gives no warning, which is why the module defaults toStandard. - Encryption on by default.
disable_vpn_encryptiondefaults tofalse; VPN traffic across the WAN is encrypted unless a caller explicitly opts out. - Branch transit is a security surface.
allow_branch_to_branch_trafficdefaults totrue(standard WAN behavior). Where branches must not reach one another, set it tofalse. - No secrets. The module accepts and emits identifiers and configuration only.
features {}dependence. Noprovider {}block here; the caller configuresprovider "azurerm" { features {} }.
| Concern | Secure/full-featured default | Opt-out |
|---|---|---|
| WAN tier | Standard (full secured-hub feature set) |
Basic (VPN-only, limited) |
| VPN encryption | disable_vpn_encryption = false (encryption on) |
true |
| Office 365 local breakout | None (no local breakout) |
Optimize / OptimizeAndAllow / All |
| Branch-to-branch transit | true (standard WAN behavior) |
false (branch isolation) |
cd terraform-azurerm-virtual-wan
terraform init -backend=false
terraform validate
terraform fmt -check
Remove-Item -Recurse -Force .terraform -ErrorAction SilentlyContinuePin the module by tag (
?ref=v1.0.0), never a branch. Plan-only during authoring; a human runsplan/applyfrom CI.
The offline proof gate — terraform init -backend=false, terraform validate, terraform fmt -check — proves the configuration is type-correct against the pinned azurerm ~> 4.0 schema (including the type and office365_local_breakout_category validations) and canonically formatted, with no cloud calls. What it does not exercise: resource-provider registration, region availability, and the downstream compatibility of a Basic tier with hubs that need Standard features — those surface only under terraform plan/apply against real credentials from CI.
$ terraform output
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-network-prod-eastus2/providers/Microsoft.Network/virtualWans/vwan-prod-eastus2"
location = "eastus2"
name = "vwan-prod-eastus2"
type = "Standard"| Symptom | Cause | Fix |
|---|---|---|
Standard → Basic rejected at apply |
The downgrade is not supported by Azure, and type is not force-new so the plan looks ordinary |
Keep the WAN at Standard. There is no in-place path back, and no replacement path either without destroying every hub attached. |
| Hub cannot host an ExpressRoute gateway | WAN is Basic |
Set type = "Standard". This is a supported in-place upgrade that converts every hub in the WAN — recreating the WAN is unnecessary and destroys everything attached to it. |
| Branches unexpectedly reach one another | allow_branch_to_branch_traffic defaults to true |
Set it to false to isolate branches. |
| VPN traffic not encrypted | disable_vpn_encryption = true was set |
Remove it (default false) to keep encryption on. |
AuthorizationFailed on apply |
Identity lacks Network Contributor | Grant Network Contributor on the resource group. |
| Create fails: resource provider not registered | Microsoft.Network not registered |
Register the provider on the subscription. |
- Provider resource:
azurerm_virtual_wan - Sibling modules:
terraform-azurerm-virtual-hub,terraform-azurerm-vpn-gateway,terraform-azurerm-express-route-gateway,terraform-azurerm-point-to-site-vpn-gateway,terraform-azurerm-resource-group - This module's cross-module contract:
SCOPE.md
💙 "Infrastructure as Code should be standardized, consistent, and secure."