You will deploy an Azure Virtual Network with tiered subnets and peer it to a second VNet. By the end you will have a segmented cloud network foundation that VMs and services can join.

A Virtual Network (VNet) is the fundamental isolation boundary in Azure networking: every VM, container app, or private endpoint you deploy lives inside one. Getting the address plan right on day one matters because the choices you make here, the VNet CIDR range, how you carve it into subnets, and which VNets are allowed to talk to each other, are painful to change once workloads are running. This lab walks through the entire lifecycle with the Azure CLI: planning a non-overlapping address space, creating the VNet, adding tiered subnets, and connecting two VNets with bidirectional peering so resources can reach each other over the Microsoft backbone instead of the public internet.

Learning Objectives

  • Create a VNet with non-overlapping subnets.
  • Peer two VNets for private connectivity.
  • Verify routing between peered networks.
  • Plan CIDR ranges so VNets can be peered later without renumbering.
  • Interpret peering states (Initiated vs Connected) and diagnose why traffic does not flow.
  • Time: ~2 hours · Difficulty: Intermediate · Prereqs: the Azure CLI configured.

Architecture Overview

The lab builds two VNets. vnet-a (10.0.0.0/16) holds the application tiers: a web subnet for internet-facing or load-balanced front ends and an app subnet for middle-tier services that should never be directly exposed. vnet-b (10.1.0.0/16) holds the data subnet, databases and storage that only the app tier should reach. Splitting data into its own VNet models a common production pattern where the data platform is managed by a different team or subscription and is connected on purpose, not by default.

A few design notes on why it is built this way:

  • /16 VNets, /24 subnets: each VNet has 65,536 addresses and each subnet 256 (251 usable, Azure reserves five addresses per subnet for its own routing and DNS). /24 leaves room to grow; you can add dozens more subnets per VNet later.
  • Non-overlapping ranges: 10.0.0.0/16 and 10.1.0.0/16 share no addresses. VNet peering flat-out refuses overlapping address spaces, so planning this up front is what makes the peering step possible at all.
  • Peering, not a VPN gateway: peering is free of gateway VMs, keeps traffic on the Microsoft backbone, and adds no bandwidth cap beyond the VMs themselves. A VPN gateway would cost money and add latency for the same east-west traffic.
  • Peering is not transitive: if you later peer vnet-b to a third vnet-c, vnet-a cannot reach vnet-c through vnet-b unless you add gateway transit or a network virtual appliance. Design for this now, not after the incident.

Core Concepts Before You Start

Three ideas carry this entire lab. Skim them even if you have touched Azure networking before, most peering failures trace back to one of these being misunderstood.

1. CIDR notation is a contract, not a suggestion

A CIDR block like 10.0.0.0/16 says "the first 16 bits are fixed, the remaining 16 are mine." That gives you 2^16 = 65,536 addresses, from 10.0.0.0 to 10.0.255.255. When you carve a subnet out of it, say 10.0.1.0/24, you are promising Azure that the web tier gets exactly the 256 addresses 10.0.1.0 through 10.0.1.255, and that no other subnet may touch them. Of those 256, Azure reserves five for itself (the network address, broadcast, and three internal addresses for its virtual router and DNS), leaving 251 usable. Two practical consequences:

  • Subnet prefixes must fit entirely inside the VNet prefix and must not overlap any sibling subnet. Azure enforces both at creation time.
  • VNet prefixes of peered networks must not overlap at all, 10.0.0.0/16 cannot peer with 10.0.0.0/24, even though one "contains" the other. Overlap is overlap.

If binary subnetting is rusty, the only math you need for this lab: a /16 is 256 blocks of /24, and 10.0.x.x and 10.1.x.x never intersect. That is why the design uses them.

2. Subnets are the security and routing boundary

Every NIC in Azure attaches to exactly one subnet, and Network Security Groups (NSGs) attach to subnets (or NICs, but subnet-level is the maintainable pattern). There is no such thing as an NSG on a VNet. This is the whole reason the architecture has three subnets instead of one big /22 per VNet: the tier boundaries are where the firewall rules will live in the follow-up labs. Decide your tiers now; moving a VM between subnets later means stopping it and moving its NIC.

