Azure Lesson 18 of 137

Azure Virtual Network Basics: Subnets, NSGs, and Peering

In a nutshell

Picture a private office building that only your company occupies. The building itself is the virtual network (VNet) — your own walled-off space in Azure with a street-address range (its IP address space) that nobody else shares. Inside, you divide the building into floors: each floor is a subnet, a slice of that address range set aside for one kind of work — reception and the public lobby on one floor, the application teams on another, the finance systems locked away on a third.

Every floor has a badge reader on its doors, and the rules programmed into those readers — who may enter, from where, through which door (port) — are the Network Security Group (NSG). A well-written badge rule says “only staff coming up from the app floor may open the finance-floor door, and nobody else,” which is exactly how you stop a compromised public-lobby machine from ever reaching the finance database. And when two of your buildings need to move people between them privately, without anyone stepping onto the public street, you build a private skybridge — that is VNet peering: a direct, private link over Azure’s own backbone, never the public internet.

That is the whole lesson in four objects: building, floors, door-badge rules, skybridge. Everything else in Azure networking — firewalls, load balancers, VPNs, private DNS — hangs off those four. Get the building’s floor plan and badge rules right at the start and the rest of your Azure estate has solid ground to stand on; get them wrong and you spend years renovating an occupied building, which is as painful as it sounds.

Level: Beginner · Time: ~37 min

Before you start, be comfortable with:

After this lesson you will be able to:

A regional logistics company — think a parcel carrier running depots, sortation hubs, and a fleet-tracking platform across three states — has just been told by its new CISO that “everything in Azure is on one flat network, and that has to change this quarter.” Today a single virtual network holds the public-facing tracking website, the internal warehouse-management app, a finance database, and a handful of developer test machines, all able to reach each other freely. An auditor flagged it: a compromised web server could talk straight to the finance database, and there is nothing in the network itself stopping it. The company is not asking for a fancy zero-trust mesh — it is asking for the fundamentals, done correctly, so the next thing they build sits on solid ground instead of being a flat network with a bigger blast radius. This article is that foundation: what a virtual network actually is, how subnets and network security groups carve it into defensible zones, how peering stitches networks together, and where the bigger pieces — a firewall, identity, monitoring — bolt on later.

If you are early in a cloud career, this is the layer everything else stands on. Compute, databases, Kubernetes, and AI services all ultimately plug into a network. Get the network model wrong and you spend years fighting it; get it right at the start and the rest of the platform has a place to live.

What a virtual network actually is

An Azure Virtual Network (VNet) is your own private slice of network inside Azure — a logically isolated space with a private IP address range that you control, the cloud equivalent of the network behind your office router. Nothing outside it can reach into it unless you explicitly allow it. When you create a VNet you give it an address space in CIDR notation, for example 10.20.0.0/16, which reserves roughly 65,000 private IP addresses for resources you place inside.

Two rules trip up almost every beginner, so learn them now:

For the logistics company, the first decision is simply to stop using one VNet for everything and instead reserve a clean, non-overlapping block — say 10.0.0.0/8 for all of Azure — and hand out /16 slices per environment: 10.10.0.0/16 for shared services, 10.20.0.0/16 for production, 10.30.0.0/16 for development. That allocation plan, boring as it sounds, is the real deliverable of week one.

Subnets: carving the network into zones

A VNet is a single big room. Subnets are the walls you build inside it. A subnet is a sub-range of the VNet’s address space — for example, inside 10.20.0.0/16 you might define 10.20.1.0/24 (256 addresses) for web servers and 10.20.2.0/24 for databases. Each resource you deploy — a virtual machine’s network card, a load balancer, a private endpoint — gets an IP from exactly one subnet.

Why bother splitting at all? Because the subnet is the natural unit you attach security and routing rules to. Putting the public website in a web subnet and the finance database in a data subnet lets you say, at the network layer, “the data subnet only accepts connections from the app subnet, and nothing else” — which is precisely the auditor’s complaint, solved.

A few subnet facts that matter in practice:

Here is a minimal Terraform sketch of the production VNet with three subnets. The logistics team uses Terraform as its infrastructure-as-code tool so the entire network is defined in version-controlled files rather than clicked together in the portal — which means the layout is reviewable, repeatable across dev and prod, and recoverable if someone deletes the wrong thing.

