Networking Deep Dive
VNet design, NSGs, and when you actually need a hub
Networking Deep Dive
See also: Architecture Decisions for details on why this layout was chosen.
Do You Even Need a VNet?
Seriously. If your entire stack is:
- Azure Container Apps or App Service (with built-in auth)
- Azure SQL or Cosmos DB (with service-level firewall)
- Azure Storage (with service-level firewall)
- Azure Cache for Redis (with access keys + TLS)
Then you can run production without a VNet. Use service-level firewalls to restrict access to your app’s outbound IPs and your team’s office/VPN IPs.
Move to VNets when:
- You need Private Endpoints (usually when an enterprise customer’s security questionnaire asks “is your database on a private network?”)
- You’re running AKS (requires a VNet)
- You need VNet integration for App Service/Container Apps (to reach private resources)
- Compliance mandates network-level isolation
VNet Design
Address Space
| VNet | CIDR | Available IPs |
|---|---|---|
vnet-<co>-prod |
10.0.0.0/16 |
65,536 |
vnet-<co>-nonprod |
10.1.0.0/16 |
65,536 |
Use 10.x.0.0/16 ranges. They’re private (RFC 1918), don’t conflict with most corporate networks, and give you room to grow. Do not use 172.16.0.0/12 or 192.168.0.0/16 — they’re more likely to conflict if you ever need VPN to an office network.
Subnet Layout
vnet-<co>-prod 10.0.0.0/16
│
├── snet-aks 10.0.0.0/20 (4,091 usable IPs)
│ └── For AKS nodes + Azure CNI pods (pod IPs come from the subnet)
│
├── snet-app 10.0.16.0/22 (1,019 usable IPs)
│ └── App Service / Container Apps VNet integration
│
├── snet-data 10.0.20.0/22 (1,019 usable IPs)
│ └── Private Endpoints for SQL, Redis, Storage, Key Vault
│
└── snet-shared 10.0.24.0/24 (251 usable IPs)
└── CI/CD agents, jump boxes, Bastion subnet
Why /20 for AKS? With Azure CNI, pods consume IPs from the subnet. A modest cluster with 10 nodes and 30 pods each is ~300 pod IPs. Add headroom for upgrades (surge nodes), system components, and future node pools. A /20 gives you ~4k usable IPs (Azure reserves 5 per subnet), which is a solid default for most startup-scale clusters. If you expect a much larger cluster (high node count, multiple pools, high max-pods), bump AKS to /19 or /18.
Why /22 for data? Each Private Endpoint uses one IP. You’ll have 5-20 Private Endpoints in a typical startup. /22 gives you 1,019 usable IPs — way more than you need, but subnets are free and re-sizing them later is painful.
Subnet Delegation
Some subnets need delegation to specific Azure services:
| Subnet | Delegation | Notes |
|---|---|---|
snet-app |
Microsoft.Web/serverFarms |
Required for App Service VNet integration |
snet-aks |
None | AKS manages its own NICs |
snet-data |
None | Private Endpoints don’t require delegation |
snet-shared |
None | General purpose |
If using Container Apps with VNet integration, delegate to Microsoft.App/environments instead.
Network Security Groups (NSGs)
Default Rules
Every subnet gets an NSG with these custom rules:
Priority Direction Action Source Destination Port
4096 Inbound Deny * * *
Yes, deny everything inbound by default. Then add explicit allow rules for what you need.
Common Allow Rules
For AKS subnet:
110 Inbound Allow AzureLoadBalancer snet-aks * (health probes)
120 Inbound Allow Internet snet-aks 80,443 (if using LoadBalancer service)
For App Service subnet (VNet integration — outbound only):
No inbound rules needed — VNet integration is outbound only.
For data subnet:
110 Inbound Allow snet-aks snet-data 1433,5432,6380,443 (SQL, PostgreSQL, Redis, Storage)
120 Inbound Allow snet-app snet-data 1433,5432,6380,443
For shared subnet:
110 Inbound Allow VirtualNetwork snet-shared 22 (SSH to jump box, if needed)
NSG Flow Logs
Enable NSG Flow Logs version 2 on all NSGs and send them to your Log Analytics workspace. This gives you:
- Network traffic visibility
- Connection troubleshooting
- Traffic analytics (optional, costs extra)
// Illustrative — requires a Storage Account and Log Analytics Workspace
// to be deployed separately for flow log storage and analytics.
param nsgName string
param nsgId string
param storageAccountId string
param logAnalyticsWorkspaceId string
resource flowLog 'Microsoft.Network/networkWatchers/flowLogs@2023-11-01' = {
name: 'nw-${location}/fl-${nsgName}'
location: location
properties: {
targetResourceId: nsgId
storageId: storageAccountId
enabled: true
format: { type: 'JSON', version: 2 }
retentionPolicy: { days: 30, enabled: true }
flowAnalyticsConfiguration: {
networkWatcherFlowAnalyticsConfiguration: {
enabled: false // enable later if you want Traffic Analytics
workspaceResourceId: logAnalyticsWorkspaceId
trafficAnalyticsInterval: 60
}
}
}
}
Private Endpoint Warning
Important: Setting
publicNetworkAccess: 'Disabled'on services like Azure SQL or Redis requires Private Endpoints or VNet integration to be configured. Without them, all connectivity (including from your application) will be blocked.If you’re not deploying Private Endpoints yet, keep
publicNetworkAccess: 'Enabled'and use service-level firewall rules to restrict access to known IPs. Seedeploy_private_endpointsin the SaaS example for an opt-in Private Endpoint setup.
DNS
Default (No Custom DNS)
By default, Azure VNets use Azure-provided DNS (168.63.129.16). This resolves:
- Public DNS names normally
- Azure Private DNS Zones linked to the VNet
This is fine for most startups. Don’t set up custom DNS unless you need it.
Private DNS Zones
When you create Private Endpoints, you need Private DNS Zones so yourdb.database.windows.net resolves to the private IP instead of the public one.
Common Private DNS Zones:
| Service | Zone |
|---|---|
| Azure SQL | privatelink.database.windows.net |
| Cosmos DB | privatelink.documents.azure.com |
| Storage (blob) | privatelink.blob.core.windows.net |
| Key Vault | privatelink.vaultcore.azure.net |
| Redis | privatelink.redis.cache.windows.net |
| ACR | privatelink.azurecr.io |
Link each zone to both VNets (prod and nonprod) if resources in both need to resolve them.
When to Centralize DNS
When you have 5+ Private DNS Zones and 3+ VNets, managing zone links becomes tedious. That’s when you create a hub VNet with a DNS forwarder (or use Azure DNS Private Resolver) and point all VNets to it.
When to Add a Hub
Before (what we deploy):
vnet-<co>-prod ←──── (no connection) ────→ vnet-<co>-nonprod
After (when you graduate):
vnet-<co>-prod ←── peering ──→ hub-vnet ←── peering ──→ vnet-<co>-nonprod
│
Azure Firewall
VPN Gateway
DNS Private Resolver
Azure Bastion
Triggers for adding a hub:
- You need VPN or ExpressRoute (gateway goes in the hub)
- Compliance requires centralized egress filtering (firewall goes in the hub)
- You have 3+ VNets that need to communicate
- DNS management across VNets is becoming painful
Hub is NOT needed for:
- Internet-facing apps behind Application Gateway or Front Door
- PaaS-only architectures
- Two VNets that don’t need to talk to each other
Azure Firewall vs NSGs
| Feature | NSGs | Azure Firewall |
|---|---|---|
| Cost | Free | ~$900/month minimum |
| L3/L4 filtering | Yes | Yes |
| L7 (FQDN) filtering | No | Yes |
| TLS inspection | No | Yes (Premium) |
| Centralized logging | Via Flow Logs | Built-in |
| Threat intelligence | No | Yes |
| Good for startups? | Yes | Not until compliance or hybrid demands it |
Start with NSGs. They’re free, they cover 95% of use cases, and they’re per-subnet so failures are isolated. Add Azure Firewall when you have a specific compliance or operational requirement.
See also: The SaaS Startup example demonstrates opt-in Private Endpoints for SQL and Redis with DNS zone configuration.
Front Door vs Application Gateway
| Feature | Azure Front Door | Application Gateway |
|---|---|---|
| Scope | Global (anycast) | Regional |
| WAF | Yes (Standard/Premium) | Yes (v2) |
| SSL offload | Yes | Yes |
| Caching / CDN | Yes (built-in) | No |
| Best for | Global apps, multi-region, CDN | Single-region apps, internal LBs |
| Starting cost | ~$35/month (Standard) | ~$175/month (v2) |
Recommendation: If your app is internet-facing and you want WAF, start with Front Door Standard. It’s cheaper, global, and includes CDN. Use Application Gateway only if you need it inside a VNet for internal routing.