3. Peering is connectivity, not merging

Peering connects two VNets at the fabric level so packets route privately, but the VNets stay separate administrative objects: separate address spaces, separate NSGs, separate DNS zones if you use them. Peering also composes asymmetric flags, allowVirtualNetworkAccess, allowForwardedTraffic, allowGatewayTransit, and useRemoteGateways, each set per direction. This lab uses only the first (allow access); the gateway-transit pair matters later if one VNet owns a VPN/ExpressRoute gateway and the other wants to share it. And remember the golden rule: peering is non-transitive. A peered to B and B peered to C does not give A a path to C.

Environment Setup

Prerequisites

  • The Azure CLI configured against your subscription (see the Azure CLI Setup lab).
  • An Azure subscription with Contributor access on a resource group.
  • Assumed skills: CIDR notation and how IP ranges nest; a pencil-and-paper plan of your address space.

You will need: the Azure CLI authenticated with a default resource group set.

Before you begin: plan non-overlapping CIDR ranges; peered VNets cannot overlap.

Confirm you are logged in and pointed at the right subscription before creating anything. Networking mistakes are quiet, the CLI will happily deploy into the wrong subscription if you let it.

$ az account show --query "{name:name, id:id, state:state}" -o table
Name id state ------------- ----------------------------------- -------- Pay-As-You-Go 11111111-2222-3333-4444-555555555555 Enabled

Everything in this lab lives in one resource group so cleanup is a single command at the end. Create it and set it (plus a region) as your CLI defaults so you can stop typing -g on every command:

az group create -n tyf-net-rg -l eastus
Creates the lab resource group in East US.
az configure --defaults group=tyf-net-rg location=eastus
All later commands assume this group and region.
$ az group show -n tyf-net-rg --query provisioningState -o tsv
Succeeded

Common mistake: forgetting that az configure --defaults persists across terminal sessions. If a later lab deploys somewhere unexpected, check az configure --list-defaults first.

Permissions: creating VNets and peerings requires the Network Contributor role (or higher) on the resource group, and peering requires it on both VNets. If you are on a shared subscription and step 03 fails with an authorization error, this, not the command syntax, is the first thing to check.

Cost: VNets and subnets are free, and peering has no hourly charge, but data transferred across a peering is billed at both ends, about $0.01/GB each direction for same-region peering. A lab ping test costs fractions of a cent; a production workload moving terabytes does not. The billable items in this lab are the optional test VMs in the validation section, any public IPs they get, and peering data transfer. Peering across regions (global VNet peering) bills per GB at higher zonal rates, irrelevant here, but worth knowing before you copy this pattern across regions.

Step-by-Step Execution

Before touching the CLI, write the address plan down. This is the artifact you will check every command against, and it is what a reviewer (or future you) needs to understand the network:

# Address plan - tyf-net-rg, eastus
VNet Address space Subnet Prefix Purpose vnet-a 10.0.0.0/16 web 10.0.1.0/24 front-end / load-balanced tier vnet-a 10.0.0.0/16 app 10.0.2.0/24 middle tier, no public exposure vnet-b 10.1.0.0/16 data 10.1.1.0/24 databases and stateful services

Notice what the plan leaves free: 10.0.3.0/24 onward in vnet-a, and almost all of vnet-b. Growth headroom is a design feature, not waste, reserved ranges cost nothing until you allocate them.

01
Create the VNet and subnets

What: create vnet-a with its /16 address space and the first subnet (web) in one command. Why: az network vnet create requires at least one subnet if you pass subnet flags, so defining the web tier up front saves a second call. The subnet prefix must sit inside the VNet prefix, 10.0.1.0/24 is the first /24 carve-out of 10.0.0.0/16.

az network vnet create -n vnet-a --address-prefixes 10.0.0.0/16 --subnet-name web --subnet-prefixes 10.0.1.0/24