resource "azurerm_virtual_network" "prod" {
  name                = "vnet-prod-eastus"
  address_space       = ["10.20.0.0/16"]
  location            = "eastus"
  resource_group_name = "rg-network-prod"
}

resource "azurerm_subnet" "web" {
  name                 = "snet-web"
  virtual_network_name = azurerm_virtual_network.prod.name
  resource_group_name  = "rg-network-prod"
  address_prefixes     = ["10.20.1.0/24"]
}

resource "azurerm_subnet" "app" {
  name                 = "snet-app"
  virtual_network_name = azurerm_virtual_network.prod.name
  resource_group_name  = "rg-network-prod"
  address_prefixes     = ["10.20.2.0/24"]
}

resource "azurerm_subnet" "data" {
  name                 = "snet-data"
  virtual_network_name = azurerm_virtual_network.prod.name
  resource_group_name  = "rg-network-prod"
  address_prefixes     = ["10.20.3.0/24"]
}

Network Security Groups: the firewall on every door

A Network Security Group (NSG) is a stateful packet filter — a list of allow/deny rules for traffic in and out. It is the tool that actually enforces “web can talk to app, app can talk to data, web cannot talk to data directly.” You attach an NSG to a subnet (covering everything in it) or to an individual network interface (one VM), and the rules are evaluated by priority number, lowest first, with the first match winning.

Each rule specifies a priority (100–4096), a direction (inbound/outbound), source and destination (IP ranges, or handy service tags like Internet, VirtualNetwork, AzureLoadBalancer), a port range, a protocol, and allow or deny. Azure adds invisible default rules at the bottom: inbound traffic within the VNet is allowed, traffic from the load balancer is allowed, and everything else inbound from the internet is denied — but you should never rely on defaults alone; write your intent explicitly.

The word that matters most is stateful. If you allow an inbound connection on port 443, the return traffic is automatically permitted — you do not write a matching outbound rule for replies. This trips up people coming from old-school stateless ACLs.

Here is the data subnet’s NSG expressing the auditor’s requirement directly: the database accepts SQL (1433) only from the app subnet, and explicitly denies everything else inbound.

resource "azurerm_network_security_group" "data" {
  name                = "nsg-data"
  location            = "eastus"
  resource_group_name = "rg-network-prod"

  security_rule {
    name                       = "Allow-SQL-From-App"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_address_prefix      = "10.20.2.0/24"   # app subnet only
    destination_port_range     = "1433"
    source_port_range          = "*"
    destination_address_prefix = "10.20.3.0/24"
  }

  security_rule {
    name                       = "Deny-All-Inbound"
    priority                   = 4096
    direction                  = "Inbound"
    access                     = "Deny"
    protocol                   = "*"
    source_address_prefix      = "*"
    destination_port_range     = "*"
    source_port_range          = "*"
    destination_address_prefix = "*"
  }
}

With that NSG attached to snet-data, a compromised web server can no longer reach the finance database at all — the network drops the packet before the database process ever sees it. That is defense in depth at the network layer, and it is exactly the control the audit demanded.

NSGs are not a full firewall, and you should know the limit. They filter on IP addresses, ports, and protocols (Layers 3–4). They do not inspect application content, do not do URL filtering, cannot detect a malicious payload inside an allowed HTTPS connection, and offer no threat intelligence. They answer “is this address allowed to talk to that port” — nothing more. That boundary is exactly why a firewall enters the picture later (below).

VNet peering: connecting networks privately

So far the production VNet stands alone. Real platforms have several VNets — separate environments, separate regions, shared services — and they need to talk. VNet peering connects two VNets so resources in each can reach the other over Azure’s private backbone, using private IPs, as if they were one network. Traffic never touches the public internet, latency is low, and there is no gateway or VPN appliance to manage.

Key properties to internalize:

This is where the upfront address plan pays off. Because shared services were placed at 10.10.0.0/16, production at 10.20.0.0/16, and dev at 10.30.0.0/16 — none overlapping — they can all peer freely. Had two of them collided, peering would simply be impossible.

Architecture overview

Azure Virtual Network Basics: Subnets, NSGs, and Peering — architecture

