Architecture
All four scenarios share the same hub-spoke topology and naming. The diagrams below show what changes between them.
Address plan
| Network | CIDR | Subnets |
|---|---|---|
| Hub VNet | 10.0.0.0/23 | AzureFirewallSubnet (/26), GatewaySubnet (/26), default (/26), AzureFirewallManagementSubnet (/26, firewall scenarios only) |
| Spoke VNet | 10.0.2.0/23 | snet-workload (/26) |
Scenarios at a glance
| Capability | baseline | firewall | vpn | full |
|---|---|---|---|---|
| Hub + spoke VNets + peering | β | β | β | β |
| Spoke egress via NAT Gateway | β | β | β | β |
| Spoke egress via Azure Firewall | β | β | β | β |
| Route Table 0/0 β firewall private IP | β | β | β | β |
| VPN Gateway (S2S to on-prem) | β | β | β | β |
| Gateway transit on peering | β | β | β | β |
| Key Vault + Private Endpoint | β | β | β | β |
| Log Analytics + Automation + RSV | β | β | β | β |
Baseline
Spoke egresses to internet through its own NAT Gateway. Hub is mostly an empty VNet β present so future scenarios can promote in place without re-IPβing.
flowchart LR
subgraph Hub["Hub VNet 10.0.0.0/23"]
H_default["default subnet"]
end
subgraph Spoke["Spoke VNet 10.0.2.0/23"]
S_workload["snet-workload"]
KV["Key Vault PE"]
NAT["NAT Gateway"]
KV --- S_workload
S_workload --- NAT
end
Hub <-- "VNet peering" --> Spoke
NAT --> Internet(("Internet"))
LA["Log Analytics"]:::shared
RSV["Recovery Vault"]:::shared
classDef shared fill:#eef,stroke:#88a
Firewall
Spoke egress is forced through Azure Firewall Basic in the hub. NAT Gateway is not deployed in the spoke β the firewallβs public IPs are the egress identity.
flowchart LR
subgraph Hub["Hub VNet"]
AF["Azure Firewall Basic<br/>(private IP 10.0.0.4)"]
MGT["AzureFirewallManagementSubnet"]
AF --- MGT
end
subgraph Spoke["Spoke VNet"]
S_workload["snet-workload"]
KV["Key Vault PE"]
RT["Route Table<br/>0.0.0.0/0 β 10.0.0.4"]
KV --- S_workload
S_workload --- RT
end
Hub <-- "VNet peering" --> Spoke
RT -- "0/0" --> AF
AF --> Internet(("Internet"))
VPN
Spoke egresses to internet via NAT (same as baseline). On-prem reaches the spoke through a VPN Gateway in the hub. Peering uses gateway transit.
flowchart LR
OnPrem(("On-prem network"))
subgraph Hub["Hub VNet"]
VPN["VPN Gateway VpnGw2AZ"]
end
subgraph Spoke["Spoke VNet"]
S_workload["snet-workload"]
KV["Key Vault PE"]
NAT["NAT Gateway"]
KV --- S_workload
S_workload --- NAT
end
OnPrem <-- "IPsec S2S" --> VPN
Hub <-- "peering<br/>(gateway transit)" --> Spoke
NAT --> Internet(("Internet"))
Full
Both firewall and VPN. Spoke egress goes through the firewall; on-prem reaches the spoke through the VPN gateway via gateway transit.
flowchart LR
OnPrem(("On-prem"))
subgraph Hub["Hub VNet"]
AF["Azure Firewall Basic<br/>(10.0.0.4)"]
VPN["VPN Gateway VpnGw2AZ"]
end
subgraph Spoke["Spoke VNet"]
S_workload["snet-workload"]
KV["Key Vault PE"]
RT["Route Table<br/>0.0.0.0/0 β 10.0.0.4"]
KV --- S_workload
S_workload --- RT
end
OnPrem <-- "IPsec S2S" --> VPN
Hub <-- "peering<br/>(gateway transit)" --> Spoke
RT --> AF --> Internet(("Internet"))
Module composition
infra/terraform/foundation/βββ terraform.tf # required_providers + Azure backendβββ providers.tf # azurerm + azapi configurationβββ variables.tf # subscription_id, scenario, location, prefix, on-prem CIDRsβββ locals.tf # scenario flags (use_firewall, use_vpn, use_nat, use_peering)βββ resource_groups.tf # 6 RGs via for_eachβββ modules.networking.tf # VNets (AVM), NAT, peering, Private DNSβββ modules.security.tf # Key Vault (AVM) + PEβββ modules.monitoring.tf # Log Analytics + Automation + RSV (AVM)βββ modules.firewall.tf # count = use_firewall β Firewall, mgmt subnet, route tableβββ modules.vpn.tf # count = use_vpn β VPN GW + PIPβββ outputs.tfβββ scenarios/*.tfvars # one file per scenarioβββ tests/*.tftest.hcl # plan-mode assertions per scenario