The command returns the full VNet object as JSON. The fields worth checking are addressSpace and the subnet's provisioningState:

$ az network vnet show -n vnet-a --query "{prefixes:addressSpace.addressPrefixes, subnets:subnets[].name}" -o json
{ "prefixes": [ "10.0.0.0/16" ], "subnets": [ "web" ] }

How to verify: az network vnet list -o table should show vnet-a with provisioning state Succeeded. Common mistake: typos in the CIDR (e.g. 10.0.0.0/8 when you meant /16) succeed silently, nothing validates your intent, only the syntax. Always echo the address space back with the query above before moving on.

DNS comes for free, but know what you got: unless you pass --dns-servers, every VNet uses Azure-provided DNS (168.63.129.16), which resolves names of resources inside that VNet automatically. It does not resolve names in the peered VNet, name resolution does not ride across peering. After this lab, VMs in vnet-a can ping vnet-b hosts by IP, but resolving their names needs a Private DNS zone linked to both VNets. That is a deliberate follow-up exercise, not a bug.

Add tags while you are here. Networking resources outlive the people who created them, and tags are how the next person learns why a VNet exists:

az network vnet update -n vnet-a --set tags.owner=taki tags.env=lab tags.tier=app-platform
Tags are free-form key/value pairs used for ownership, billing, and policy.

Expected output:

{
  "name": "tyf-vnet",
  "properties": {
    "addressSpace": { "addressPrefixes": ["10.1.0.0/16"] },
    "provisioningState": "Succeeded"
  }
}

What this teaches: Address planning is the real deliverable; non-overlapping CIDR ranges are a hard prerequisite for peering and are expensive to fix later.

02
Add a second subnet

What: add the app subnet to vnet-a. Why: tiers belong in separate subnets because Network Security Groups and route tables attach to subnets, not to individual VMs. If web and app shared a subnet, you could not write an NSG rule that lets the internet reach web but not app, the segmentation this whole lab is about would be impossible.

az network vnet subnet create --vnet-name vnet-a -n app --address-prefixes 10.0.2.0/24

List the subnets to confirm both exist and did not overlap:

$ az network vnet subnet list --vnet-name vnet-a -o table
Name AddressPrefix PrivateEndpointNetworkPolicies ------ --------------- ------------------------------ app 10.0.2.0/24 Enabled web 10.0.1.0/24 Enabled

Common mistake: trying to create a subnet whose range overlaps an existing one (say 10.0.1.128/25) fails with NetcfgSubnetRangesOverlap. Azure checks every new subnet against its siblings, which is your safety net, but the error means you have to pick a free range, so keep a written map of what is allocated.

Sizing note: the error you will hit in real life is not overlap but exhaustion. A /24 with 251 usable IPs sounds like a lot until an AKS node pool or a VM scale set lands in it. If you already know a subnet will host autoscaled compute, give it a /22 or /21 now, resizing a subnet means recreating it, which means evicting everything in it.

Expected output:

$ az network vnet subnet list -g tyf-rg --vnet-name tyf-vnet -o table
Name    AddressPrefix
------  ---------------
web     10.1.1.0/24
app     10.1.2.0/24

What this teaches: Subnets are the unit of segmentation; NSGs and route tables attach there, so tier workloads across subnets from day one.

03
Peer two VNets

What: create the second VNet and connect the two with peering. Why: peering is how separate VNets exchange private traffic. One critical detail trips up almost everyone the first time: peering is a pair of one-way links. Creating a-to-b alone leaves the peering stuck in the Initiated state; traffic only flows once the reverse link b-to-a also exists and both sides report Connected.

First, create vnet-b with its data subnet. Notice the completely different /16, this is the non-overlap requirement in action:

az network vnet create -n vnet-b --address-prefixes 10.1.0.0/16 --subnet-name data --subnet-prefixes 10.1.1.0/24
Second VNet with the data tier; ranges must not overlap vnet-a.

Now create the first half of the peering pair, from vnet-a to vnet-b:

$ az network vnet peering create -n a-to-b --vnet-name vnet-a --remote-vnet vnet-b --allow-vnet-access
"peeringState": "Connected"