The starter design the logistics company lands on is a hub-spoke topology — the most common enterprise pattern in Azure, and one you should learn early because nearly every real deployment grows into it. A central hub VNet holds shared services that everything needs; each workload lives in its own spoke VNet, peered to the hub but not to each other. Traffic between spokes flows through the hub, where it can be inspected and controlled centrally.

The hub (10.10.0.0/16) holds the things every workload shares: a connectivity gateway back to the depots, DNS, a future firewall, and a secure-jump path for admins. The hub is where central controls live so you build them once, not per spoke.

Production spoke (10.20.0.0/16) holds the live tracking platform across its three subnets — web (public-facing, behind a load balancer and a CDN), app (the warehouse-management logic), and data (the finance and tracking databases). Each subnet has an NSG; the data NSG is the locked-down one shown above.

Development spoke (10.30.0.0/16) is an isolated copy for the engineers. Because it is a separate spoke peered only to the hub, a mistake in dev cannot reach production data — the network itself enforces the boundary the flat design never had.

Control and data flow, end to end:

  1. A customer tracking a parcel hits the public website. Akamai sits at the edge as the CDN and web application firewall — it terminates TLS close to the user, caches static tracking-page assets, and absorbs bot and DDoS traffic before any request reaches Azure. Only legitimate, filtered traffic arrives at the web subnet’s load balancer.
  2. The web tier calls the app tier. The web-to-app hop is permitted by NSG rules; the reverse and any direct web-to-data attempt is denied.
  3. The app tier queries the data tier on port 1433, allowed by the single Allow-SQL-From-App rule. Nothing else on the network can reach the database.
  4. When production needs something from shared services — central DNS, a secrets fetch, the route to on-premises depots — it crosses the hub-to-spoke peering into the hub.
  5. Administrators never expose RDP/SSH to the internet. They reach VMs through Azure Bastion in the hub (its own AzureBastionSubnet), brokered by identity, so management ports stay closed on every NSG.

This is deliberately a starting architecture. It is correct, defensible, and small — and it has obvious seams where the next pieces attach.

Where the bigger pieces attach

The fundamentals above are necessary but not sufficient for a mature platform. Here is where the named enterprise tools plug in, and crucially why each one is needed beyond what NSGs and peering already give you.

Concern Tool What it adds that the network layer cannot
Deep traffic inspection Azure Firewall (or a third-party virtual appliance like Palo Alto / Fortinet) Application-layer and URL filtering, threat intelligence, and a central choke point — far beyond an NSG’s IP/port matching
Human & service identity Microsoft Entra ID, federated from Okta The network says where traffic may go; identity says who may sign in. Okta is the workforce IdP; it federates to Entra so Azure honors a first-class token, and Entra Conditional Access gates the Bastion admin path
Secrets HashiCorp Vault Database credentials and API keys must not sit in NSG rules, code, or config files. Vault issues short-lived, dynamic secrets so the app authenticates to the database without a static password living anywhere
Cloud posture Wiz / Wiz Code Continuously scans for the exact mistakes this article warns about — an NSG accidentally opened to Internet, a database subnet that drifted public — and maps attack paths across the peered VNets. Wiz Code catches a risky NSG change in the Terraform pull request, before it ships
Runtime threat detection CrowdStrike Falcon NSGs cannot see a process running on a VM. Falcon sensors on the VMs detect a compromise inside an allowed connection and feed the security team
Monitoring & flow visibility Dynatrace / Datadog NSG flow logs show connections; Dynatrace or Datadog turn that into dashboards and alerts — which subnet talks to which, latency across the peering, and an alert when a denied-traffic spike signals an attack or a misconfiguration
Operations & change control ServiceNow A new peering or an NSG rule change is a network change with blast radius. ServiceNow gates it behind an approval and records who changed what, when
Automation & delivery Terraform + Ansible, driven by GitHub Actions / Jenkins / Argo CD Terraform defines the VNets, subnets, NSGs, and peerings; Ansible configures the OS inside the VMs; the pipeline (GitHub Actions or Jenkins, with Argo CD for any Kubernetes spoke) applies changes consistently with no manual portal clicking

A few of these deserve a sentence on why now, because a junior engineer will be asked.

