Skip to content

Architecture

All four scenarios share the same hub-spoke topology and naming. The diagrams below show what changes between them.

Address plan

NetworkCIDRSubnets
Hub VNet10.0.0.0/23AzureFirewallSubnet (/26), GatewaySubnet (/26), default (/26), AzureFirewallManagementSubnet (/26, firewall scenarios only)
Spoke VNet10.0.2.0/23snet-workload (/26)

Scenarios at a glance

Capabilitybaselinefirewallvpnfull
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