--allow-vnet-access is the flag that actually permits traffic between the address spaces; without it the peering exists but drops packets. Then create the reverse direction. Until this command runs, the first peering sits at Initiated and nothing routes:

$ az network vnet peering create -n b-to-a --vnet-name vnet-b --remote-vnet vnet-a --allow-vnet-access
"peeringState": "Connected"

Confirm both directions finished handshaking, you are looking for Connected on each side:

$ az network vnet peering list --vnet-name vnet-a --query "[].{name:name, state:peeringState, access:allowVirtualNetworkAccess}" -o table
Name State Access ------ --------- -------- a-to-b Connected True
$ az network vnet peering list --vnet-name vnet-b --query "[].{name:name, state:peeringState}" -o table
Name State ------ --------- b-to-a Connected

Common mistake: creating only one direction and assuming it worked because the command succeeded. The CLI happily creates a half-pair. How to verify: a peering stuck at Initiated always means the reverse link is missing, create it, do not delete and retry the existing one.

Cross-subscription peering: when the remote VNet lives in another subscription or resource group, a bare name is ambiguous, so --remote-vnet takes the full resource ID instead. The pattern below resolves the ID dynamically; both directions are still required, and each command runs in the context of the subscription that owns its local VNet:

az network vnet peering create -n a-to-b --vnet-name vnet-a --remote-vnet $(az network vnet show -n vnet-b -g other-rg --query id -o tsv) --allow-vnet-access
Use full resource IDs when names alone are not unique.

One more state worth knowing: besides Initiated and Connected, the portal sometimes shows a peering as needing a sync after the remote address space changed. If connectivity silently broke after someone edited a peered VNet's ranges, run az network vnet peering sync -n a-to-b --vnet-name vnet-a on both directions to re-push the address space into the effective routes.

Expected output:

$ az network vnet peering list -g tyf-rg --vnet-name tyf-vnet -o table
Name           PeeringState    RemoteVnet
-------------  --------------  ------------
tyf-to-branch  Connected       branch-vnet

What this teaches: Peering merges two VNets' routing at the control plane, but it is not transitive; design hub-and-spoke accordingly.

Step Flow Map

Each box is one step in the lab; work them in order, since later steps build on earlier state.

Progress So Far

How It Works

A virtual network is your private address space in Azure, and the address plan is the real deliverable of this lab. CIDR ranges nest: a /16 contains 256 /24s, and once two VNets peer, their ranges must not overlap, ever. Azure also reserves the first four and the last address of every subnet, so a /24 yields 251 usable addresses, not 256. Getting the plan right on paper before creating anything is the difference between a clean topology and a rebuild.

Subnets are the unit of segmentation inside a VNet. They do not route between each other through anything you configure: intra-VNet routing is automatic, and segmentation comes from attaching NSGs and route tables at the subnet boundary. Tier your workloads across subnets from day one (web, app, data) because moving a running VM between subnets later is far more painful than planning for it.

VNet peering merges two VNets at the control plane: each side learns the other's routes, and traffic stays on the Microsoft backbone. Peering is explicitly non-transitive: if A peers with B and B peers with C, A cannot reach C. Hub-and-spoke designs exist precisely because of this property, with the hub (often holding the firewall) peered to every spoke.

Testing & Validation

az network vnet peering show -n a-to-b --vnet-name vnet-a --query peeringState -o tsv

You should see Connected. With both directions peered, resources in app and data subnets can reach each other privately.

State checks prove the control plane agreed to peer, but they do not prove packets flow. The authoritative data-plane check is the effective routes on a network interface. If you have a VM in the app subnet (see the Azure VM Configuration lab), look at what its NIC learned from the peering:

$ az network nic show-effective-route-table --ids $(az vm show -n app-vm-01 --query networkProfile.networkInterfaces[0].id -o tsv) -o table
Source State Address Prefix Next Hop Type Next Hop IP -------- ------- ---------------- ---------------- ------------- Default Active 10.0.0.0/16 VnetLocal Default Active 10.1.0.0/16 VNetPeering Default Active 0.0.0.0/0 Internet