Why a firewall when you already have NSGs. The auditor’s next finding will be outbound: a compromised VM in any subnet can currently reach the entire internet on port 443, which is how data gets exfiltrated and malware phones home. NSGs cannot tell a legitimate API call from a connection to an attacker’s server — both are “443 to the internet.” Put Azure Firewall (or a vendor virtual appliance) in the hub, point every spoke’s outbound traffic at it with a route, and you get URL filtering, threat-intel-based blocking, and one inspected, logged egress point. NSGs do the cheap, fast, near-the-resource filtering; the firewall does the deep, central inspection. You want both — defense in depth — not one or the other.

Why identity sits beside the network, not inside it. Network rules answer “can 10.20.2.4 reach port 1433.” They say nothing about which human is logged into that machine. Okta authenticates the workforce and federates to Microsoft Entra ID, so an admin reaching a VM through Bastion is first proven to be a real, authorized person under Conditional Access — the network controls the path, identity controls the principal, and a real platform needs both.

Why secrets never live in the network config. It is tempting to think a locked-down data subnet means the database is safe enough to use a shared password. It is not — anything inside the app subnet can read that password. HashiCorp Vault issues a short-lived, per-application credential, so even a breach of the app tier yields a secret that expires in minutes rather than a permanent key to the finance database.

Failure modes, cost, and tradeoffs

The failure modes here are mostly self-inflicted, and naming them prevents them:

Cost. The fundamentals are cheap by design. VNets, subnets, and NSGs are free — you pay for the resources inside them, not the network constructs. The meaningful line items are: VNet peering charges a small per-GB fee for data crossing the peering (in and out, and more for cross-region global peering — a reason to keep chatty workloads in the same region); Azure Bastion, Azure Firewall, and VPN/ExpressRoute gateways are billed hourly whether busy or idle, so add them when you need them, not speculatively. For a junior estimate: the starter hub-spoke with Bastion runs a modest fixed monthly cost; the firewall roughly doubles the network baseline and is the single biggest network line item, which is why teams adopt it when egress control becomes a real requirement rather than on day one.

Approach Pros Cons When to choose
Single flat VNet, NSGs only Simplest, cheapest, fastest to stand up No central control point; spokes cannot be isolated cleanly; outbound is unfiltered A genuinely small, single-workload deployment or a sandbox
Hub-spoke, NSGs, no firewall yet Clean isolation, central shared services, room to grow, low cost Outbound traffic still unfiltered; relies on NSGs alone for east-west The right starting point for most enterprises — where the logistics company lands
Hub-spoke with Azure Firewall Central inspected egress, URL filtering, threat intel, full defense in depth Higher fixed cost; routing complexity; another component to operate When outbound control, compliance, or scale demands it — the natural next step

The honest tradeoffs. Hub-spoke adds moving parts a single flat VNet does not have: peerings to manage, routing to reason about, and the discipline of a central address plan. For a five-resource hobby project that overhead is not worth it — a single VNet with good NSGs is fine. But for anything an auditor will look at, anything multi-environment, or anything that will grow, the flat network is the liability the logistics company started with, and the modest extra structure here is what lets the platform scale without a painful renumbering project two years in. Start with this hub-spoke, leave the seams for the firewall and identity and monitoring tools visible, and add each piece when its specific need shows up.

The shape of the win

The logistics company’s auditor reopens the finding a quarter later and walks the same path: the public website can no longer reach the finance database, because the data subnet’s NSG drops the packet; the development environment is a separate spoke that cannot touch production data at all; and admin access runs through Bastion under an Entra-federated Okta login instead of an RDP port open to the internet. None of that required exotic technology — it is VNets, subnets, NSGs, and peering, the four fundamentals, arranged into a small hub-spoke and laid down in Terraform so it is repeatable and reviewable. The firewall, the deeper monitoring in Dynatrace, the Vault-issued database credentials, and the Wiz posture scanning all have an obvious place to attach when their need arrives. That is the whole point of getting the foundation right: the next thing the company builds sits on defensible ground, and the flat-network blast radius that started this is gone for good.

Going deeper

The sections above are the working knowledge — enough to design and defend a starter network. This section is the mechanism underneath: the details that separate someone who uses a VNet from someone who can debug one at 2 a.m. If you are brand new, skim it now and return when the fundamentals feel comfortable.

