Building a tiny Azure hub with VPN, firewall, and peered VNets
Greetings!
I’ve been tightening up my Azure homelab so that West US and West US 3 can both ride a single VPN gateway and still force traffic through an Azure Firewall. This is all in preps and hopes to get my AZ-700 exam completed next month.
This post walks through the exact Terraform I’m using (and a couple of defaults that keep me honest).
Lets dive in.
What we’re building
- Two VNets: West US (
10.51.0.0/16) with the VPN gateway, and West US 3 (10.50.0.0/16) with the firewall. - VNet peering with gateway transit enabled so West US 3 can use the West US VPN gateway to reach on‑prem.
- Azure Firewall Basic sitting in West US 3 with a simple “allow everything” outbound rule set.
- One Ubuntu VM in each VNet for quick tests.
Remote state (because losing state is pain)
I back state with an Azure Storage account. Drop these values in backend.hcl and keep the access key out of git:
resource_group_name = "rg-terraform-state"
storage_account_name = "tfhomelabstate01"
container_name = "tfstate"
key = "homelab/terraform.tfstate"
subscription_id = "00000000-1111-2222-3333-444444444444"
access_key = "azsa_access_key_goes_here"
Terraform init (from azure/terraform/):
terraform init -backend-config=backend.hcl
Helper script to create state storagecreate-state-storage.ps1 stands up the storage account and container. Defaults are placeholders—override as needed.
.\create-state-storage.ps1 `
-SubscriptionId "00000000-1111-2222-3333-444444444444" `
-ResourceGroupName "rg-terraform-state" `
-Location "westus3" `
-StorageAccountName "tfstatesa12345" `
-ContainerName "tfstate" `
-EnableSoftDelete
The script ensures you’re logged in, creates the RG/storage/container, and prints an access key you can drop into backend.hcl.
Variable file with consistent examples
Here’s the trimmed terraform.tfvars I’m using for the lab. Secrets are placeholders—swap your own.
subscription_id = "00000000-1111-2222-3333-444444444444"
network_resource_group_name = "rg-tf-networking-west"
resource_group_location = "westus3"
uswest3_vnet_name = "tf-uswest3-vnet1"
uswest3_vnet_address_space = "10.50.0.0/16"
uswest3_location = "westus3"
uswest3_default_subnet_prefix = "10.50.0.0/26"
uswest3_vm_subnet_prefix = "10.50.0.64/29"
uswest3_firewall_subnet_prefix = "10.50.0.128/26"
uswest3_firewall_mgmt_subnet_prefix = "10.50.0.192/26"
uswest3_firewall_name = "tf-uswest3-azfw1"
uswest3_firewall_policy_name = "tf-uswest3-azfw1-policy"
uswest3_firewall_public_ip_name = "tf-uswest3-azfw1-pip"
uswest3_firewall_mgmt_public_ip_name = "tf-uswest3-azfw1-mgmt-pip"
uswest3_route_table_name = "tf-uswest3-rt-azfw"
uswest1_vnet_name = "tf-uswest1-vnet1"
uswest1_vnet_address_space = "10.51.0.0/16"
uswest1_location = "westus"
uswest1_gateway_subnet_prefix = "10.51.0.224/27"
uswest1_vm_subnet_prefix = "10.51.0.0/29"
uswest1_vm_name = "tf-uswest1-vm1"
uswest1_vm_nic_name = "tf-uswest1-vm1-nic1"
uswest1_vm_size = "Standard_D2as_v5"
uswest1_vm_admin_username = "zbc-admin"
uswest1_vm_admin_password = "SuperSecretPassw0rd!"
vpn_gateway_name = "tf-uswest1-vpngw1"
vpn_gateway_public_ip_name = "tf-uswest1-vpngw1-pip"
vpn_gateway_sku = "VpnGw1"
local_network_gateway_name = "homelab-onprem"
local_network_gateway_public_ip = "203.0.113.24"
local_network_gateway_address_spaces = ["10.0.0.0/24"]
vpn_connection_name = "tf-uswest1-s2s-conn1"
vpn_connection_shared_key = "AnotherS3cretKey!"
uswest3_vm_name = "tf-uswest3-vm1"
uswest3_vm_nic_name = "tf-uswest3-vm1-nic1"
uswest3_vm_size = "Standard_D2as_v5"
uswest3_vm_admin_username = "zbc-admin"
uswest3_vm_admin_password = "SuperSecretPassw0rd!"
tags = {
Owner = "Example Owner"
Environment = "Dv"
Product = "AZ700"
ManagedBy = "Terraform"
}
Core Terraform highlights
VNet peering with gateway transit
# West US advertises its VPN gateway
resource "azurerm_virtual_network_peering" "uswest1_to_uswest3" {
name = "uswest1-to-uswest3"
resource_group_name = azurerm_resource_group.network.name
virtual_network_name = azurerm_virtual_network.uswest1.name
remote_virtual_network_id = azurerm_virtual_network.uswest3.id
allow_gateway_transit = true
allow_virtual_network_access = true
allow_forwarded_traffic = true
}
# West US 3 consumes that gateway
resource "azurerm_virtual_network_peering" "uswest3_to_uswest1" {
name = "uswest3-to-uswest1"
resource_group_name = azurerm_resource_group.network.name
virtual_network_name = azurerm_virtual_network.uswest3.name
remote_virtual_network_id = azurerm_virtual_network.uswest1.id
use_remote_gateways = true
allow_virtual_network_access = true
allow_forwarded_traffic = true
}
VPN gateway without BGP (static S2S)
resource "azurerm_virtual_network_gateway" "vpn" {
name = var.vpn_gateway_name
location = var.uswest1_location
resource_group_name = azurerm_resource_group.network.name
type = "Vpn"
vpn_type = "RouteBased"
sku = var.vpn_gateway_sku
active_active = false
# BGP is off; we rely on static prefixes from the local network gateway
ip_configuration {
name = "vnetGatewayConfig"
public_ip_address_id = azurerm_public_ip.vpn.id
private_ip_address_allocation = "Dynamic"
subnet_id = azurerm_subnet.uswest1_gateway.id
}
}
Routing on the firewall side
# Default 0/0 through Azure Firewall
resource "azurerm_route" "uswest3_default_to_firewall" {
name = "default-to-firewall"
route_table_name = azurerm_route_table.uswest3_default.name
address_prefix = "0.0.0.0/0"
next_hop_type = "VirtualAppliance"
next_hop_in_ip_address = azurerm_firewall.uswest3_firewall.ip_configuration[0].private_ip_address
}
# On-prem prefix via the remote VPN gateway (enabled by peering)
resource "azurerm_route" "uswest3_onprem_via_vpngw" {
name = "onprem-via-vpngw"
route_table_name = azurerm_route_table.uswest3_default.name
address_prefix = var.local_network_gateway_address_spaces[0]
next_hop_type = "VirtualNetworkGateway"
}
Firewall policy: wide open for now
resource "azurerm_firewall_policy_rule_collection_group" "uswest3_allow_outbound" {
name = "allow-outbound"
priority = 100
firewall_policy_id = azurerm_firewall_policy.uswest3.id
network_rule_collection {
name = "allow-all-vnet-out"
priority = 100
action = "Allow"
rule {
name = "All-out-uswest3"
source_addresses = [azurerm_virtual_network.uswest3.address_space[0]]
destination_addresses = ["*"]
destination_ports = ["*"]
protocols = ["Any"]
}
rule {
name = "All-out-uswest1"
source_addresses = [azurerm_virtual_network.uswest1.address_space[0]]
destination_addresses = ["*"]
destination_ports = ["*"]
protocols = ["Any"]
}
}
}
Deploying
cd azure/terraform
terraform plan -var-file=terraform.tfvars
terraform apply -var-file=terraform.tfvars
What to tighten next
- Replace the “allow all” firewall rules with scoped collections and logging to a workspace.
- If you want dynamic routing, turn BGP back on and add
bgp_settingsto both the VPN gateway and local network gateway. - Loop over
local_network_gateway_address_spacesto install all on-prem prefixes instead of just the first entry.
That’s it for now. The peering + remote gateway setup keeps the topology simple while I test. Next iteration will lock down egress and add better route handling. Thanks for reading!
Grab the code
If you want to clone the exact public version of this lab, it’s on my GitHub:
https://github.com/zveroboy152/zbc-azure-network-lab-example
Leave a Reply