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

  1. Create two VNets and peer them
  2. Verify non-transitive peering behavior
  3. Configure NSG rules and test traffic filtering
  4. Deploy a Standard Load Balancer with two backend VMs
  5. Create a User-Defined Route to direct traffic through a next hop
  6. 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-web we 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 to true, 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

TrapEffectFix
NSG Allow rule priority higher than Deny rule for same portDeny rule evaluated first — traffic blockedUse lower priority numbers for Allow rules that should take precedence
Standard LB with no NSG on backend subnetBackend VMs unreachable despite correct LB configCreate NSG with Allow rule for the load-balanced port on backend subnet
VNet peering without allow-forwarded-trafficTraffic from outside the VNet can't traverse the peerEnable allowForwardedTraffic on both peering connections
UDR next-hop-type VirtualAppliance with unreachable NVATraffic black-holes — all connectivity breaksTest NVA reachability before associating UDR; ensure NVA has IP forwarding enabled
NSG on NIC AND subnet — forgetting bothTraffic blocked even though one NSG allows itCheck both NSG effective rules using az network nic show-effective-nsg

Lab Takeaways

  1. VNet peering is non-transitive — always draw your topology before peering to identify missing direct links.
  2. Standard LB mandates NSG — the most common reason for "LB is configured but VMs are unreachable" in the exam.
  3. Effective routes view (az network nic show-effective-route-table) is the fastest way to diagnose routing issues.
  4. Effective NSG view (az network nic show-effective-nsg) combines subnet + NIC NSG rules — use it to debug access issues.
  5. UDR overrides BGP — be careful when adding 0.0.0.0/0 routes if you have VPN connectivity you want to keep working.