Address space, subnet CIDR, and the five IPs Azure takes from every subnet

A VNet can hold more than one address space — you can add a second block like 10.21.0.0/16 later if the first fills up — but every subnet must be a non-overlapping sub-range that fits inside one of those blocks. The prefix length is the whole game: each +1 on the /n halves the range. A /16 is ~65,536 addresses; a /24 is 256; a /27 is 32; a /29 is 8. Azure’s smallest allowed subnet is a /29, and because of the reservation below that leaves only three usable addresses — a practical floor of /28 or larger for anything real.

The reservation is the part beginners miss. In every subnet, Azure takes five addresses, not the two you lose on a traditional on-premises subnet. For a subnet 10.20.1.0/24:

Address Reserved for
10.20.1.0 Network address (the subnet identifier itself)
10.20.1.1 The subnet’s default gateway
10.20.1.2 Azure DNS mapping (maps the platform DNS into this subnet)
10.20.1.3 Azure DNS mapping (second)
10.20.1.255 Network broadcast address (the last address in the range)

So a /24 yields 251 usable addresses, a /27 yields 27, and a /29 yields 3. Size for five fewer than the arithmetic suggests, and always leave headroom — you can grow a subnet’s prefix only if the adjacent space is free and (in most cases) the resources tolerate it, so it is far cheaper to over-provision the plan than to renumber later.

NSG rules in depth — priority, direction, defaults, service tags, ASGs, and the two-NSG evaluation

An NSG is two ordered rule lists, one inbound and one outbound, evaluated independently. Within each list, rules run lowest priority number first, and the first match wins — evaluation stops there, so a broad Deny at priority 200 is never reached if a narrow Allow at 100 already matched. Your rules occupy priorities 100–4096.

Below your rules sit Azure’s default rules, which you cannot delete (only override with a lower number). They are worth memorising:

Direction Priority Name Effect
Inbound 65000 AllowVnetInBound Allow anything sourced from VirtualNetwork (including peered VNets)
Inbound 65001 AllowAzureLoadBalancerInBound Allow the AzureLoadBalancer service tag (health probes)
Inbound 65500 DenyAllInBound Deny everything else
Outbound 65000 AllowVnetOutBound Allow anything destined for VirtualNetwork
Outbound 65001 AllowInternetOutBound Allow all outbound to Internet
Outbound 65500 DenyAllOutBound Deny everything else

Two consequences fall straight out of that table: inside a VNet, east-west traffic is allowed by default (rule 65000) — a flat VNet with no custom NSGs is wide open between resources — and outbound to the internet is open by default (65001), which is the exact gap a firewall later closes. List them live with az network nsg rule list -g rg-network-prod --nsg-name nsg-data --include-default -o table.

Stateful is the property that saves you writing half your rules. Allow an inbound flow on 443 and the return packets are permitted automatically; you never write an outbound “reply” rule. The NSG tracks the flow’s state and matches the response to the request.

Service tags are Microsoft-maintained, auto-updated groups of IP ranges you reference by name instead of hardcoding addresses: Internet, VirtualNetwork, AzureLoadBalancer, AzureCloud, Storage, Sql, AzureKeyVault, and regional variants such as Storage.EastUS or Sql.WestEurope. Microsoft updates the underlying IPs; your rule stays correct. Always prefer a service tag or a specific subnet range over *.

Application Security Groups (ASGs) let rules read like intent instead of arithmetic. Put every web NIC in a web-asg and every app NIC in an app-asg, then a rule says “web-asgapp-asg on 443” — no IP ranges, and it keeps working as you scale the tiers. ASG members must live in the same VNet.

resource "azurerm_application_security_group" "web" {
  name                = "asg-web"
  location            = "eastus"
  resource_group_name = "rg-network-prod"
}

resource "azurerm_application_security_group" "app" {
  name                = "asg-app"
  location            = "eastus"
  resource_group_name = "rg-network-prod"
}

# ...then a rule references the groups instead of CIDRs:
#   source_application_security_group_ids      = [azurerm_application_security_group.web.id]
#   destination_application_security_group_ids = [azurerm_application_security_group.app.id]