The line that matters is 10.1.0.0/16 → VNetPeering: the app subnet now routes the entire vnet-b address space across the peering. No manual route table entries were needed, peering programs these system routes automatically on every subnet in both VNets.

For an end-to-end test, deploy a small VM into the data subnet and one into app, then ping or SSH between their private IPs:

az vm create -n data-vm-01 --image Ubuntu2204 --vnet-name vnet-b --subnet data --size Standard_B1s --admin-username azureuser --generate-ssh-keys
Cheapest test VM; delete it when validation is done.
$ ssh azureuser@app-vm-01 "ping -c 3 10.1.1.4"
3 packets transmitted, 3 received, 0% packet loss, time 2003ms rtt min/avg/max/mdev = 1.242/1.389/1.604/0.151 ms

Sub-2ms latency between VNets in the same region is the signature of peering on the Microsoft backbone, a path through the public internet would show noticeably higher and more variable times. Note that default NSG rules allow VNet-to-VNet traffic, so this works before you lock anything down; once you attach NSGs in the next lab, expect to re-visit this test.

If the ping fails, the next diagnostic layer is the effective security rules on the destination NIC, the merged result of every NSG attached to its subnet and NIC:

$ az network nic list-effective-nsg --ids $(az vm show -n data-vm-01 --query networkProfile.networkInterfaces[0].id -o tsv) --query "value[].defaultSecurityRules[?direction=='Inbound'].{name:name, access:access, prefix:sourceAddressPrefix}" -o table
Name access prefix ------------------------- -------- ---------------------- AllowVnetInBound Allow VirtualNetwork AllowAzureLoadBalancer... Allow AzureLoadBalancer DenyAllInBound Deny 0.0.0.0/0

AllowVnetInBound is the default rule doing the heavy lifting: its VirtualNetwork service tag automatically includes peered VNet address spaces, which is why no firewall change was needed for this lab. DenyAllInBound at priority 65500 catches everything else, internet traffic to the data VM is already dropped even without any NSG you wrote.

Portal cross-check: open the resource group in the Azure portal and confirm you can see both VNets, three subnets, and two peering resources. CLI-first labs are faster to repeat, but a quick portal glance catches the "I deployed into the wrong subscription" class of mistake that CLI output will not.

Cleanup when you are done, so test VMs do not bill overnight:

az group delete -n tyf-net-rg --yes --no-wait
Deletes the VNets, peerings, subnets, and test VMs in one shot.

Troubleshooting

Quick-reference for the failures you are most likely to hit in this lab.

SymptomLikely causeFix
Peering creation failsThe two address spaces overlapRe-plan with non-overlapping ranges; overlapping VNets can never peer, no matter the settings.
Peering shows DisconnectedPeering is one-directional until both sides are createdCreate the reverse peering; both directions must exist for traffic to flow.
Fewer usable IPs than expectedAzure reserves five addresses per subnet (.0-.3 and the last)Account for the reservation in your plan; a /29 leaves only 3 usable addresses.
Can reach peer A but not peer C through BPeering is non-transitive by designPeer A directly with C, or route through a hub with gateway transit enabled.
Name resolution fails across the peerCustom DNS not configured on the VNetSet the DNS servers on the VNet and restart the VMs to pick them up.
Troubleshooting
  • Peering stuck at Initiated: create the peering in both directions; it requires a pair. Check both sides with az network vnet peering list --vnet-name <vnet>, whichever side is missing a link is the one you still need to create.
  • Overlapping address space: peered VNets must use non-overlapping CIDR ranges. The error NetcfgAddressPrefixesOverlapForPeering means exactly this. The only clean fix is to recreate one VNet with a different range, address spaces of peered VNets cannot be edited in place without removing the peering first.
  • No connectivity despite peering: check NSGs and that -allow-vnet-access is set. Run az network vnet peering show -n a-to-b --vnet-name vnet-a --query allowVirtualNetworkAccess, if it returns false, update the peering: az network vnet peering update -n a-to-b --vnet-name vnet-a --set allowVirtualNetworkAccess=true.
  • Ping fails but SSH works (or vice versa): this is almost never peering. NSG default rules allow all VNet-internal traffic, but a VM-level firewall (e.g. ufw on Ubuntu) or a custom NSG rule may block ICMP while allowing TCP/22. Test with a TCP tool like curl -v telnet://10.1.1.4:22 before blaming the network.
  • New address range not reachable after peering: adding prefixes to a peered VNet is allowed, but the peering must be synced before the new range propagates (az network vnet update --add addressSpace.addressPrefixes=10.0.4.0/22, then sync the peering in the portal under Peering). Plan the /16 sizing generously at the start to avoid touching live routing later.

