Azure · lesson 5 of 10
Also part of: Networking
Azure Virtual Network Basics
Build a VNet with two subnets, apply an NSG, then walk the exact debugging sequence for a VM that can't reach the internet.
By SkillByExample · Updated
A virtual network (VNet) is an address space; subnets carve it into ranges that resources deploy into; NSGs attach to a subnet or a network interface and filter what passes. Almost every “my VM can’t talk to X” problem in Azure networking is a rule in one of these three layers, evaluated in an order that isn’t obvious until you’ve been burned by it once.
Building the network
az network vnet create \
--resource-group learn-rg \
--name learn-vnet \
--address-prefix 10.1.0.0/16 \
--subnet-name app-subnet \
--subnet-prefix 10.1.1.0/24
$ az network vnet create --resource-group learn-rg --name learn-vnet --address-prefix 10.1.0.0/16 --subnet-name app-subnet --subnet-prefix 10.1.1.0/24
{
"newVNet": {
"addressSpace": {
"addressPrefixes": ["10.1.0.0/16"]
},
"name": "learn-vnet",
"provisioningState": "Succeeded",
"subnets": [
{
"addressPrefix": "10.1.1.0/24",
"name": "app-subnet"
}
]
}
}
az network vnet subnet create \
--resource-group learn-rg \
--vnet-name learn-vnet \
--name data-subnet \
--address-prefix 10.1.2.0/24
$ az network vnet subnet create --resource-group learn-rg --vnet-name learn-vnet --name data-subnet --address-prefix 10.1.2.0/24
{
"addressPrefix": "10.1.2.0/24",
"name": "data-subnet",
"provisioningState": "Succeeded"
}
Two subnets, one VNet: app-subnet for VMs that need inbound access, data-subnet for
resources that should never take inbound traffic directly. Splitting subnets by exposure
level is what makes an NSG rule set legible later — one NSG per subnet, not one giant NSG
covering everything.
Attaching an NSG that (deliberately) blocks outbound
az network nsg create --resource-group learn-rg --name app-nsg
$ az network nsg create --resource-group learn-rg --name app-nsg
{
"NewNSG": {
"name": "app-nsg",
"provisioningState": "Succeeded",
"securityRules": []
}
}
az network nsg rule create \
--resource-group learn-rg \
--nsg-name app-nsg \
--name deny-all-outbound \
--priority 100 \
--direction Outbound \
--access Deny \
--protocol "*" \
--destination-port-ranges "*"
az network vnet subnet update \
--resource-group learn-rg \
--vnet-name learn-vnet \
--name app-subnet \
--network-security-group app-nsg
$ az network nsg rule create --resource-group learn-rg --nsg-name app-nsg --name deny-all-outbound --priority 100 --direction Outbound --access Deny --protocol "*" --destination-port-ranges "*"
{
"access": "Deny",
"direction": "Outbound",
"name": "deny-all-outbound",
"priority": 100,
"protocol": "*"
}
$ az network vnet subnet update --resource-group learn-rg --vnet-name learn-vnet --name app-subnet --network-security-group app-nsg
{
"name": "app-subnet",
"networkSecurityGroup": {
"id": "/subscriptions/3b1f9c2a-6d4e-4a7b-9c11-2f5e8a0b7d33/resourceGroups/learn-rg/providers/Microsoft.Network/networkSecurityGroups/app-nsg"
}
}
Priority 100 beats the NSG’s built-in 65001 outbound-allow default rule, so this VM’s
internet access is now blocked — set up on purpose to walk through diagnosing it.
The debugging sequence: a VM that can’t reach the internet
A VM deployed into app-subnet can’t curl out. First, confirm it really is a network
problem and not a DNS or application one:
curl -m 5 https://example.com
$ curl -m 5 https://example.com
curl: (28) Failed to connect to example.com port 443 after 5001 ms: Connection timed out
A timeout, not “could not resolve host” — DNS resolved fine, the connection itself is being dropped somewhere. Next, check the NSG rules actually in effect for the VM’s network interface, not just what’s on the subnet:
az network nic list-effective-nsg --resource-group learn-rg --name learn-vmVMNic --output table
$ az network nic list-effective-nsg --resource-group learn-rg --name learn-vmVMNic --output table
NSG RuleName Priority Direction Access
---------------------- ------------------- ---------- ----------- --------
app-nsg deny-all-outbound 100 Outbound Deny
app-nsg AllowVnetOutBound 65000 Outbound Allow
app-nsg AllowInternetOutBound 65001 Outbound Allow
deny-all-outbound at priority 100 sits above both default allow rules and matches
*/* — it wins for every outbound packet regardless of destination. This is the finding:
not a routing problem, not a missing public IP, but a deny rule shadowing the defaults.
az network nsg rule delete --resource-group learn-rg --nsg-name app-nsg --name deny-all-outbound
$ az network nsg rule delete --resource-group learn-rg --nsg-name app-nsg --name deny-all-outbound
curl -m 5 -o /dev/null -w "%{http_code}\n" https://example.com
$ curl -m 5 -o /dev/null -w "%{http_code}\n" https://example.com
200
Removing the one over-broad rule restored outbound access immediately — no VM restart, no route change needed, because NSG rule changes apply to new connections right away.
Practice
1. List all subnets in the VNet along with their address prefixes.
az network vnet subnet list --resource-group learn-rg --vnet-name learn-vnet --query "[].{Name:name, Prefix:addressPrefix}" --output table
$ az network vnet subnet list --resource-group learn-rg --vnet-name learn-vnet --query "[].{Name:name, Prefix:addressPrefix}" --output table
Name Prefix
----------- ------------
app-subnet 10.1.1.0/24
data-subnet 10.1.2.0/24
Reading subnet ranges before adding a new one avoids an overlapping-prefix error, which Azure rejects at creation time rather than allowing and routing ambiguously.
2. Add an NSG rule that allows outbound HTTPS specifically, instead of removing the deny-all rule.
az network nsg rule create \
--resource-group learn-rg \
--nsg-name app-nsg \
--name allow-https-outbound \
--priority 90 \
--direction Outbound \
--access Allow \
--protocol Tcp \
--destination-port-ranges 443
$ az network nsg rule create --resource-group learn-rg --nsg-name app-nsg --name allow-https-outbound --priority 90 --direction Outbound --access Allow --protocol Tcp --destination-port-ranges 443
{
"access": "Allow",
"direction": "Outbound",
"name": "allow-https-outbound",
"priority": 90,
"protocol": "Tcp",
"destinationPortRange": "443"
}
Priority 90 beats deny-all-outbound’s 100, so HTTPS specifically gets through while
everything else stays blocked — a narrower fix than deleting the deny rule outright,
useful when you want default-deny with named exceptions.
3. Peer two VNets and confirm the peering state.
az network vnet create --resource-group learn-rg --name learn-vnet-2 --address-prefix 10.2.0.0/16
az network vnet peering create --resource-group learn-rg --name learn-to-2 --vnet-name learn-vnet --remote-vnet learn-vnet-2 --allow-vnet-access true
az network vnet peering list --resource-group learn-rg --vnet-name learn-vnet --output table
$ az network vnet peering list --resource-group learn-rg --vnet-name learn-vnet --output table
Name PeeringState ProvisioningState
---------- -------------- -------------------
learn-to-2 Initiated Succeeded
Initiated rather than Connected — peering requires a matching peering resource created
on the other VNet too; only once both sides exist does the state flip to Connected and
traffic actually flows.
4. Add a route table that sends all outbound traffic through a virtual appliance instead of directly to the internet.
az network route-table create --resource-group learn-rg --name learn-rt
az network route-table route create \
--resource-group learn-rg \
--route-table-name learn-rt \
--name default-via-appliance \
--address-prefix 0.0.0.0/0 \
--next-hop-type VirtualAppliance \
--next-hop-ip-address 10.1.1.10
az network vnet subnet update --resource-group learn-rg --vnet-name learn-vnet --name app-subnet --route-table learn-rt
$ az network vnet subnet update --resource-group learn-rg --vnet-name learn-vnet --name app-subnet --route-table learn-rt
{
"name": "app-subnet",
"routeTable": {
"id": "/subscriptions/3b1f9c2a-6d4e-4a7b-9c11-2f5e8a0b7d33/resourceGroups/learn-rg/providers/Microsoft.Network/routeTables/learn-rt"
}
}
This is the other common cause of “can’t reach the internet”: a user-defined route (UDR) overriding Azure’s default internet route with a next hop that points at an appliance which may not actually be forwarding traffic — worth checking alongside NSGs, not instead of them.
5. Check whether a specific port between two subnets is allowed, using Azure's built-in connectivity check rather than reasoning about rules by hand.
az network watcher test-ip-flow \
--resource-group learn-rg \
--vm learn-vm \
--direction Outbound \
--protocol TCP \
--local 10.1.1.4:0 \
--remote 10.1.2.5:5432
$ az network watcher test-ip-flow --resource-group learn-rg --vm learn-vm --direction Outbound --protocol TCP --local 10.1.1.4:0 --remote 10.1.2.5:5432
{
"access": "Deny",
"ruleName": "deny-all-outbound"
}
test-ip-flow simulates the exact packet through the current NSG rule set and reports which
rule decided the outcome — the fast path to an answer instead of manually reading rule
priorities against every layer.
Next: Azure SQL — provisioning a database, connecting to it, and a backup/restore walkthrough.