Finally, the subtlety that produces the most “but my rule allows it!” tickets: an NSG can be attached to a subnet, to a NIC, or to both, and when both exist, traffic must pass BOTH. For a packet arriving inbound to a VM, Azure evaluates the subnet NSG first, then the NIC NSG; for a packet leaving outbound, it evaluates the NIC NSG first, then the subnet NSG. It is a logical AND — the more restrictive layer wins. A subnet-level Deny at priority 200 blocks the traffic even if the NIC NSG explicitly allows it, because the packet never survives the subnet layer. When you attach an NSG to a subnet in current azurerm, you use a dedicated association resource (the inline network_security_group_id argument on azurerm_subnet was removed):

resource "azurerm_subnet_network_security_group_association" "data" {
  subnet_id                 = azurerm_subnet.data.id
  network_security_group_id = azurerm_network_security_group.data.id
}

When a rule and reality disagree, ask Azure for the effective rules rather than re-reading your Terraform: az network nic list-effective-nsg --resource-group rg-network-prod --name nic-app-01 flattens both NSGs into the actual evaluated set, and Network Watcher’s az network watcher test-ip-flow tells you exactly which rule would allow or deny a specific 5-tuple.

Peering — non-transitive, forwarded traffic, gateway transit, and reaching across regions and subscriptions

Peering carries four flags worth knowing by name, all set per side on each azurerm_virtual_network_peering:

Because peering is non-transitive, spoke-to-spoke traffic is not automatic even when both peer the hub. To make it flow you place a firewall or NVA in the hub, add a User-Defined Route on each spoke pointing the other spoke’s range at the hub appliance’s IP, and set allow_forwarded_traffic = true on the hub-side peerings — the hub becomes a router you programmed, not a magic pass-through.

resource "azurerm_virtual_network_peering" "spoke_to_hub" {
  name                         = "prod-to-hub"
  resource_group_name          = "rg-network-prod"
  virtual_network_name         = azurerm_virtual_network.prod.name
  remote_virtual_network_id    = azurerm_virtual_network.hub.id
  allow_virtual_network_access = true
  allow_forwarded_traffic      = true
  use_remote_gateways          = true   # consume the hub's gateway
}

resource "azurerm_virtual_network_peering" "hub_to_spoke" {
  name                         = "hub-to-prod"
  resource_group_name          = "rg-network-hub"
  virtual_network_name         = azurerm_virtual_network.hub.name
  remote_virtual_network_id    = azurerm_virtual_network.prod.id
  allow_virtual_network_access = true
  allow_forwarded_traffic      = true
  allow_gateway_transit        = true   # offer the hub's gateway
}

Global VNet peering connects VNets in different regions, and peering also works across subscriptions and even across Entra tenants — you just need the right RBAC (the Network Contributor role, or a custom role with the peering action) on both VNets, and you must create both peering resources, since a one-sided peering sits in an Initiated/Disconnected state until its partner exists. Data across a peering is billed both directions, and cross-region global peering costs more per GB than same-region — a concrete reason to keep chatty workloads co-located. For the hub-spoke at enterprise scale, and the Virtual WAN alternative that replaces the hand-built mesh, see the network landing zone lesson and Azure Firewall forced tunneling.

Service endpoints vs private endpoints

Both keep traffic to Azure PaaS (Storage, SQL, Key Vault) off the public internet, but they are very different tools:

Service endpoint Private endpoint (Private Link)
What it is A subnet setting that routes PaaS traffic over the Azure backbone and stamps it with your subnet identity A NIC with a private IP in your subnet that maps to one specific PaaS resource
The PaaS resource Keeps its public IP; you lock it to your subnet via the service’s own firewall Gets a private IP in your address space; public access can be disabled entirely
Reach from on-premises No — service endpoints do not extend over VPN/ExpressRoute Yes — reachable from on-prem over private connectivity
DNS No DNS change needed Requires a privatelink.* Private DNS zone or the name still resolves public
Cost Free Hourly + per-GB
Scope Whole service (e.g. all of Storage) in the region One specific resource — stronger exfiltration protection

Rule of thumb: reach for private endpoints for new designs, on-prem-reachable data paths, and anywhere data-exfiltration protection matters; service endpoints are a lighter, free lock-down for simple same-region cases. Doing private endpoints at scale is its own discipline — see private endpoints and Private DNS at scale.