Extension Ideas

  • Place a VM in a subnet with Azure VM Configuration.
  • Control traffic with NSGs & Route Tables.
  • Codify the whole network with IaC.
  • Add a third VNet and observe first-hand that peering is not transitive, then fix it with a hub-and-spoke design where a hub VNet hosts shared services.
  • Attach an NSG to the data subnet that only accepts traffic from 10.0.2.0/24 (the app tier), and re-run the connectivity test from the web subnet to prove it now fails.

Key Results

  • Deployed a VNet with two non-overlapping subnets.
  • Connected two VNets via bidirectional peering.
  • Confirmed the peering reached the Connected state.
  • Built a segmented foundation for cloud workloads.

Key Takeaways

  • Address planning is the real deliverable: non-overlapping CIDR ranges are a hard prerequisite for peering and are expensive to fix later.
  • Subnets are the unit of segmentation, NSGs and route tables attach there, so tier your workloads across subnets from day one.
  • Peering is a pair of one-way links; Initiated means the reverse side is missing, and --allow-vnet-access is what actually permits traffic.
  • Peering is not transitive: each pair of VNets that must talk needs its own peering (or a hub-and-spoke design).
  • Verify the data plane, not just the control plane: Connected state plus an effective-route-table entry plus a real ping is the complete proof.

Full Command Recap

Every command from this lab in order, assuming the defaults set in Environment Setup. This is the checklist to re-run when reproducing the lab from scratch:

$ az group create -n tyf-net-rg -l eastus
$ az network vnet create -n vnet-a --address-prefixes 10.0.0.0/16 --subnet-name web --subnet-prefixes 10.0.1.0/24
$ az network vnet subnet create --vnet-name vnet-a -n app --address-prefixes 10.0.2.0/24
$ az network vnet create -n vnet-b --address-prefixes 10.1.0.0/16 --subnet-name data --subnet-prefixes 10.1.1.0/24
$ az network vnet peering create -n a-to-b --vnet-name vnet-a --remote-vnet vnet-b --allow-vnet-access
$ az network vnet peering create -n b-to-a --vnet-name vnet-b --remote-vnet vnet-a --allow-vnet-access
$ az network vnet peering list --vnet-name vnet-a -o table
$ az network vnet peering list --vnet-name vnet-b -o table
# cleanup az group delete -n tyf-net-rg --yes --no-wait

Notice what is not in the list: no route tables, no NSGs, no gateway resources. The entire routed, segmented, two-VNet topology is nine commands, because Azure's system routes and default NSG rules do the rest. Everything you add from here, NSGs, route tables, DNS zones, is tightening or extending this baseline, and each of those layers is a follow-up lab.

Practice Challenges

Challenge 1: Plan on paper first

Goal: Split a /16 into four /18s on paper, assign each a purpose, then deploy exactly that.

Hint: Write the ranges down before touching the CLI; the plan is the deliverable.

Challenge 2: Hub and spoke

Goal: Build a hub VNet peered to two spokes and prove the spokes cannot reach each other directly.

Hint: The failed ping between spokes is the proof that peering is non-transitive.

Challenge 3: Custom DNS

Goal: Point the VNet at a custom DNS server and verify name resolution from a VM.

Hint: Run nslookup from the VM; it shows which server answered.