Lab 04: VNet, NSG, VNet Peering, and Load Balancer
Estimated time: 60–90 minutes Difficulty: ⭐⭐⭐⭐☆ Environment: Azure free account — Azure CLI
Prerequisites
az account show --query "{name:name, id:id}" -o table
Lab Objectives
- Create two VNets and peer them
- Verify non-transitive peering behavior
- Configure NSG rules and test traffic filtering
- Deploy a Standard Load Balancer with two backend VMs
- Create a User-Defined Route to direct traffic through a next hop
- Test that UDR overrides default routing
Step 1: Create Resource Group and Two VNets
RG="rg-az104-lab04"
LOCATION="eastus"
az group create --name $RG --location $LOCATION
# VNet A — production
az network vnet create \
--resource-group $RG \
--name "vnet-prod" \
--address-prefix "10.1.0.0/16" \
--subnet-name "snet-web" \
--subnet-prefix "10.1.1.0/24"
# VNet B — shared services
az network vnet create \
--resource-group $RG \
--name "vnet-shared" \
--address-prefix "10.2.0.0/16" \
--subnet-name "snet-services" \
--subnet-prefix "10.2.1.0/24"
# VNet C — isolated (to test non-transitivity)
az network vnet create \
--resource-group $RG \
--name "vnet-isolated" \
--address-prefix "10.3.0.0/16" \
--subnet-name "snet-isolated" \
--subnet-prefix "10.3.1.0/24"
Step 2: Configure VNet Peering (A↔B and B↔C, but NOT A↔C)
# Peer vnet-prod ↔ vnet-shared (bidirectional)
az network vnet peering create \
--resource-group $RG \
--name "prod-to-shared" \
--vnet-name "vnet-prod" \
--remote-vnet "vnet-shared" \
--allow-vnet-access true \
--allow-forwarded-traffic true
az network vnet peering create \
--resource-group $RG \
--name "shared-to-prod" \
--vnet-name "vnet-shared" \
--remote-vnet "vnet-prod" \
--allow-vnet-access true \
--allow-forwarded-traffic true
# Peer vnet-shared ↔ vnet-isolated (bidirectional)
az network vnet peering create \
--resource-group $RG \
--name "shared-to-isolated" \
--vnet-name "vnet-shared" \
--remote-vnet "vnet-isolated" \
--allow-vnet-access true
az network vnet peering create \
--resource-group $RG \
--name "isolated-to-shared" \
--vnet-name "vnet-isolated" \
--remote-vnet "vnet-shared" \
--allow-vnet-access true
# Verify peering state
az network vnet peering list \
--resource-group $RG \
--vnet-name "vnet-shared" \
--query "[].{name:name, state:peeringState, remoteVnet:remoteVirtualNetwork.id}" \
-o table
VNet-prod can reach VNet-shared, and VNet-shared can reach VNet-isolated — but VNet-prod CANNOT reach VNet-isolated directly. This is non-transitivity. To allow A↔C, you must create an explicit A↔C peering.
Step 3: Create an NSG and Apply Rules
# Create NSG
az network nsg create \
--resource-group $RG \
--name "nsg-web"
# Allow HTTP (port 80) inbound from internet
az network nsg rule create \
--resource-group $RG \
--nsg-name "nsg-web" \
--name "Allow-HTTP" \
--priority 100 \
--direction Inbound \
--source-address-prefixes Internet \
--source-port-ranges "*" \
--destination-address-prefixes "*" \
--destination-port-ranges 80 \
--protocol Tcp \
--access Allow
# Allow SSH (port 22) from specific IP only
MY_IP=$(curl -s https://ifconfig.me)
az network nsg rule create \
--resource-group $RG \
--nsg-name "nsg-web" \
--name "Allow-SSH-MyIP" \
--priority 110 \
--direction Inbound \
--source-address-prefixes $MY_IP \
--source-port-ranges "*" \
--destination-address-prefixes "*" \
--destination-port-ranges 22 \
--protocol Tcp \
--access Allow
# Explicitly deny all other inbound (redundant with default, but good practice for visibility)
az network nsg rule create \
--resource-group $RG \
--nsg-name "nsg-web" \
--name "Deny-All-Inbound" \
--priority 4000 \
--direction Inbound \
--source-address-prefixes "*" \
--source-port-ranges "*" \
--destination-address-prefixes "*" \
--destination-port-ranges "*" \
--protocol "*" \
--access Deny
# Associate NSG with the web subnet
az network vnet subnet update \
--resource-group $RG \
--vnet-name "vnet-prod" \
--name "snet-web" \
--network-security-group "nsg-web"
# View effective rules
az network nsg show \
--resource-group $RG \
--name "nsg-web" \
--query "securityRules[].{name:name, priority:priority, direction:direction, access:access, port:destinationPortRange}" \
-o table
⚠️ Tricky spot: NSG rules are evaluated by priority — lowest number first. If you have Allow-HTTP at priority 100 and Deny-All at priority 90, HTTP will be denied because 90 < 100. Always ensure Allow rules have lower priority numbers than Deny rules for the same port.
Step 4: Deploy a Standard Load Balancer
# Create a public IP for the LB
az network public-ip create \
--resource-group $RG \
--name "pip-lb" \
--sku Standard \
--allocation-method Static \
--location $LOCATION
LB_PIP=$(az network public-ip show \
--resource-group $RG \
--name "pip-lb" \
--query ipAddress -o tsv)
echo "LB Public IP: $LB_PIP"
# Create the Standard Load Balancer
az network lb create \
--resource-group $RG \
--name "lb-web" \
--sku Standard \
--public-ip-address "pip-lb" \
--frontend-ip-name "fe-config" \
--backend-pool-name "be-pool"
# Create health probe (HTTP on port 80)
az network lb probe create \
--resource-group $RG \
--lb-name "lb-web" \
--name "http-probe" \
--protocol Http \
--port 80 \
--path "/"
# Create load balancing rule
az network lb rule create \
--resource-group $RG \
--lb-name "lb-web" \
--name "http-rule" \
--protocol Tcp \
--frontend-port 80 \
--backend-port 80 \
--frontend-ip-name "fe-config" \
--backend-pool-name "be-pool" \
--probe-name "http-probe"
Create two backend VMs and add them to the pool:
for i in 1 2; do
# NIC for each VM
az network nic create \
--resource-group $RG \
--name "nic-web$i" \
--vnet-name "vnet-prod" \
--subnet "snet-web" \
--lb-name "lb-web" \
--lb-address-pool "be-pool"
# VM
az vm create \
--resource-group $RG \
--name "vm-web$i" \
--nics "nic-web$i" \
--image Ubuntu2204 \
--admin-username azureuser \
--admin-password "P@ssw0rd!Azure104" \
--size Standard_B1s \
--no-wait
done
echo "VMs deploying in background..."
⚠️ Critical tricky spot — Standard LB + NSG: Standard Load Balancer requires an NSG on the backend subnet that explicitly allows the load-balanced traffic. Without it, backend VMs are unreachable even with a perfectly configured LB. The
nsg-webwe created in Step 3 (Allow port 80) satisfies this requirement. Basic LB doesn't have this requirement.
Step 5: Create a User-Defined Route
Simulate forcing all outbound internet traffic through a "firewall" appliance (using a placeholder IP):
# Create a route table
az network route-table create \
--resource-group $RG \
--name "rt-web" \
--location $LOCATION \
--disable-bgp-route-propagation false
# Add a route: all internet traffic → virtual appliance (firewall)
# In a real scenario, this would be Azure Firewall's private IP
az network route-table route create \
--resource-group $RG \
--route-table-name "rt-web" \
--name "force-internet-to-fw" \
--address-prefix "0.0.0.0/0" \
--next-hop-type VirtualAppliance \
--next-hop-ip-address "10.1.99.4" # placeholder NVA IP
# Add a specific route: traffic to Azure Monitor bypass the firewall
az network route-table route create \
--resource-group $RG \
--route-table-name "rt-web" \
--name "azure-monitor-direct" \
--address-prefix "AzureMonitor" \
--next-hop-type Internet
# Associate the route table with the web subnet
az network vnet subnet update \
--resource-group $RG \
--vnet-name "vnet-prod" \
--name "snet-web" \
--route-table "rt-web"
# Verify effective routes on a VM's NIC
az network nic show-effective-route-table \
--resource-group $RG \
--name "nic-web1" \
-o table
⚠️ Tricky spot: With
--disable-bgp-route-propagation false, routes learned from a VPN/ExpressRoute gateway are added to the route table. With it set totrue, BGP routes are suppressed and only your UDRs apply. The UDR always wins over a BGP route for the same prefix.
Step 6: Clean Up
az group delete --name $RG --yes --no-wait
Lab Tricky Spots Summary
| Trap | Effect | Fix |
|---|---|---|
| NSG Allow rule priority higher than Deny rule for same port | Deny rule evaluated first — traffic blocked | Use lower priority numbers for Allow rules that should take precedence |
| Standard LB with no NSG on backend subnet | Backend VMs unreachable despite correct LB config | Create NSG with Allow rule for the load-balanced port on backend subnet |
VNet peering without allow-forwarded-traffic | Traffic from outside the VNet can't traverse the peer | Enable allowForwardedTraffic on both peering connections |
UDR next-hop-type VirtualAppliance with unreachable NVA | Traffic black-holes — all connectivity breaks | Test NVA reachability before associating UDR; ensure NVA has IP forwarding enabled |
| NSG on NIC AND subnet — forgetting both | Traffic blocked even though one NSG allows it | Check both NSG effective rules using az network nic show-effective-nsg |
Lab Takeaways
- VNet peering is non-transitive — always draw your topology before peering to identify missing direct links.
- Standard LB mandates NSG — the most common reason for "LB is configured but VMs are unreachable" in the exam.
- Effective routes view (
az network nic show-effective-route-table) is the fastest way to diagnose routing issues. - Effective NSG view (
az network nic show-effective-nsg) combines subnet + NIC NSG rules — use it to debug access issues. - UDR overrides BGP — be careful when adding
0.0.0.0/0routes if you have VPN connectivity you want to keep working.