The Terraform trap: inline subnet blocks vs standalone azurerm_subnet

azurerm lets you declare subnets two ways, and mixing them is the single most common networking footgun in the provider. You can nest subnet { } blocks inside the azurerm_virtual_network resource, or declare each subnet as a separate azurerm_subnet resource (as this lesson does). Use both and they fight: on every apply, the VNet resource notices subnets it does not manage inline and tries to remove them, while the standalone resources notice they are gone and recreate them — a perpetual diff, and worse, a subnet being deleted and recreated takes everything in it with it.

Pick one model and stay in it. Prefer standalone azurerm_subnet resources: they let you attach NSGs and route tables via association resources, set delegations, and manage each subnet’s lifecycle independently — which is exactly why the code above uses them and never writes an inline subnet { } block on the VNet.

UDR and route tables — overriding Azure’s default routing

Azure gives every subnet system routes for free: traffic to the local VNet, to peered VNets, to on-prem via a gateway, and a default 0.0.0.0/0 → Internet. A route table (a “User-Defined Route”, UDR) lets you override them. Each route names a destination prefix and a next hop type: VirtualAppliance (with an IP — a firewall/NVA), VirtualNetworkGateway, Internet, VnetLocal, or None (a black hole). Routing picks the longest-prefix match; on a tie, a UDR beats a BGP-learned route, which beats a system route.

The canonical use is forcing inspected egress: point 0.0.0.0/0 at the hub firewall so nothing leaves without passing through it.

resource "azurerm_route_table" "spoke_egress" {
  name                = "rt-spoke-egress"
  location            = "eastus"
  resource_group_name = "rg-network-prod"
}

resource "azurerm_route" "default_to_firewall" {
  name                   = "default-to-hub-firewall"
  resource_group_name    = "rg-network-prod"
  route_table_name       = azurerm_route_table.spoke_egress.name
  address_prefix         = "0.0.0.0/0"
  next_hop_type          = "VirtualAppliance"
  next_hop_in_ip_address = "10.10.0.4"   # the hub firewall's private IP
}

resource "azurerm_subnet_route_table_association" "app" {
  subnet_id      = azurerm_subnet.app.id
  route_table_id = azurerm_route_table.spoke_egress.id
}

When traffic goes somewhere you did not expect, ask for the effective routes on the NIC: az network nic show-effective-route-table --resource-group rg-network-prod --name nic-app-01 shows every system, peering, and user route as Azure actually applies them. Mind the asymmetric-routing trap the existing failure-modes section warns about — once a firewall is the next hop, route both directions through it or the stateful firewall drops the return half of the conversation.

DNS and the magic address 168.63.129.16

By default, VMs in a VNet resolve names through Azure-provided DNS, and the resolver they talk to lives at a special virtual IP: 168.63.129.16. This address is constant in every region and every VNet, and Azure uses it for far more than DNS — it is the platform channel for DHCP lease, the source of load-balancer health probes, and general host-to-platform communication. Practical rule: never block 168.63.129.16 in an NSG or black-hole it with a None route, or you break DNS, DHCP, and health probes for yourself. It is not reachable from outside your VNet, so there is nothing to “lock down” about it. (Do not confuse it with 169.254.169.254, the link-local Instance Metadata Service that serves VM metadata and managed-identity tokens — a different address doing a different job.)

Two extensions of the default you will meet quickly:

One NSG corollary: because health probes and some platform traffic arrive from 168.63.129.16 under the AzureLoadBalancer service tag, keep the default AllowAzureLoadBalancerInBound rule (or an explicit equivalent) in place on subnets behind a load balancer, or your healthy instances start failing their probes.

Practice challenges

Work these top to bottom; they escalate from beginner to advanced. Try each before opening the solution.

1. (Beginner) Count the usable addresses. You carve a /27 subnet at 10.20.5.0/27 for a small app tier. How many usable IPs do you get, and which specific addresses can you not assign?

<details><summary>Solution</summary>

A /27 is 32 addresses; Azure reserves 5, leaving 27 usable. The unusable ones are 10.20.5.0 (network), 10.20.5.1 (default gateway), 10.20.5.2 and 10.20.5.3 (Azure DNS mapping), and 10.20.5.31 (broadcast). Why: every Azure subnet loses five addresses, not the two you lose on-prem — forgetting this is how a subnet you sized “exactly right” runs out early. </details>

2. (Beginner) Allow only HTTPS inbound to the web subnet. Write the NSG rule (portal fields or az) that lets the internet reach the web tier on 443 and nothing else, and say why you do not need a matching outbound rule.

<details><summary>Solution</summary>

az network nsg rule create -g rg-network-prod --nsg-name nsg-web \
  -n Allow-HTTPS-Inbound --priority 100 --direction Inbound --access Allow \
  --protocol Tcp --source-address-prefixes Internet \
  --destination-port-ranges 443 --destination-address-prefixes '*'

Everything else inbound is already dropped by the default DenyAllInBound (65500). Why: NSGs are stateful — the return traffic for an allowed inbound 443 flow is permitted automatically, so no outbound “reply” rule is needed. Using the Internet service tag as the source is cleaner than a CIDR list. </details>

3. (Intermediate) The rule that “should” work. A VM’s NIC NSG allows SSH (22) from your office IP, but you still cannot connect. The subnet NSG has only the default rules plus one custom rule: Deny-All-Inbound at priority 200. What is happening, and how do you fix it without opening the subnet wide?

<details><summary>Solution</summary>

Both NSGs are evaluated and traffic must pass both; for inbound, the subnet NSG is evaluated first, and its priority-200 Deny blocks 22 before the NIC’s allow is ever consulted at the subnet layer. Fix: add an Allow-SSH-From-Office rule at a lower priority number (e.g. 150) on the subnet NSG as well, scoped to your office IP. Why: subnet-plus-NIC evaluation is a logical AND — the more restrictive layer wins, so an allow must exist at every layer the packet crosses. </details>

4. (Intermediate) Make two spokes talk. Spoke A (10.20.0.0/16) and Spoke B (10.30.0.0/16) both peer the hub (10.10.0.0/16), but A cannot reach B. Give two different ways to fix it and the tradeoff between them.

<details><summary>Solution</summary>

(a) Direct peering between A and B — simplest, lowest latency, but the number of peerings grows combinatorially and skips central inspection. (b) Route through the hub firewall/NVA: add a UDR on A sending 10.30.0.0/16 → hubFirewallIP (and the mirror on B), set allow_forwarded_traffic = true on the hub-side peerings, and let the appliance forward. Why: peering is non-transitive — A↔hub and B↔hub never imply A↔B; the hub only routes between spokes if you program it to. </details>

5. (Advanced) The private endpoint that still resolves public. You created a private endpoint for a storage account, disabled its public access, and now the app VM cannot reach the blob URL — a DNS lookup returns a public IP. Diagnose and fix.

<details><summary>Solution</summary>

The privatelink.blob.core.windows.net Private DNS zone is not linked to the VNet (or the endpoint’s A record was never registered), so resolution falls through the public CNAME chain and hands back the public IP — which now refuses the connection. Fix: create/link the privatelink.blob.core.windows.net zone to the VNet and register the private endpoint’s record (the portal does this automatically if you let it during endpoint creation). Why: a private endpoint is half networking and half DNS; without the private zone the private IP exists but nothing resolves to it. </details>

6. (Advanced) The subnets that will not stop churning. In Terraform, your azurerm_subnet resources are destroyed and recreated on every apply, even though no one edited them. terraform plan shows the subnets being removed and added each time. Diagnose the root cause and give the fix.

<details><summary>Solution</summary>

The parent azurerm_virtual_network also declares inline subnet { } blocks. The two models fight: the VNet resource removes subnets it does not see inline, while the standalone azurerm_subnet resources recreate them — forever. Fix: remove the inline subnet { } blocks and manage every subnet as a standalone azurerm_subnet resource (or, less commonly, do the reverse — but never both). Why: the inline-vs-standalone subnet conflict is the classic azurerm footgun, and it can take live resources down with each recreate. </details>

Common beginner mistakes

The Failure modes section above lists operational traps. These are conceptual ones — the wrong mental model a beginner carries in, and the right one to replace it with.

Glossary

AzureNetworkingVNetNSGHub-SpokeFundamentals
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments