
Devops Network Calculator For Azure
- 56 installs
- 6 repo stars
- Updated July 22, 2026
- julianobarbosa/claude-code-skills
Work with Azure cloud infrastructure and DevOps automation.
About
Azure Network Calculator. Offline Azure network planning tool that calculates CIDRs, detects overlaps, analyzes VNet utilization, plans AKS networking.
- 5 reserved IPs per subnet
- Bastion min /26, Gateway min /27, Firewall min /26
Devops Network Calculator For Azure by the numbers
- 56 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #685 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/julianobarbosa/claude-code-skills --skill devops-network-calculator-for-azureAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 56 |
|---|---|
| repo stars | ★ 6 |
| Last updated | July 22, 2026 |
| Repository | julianobarbosa/claude-code-skills ↗ |
What it does
Work with Azure cloud infrastructure and DevOps automation.
Files
Azure Network Calculator
Offline Azure network planning tool. Calculates CIDRs, detects overlaps, analyzes VNet utilization, plans AKS networking, and generates Terraform-ready output. Zero external dependencies — uses Python stdlib only.
Quick Start
# What CIDR do I need for 500 hosts?
python3 scripts/network-calc.py calculate --from-hosts 500
# Analyze current VNet
python3 scripts/network-calc.py analyze --from-tfvars terraform/terraform.tfvars
# Validate for overlaps (pre-commit compatible)
python3 scripts/network-calc.py validate --from-tfvars terraform/terraform.tfvars
# Find where to place a new subnet
python3 scripts/network-calc.py first-fit --vnet 10.248.0.0/20 \
--subnets "10.248.0.0/22,10.248.4.0/22,10.248.8.0/26,10.248.9.0/24" --hosts 500Commands
| Command | Purpose | Reference |
|---|---|---|
calculate | CIDR info, host sizing, subnet splitting | CIDR Guide |
analyze | VNet utilization, gap analysis | CIDR Guide |
validate | Overlap detection, Azure constraint checks | Azure Constraints |
first-fit | Find optimal placement for new subnet | CIDR Guide |
plan-multi | Multi-environment VNet allocation | Segmentation |
Project Context
This project's current VNet: 10.248.0.0/20 (4,096 IPs, 57.8% utilized)
| Subnet | CIDR | Usable |
|---|---|---|
| GatewaySubnet | 10.248.0.0/22 | 1,019 |
| PublicSubnet | 10.248.4.0/22 | 1,019 |
| AzureBastionSubnet | 10.248.8.0/26 | 59 |
| PrivateSubnet | 10.248.9.0/24 | 251 |
| Available gaps | 1,708 |
Key files: terraform/terraform.tfvars, terraform/networking.tf, terraform/nsg.tf
Azure Quick Reference
- 5 reserved IPs per subnet (.0, .1, .2, .3, broadcast)
- Bastion: min /26 | Gateway: min /27 | Firewall: min /26
- Max subnets/VNet: 3,000 | Max NSG rules: 1,000
- Full reference: Azure Constraints
Reference Guides
| Guide | When to Read |
|---|---|
| CIDR Calculation Guide | Subnet sizing, gap analysis, overlap detection |
| AKS Networking Guide | CNI comparison, pod/service CIDR, node sizing |
| Segmentation Patterns | Design patterns, anti-patterns, decision matrices |
| Azure Constraints | Hard limits, naming rules, reserved addresses |
Templates
| Template | Purpose |
|---|---|
| VNet Layout | Terraform variable blocks for VNet config |
| AKS NSG Rules | NSG rules for AKS workloads |
| Multi-Env Plan | Multi-environment planning output |
Execution
Follow the instructions in ./workflow.md.
---
Gotchas
- Azure reserves 5 IPs per subnet, not 2: A standard /29 gives you 3 usable IPs, not 6. Sizing AKS node pools with /29 starves scaling and surfaces as
InsufficientFreeAddressesInSubnetmid-scale. - GatewaySubnet name is literal: Azure refuses VPN Gateway attach unless the subnet is named exactly
GatewaySubnet. Same forAzureBastionSubnetandAzureFirewallSubnet. Typo breaks at apply, not plan. - AKS Azure CNI eats one subnet IP per pod: A 30-node cluster at 30 pods/node needs 900+ subnet IPs, not 30. Plan
--max-podsfirst; switching CNI later requires cluster rebuild. - first-fit picks lowest gap, not smartest: Places a /24 in a /22 gap and leaves fragmented unusable /25 leftovers. For long-lived VNets, use
plan-multiand reserve growth gaps explicitly. - CIDR overlap with on-prem fails silently until peering: A 10.0.0.0/16 VNet looks fine alone, but ExpressRoute peering black-holes routes overlapping corporate ranges. Validate against full corporate IPAM.
- NSG rule limit is 1,000 total, not per-direction: Inbound + outbound + default + Azure-auto rules share the budget. Leave headroom or hit a hard wall during scale.
AKS Networking Guide
Comprehensive reference for Azure Kubernetes Service networking models, subnet sizing, and integration with Azure VNets.
CNI Comparison Matrix
| Feature | Azure CNI | Kubenet | CNI Overlay | CNI + Cilium |
|---|---|---|---|---|
| Pod IP source | VNet subnet | Private bridge (NAT) | Overlay network | Overlay network |
| Pod IPs routable in VNet | Yes | No (requires UDR) | No | No |
| Min node subnet | /24 recommended | /24 recommended | /27 minimum | /27 minimum |
| IP consumption per node | node + (max_pods * nodes) | 1 IP per node | 1 IP per node | 1 IP per node |
| Max pods per node (default) | 30 | 110 | 250 | 250 |
| Max pods per node (max) | 250 | 110 | 250 | 250 |
| Network Policy support | Azure + Calico | Calico only | Azure + Calico | Cilium (native) |
| Windows node pools | Yes | No | Yes | No |
| Service mesh integration | External (Istio, etc.) | External | External | Native (Cilium) |
| eBPF dataplane | No | No | No | Yes |
| Performance overhead | Lowest (native VNet) | Moderate (NAT) | Low (VXLAN) | Lowest (eBPF) |
| DNS resolution | Azure DNS | Azure DNS | Azure DNS | Azure DNS |
| Dual-stack (IPv4/IPv6) | Yes | No | Yes | Yes |
| Private endpoint access | Direct (same VNet) | Via UDR | Via UDR or NAT | Via UDR or NAT |
| Complexity | Medium | Low | Low | Medium |
| Recommended for | Large clusters needing VNet-routed pods | Dev/test, small clusters | Most production workloads | Advanced networking, observability |
Node Subnet Sizing Formulas
Azure CNI (VNet-Allocated Pod IPs)
Every pod gets a real VNet IP. This is the most IP-hungry model.
required_IPs = (max_pods_per_node + 1) * node_count + reserved(5)Default max_pods_per_node = 30, so:
required_IPs = 31 * node_count + 5| Nodes | Required IPs | Min Prefix | Usable IPs |
|---|---|---|---|
| 10 | 315 | /23 (512) | 507 |
| 50 | 1,555 | /21 (2,048) | 2,043 |
| 100 | 3,105 | /20 (4,096) | 4,091 |
| 250 | 7,755 | /19 (8,192) | 8,187 |
Kubenet (NAT Bridge)
Only node IPs come from the VNet. Pods use a private 10.244.0.0/16 overlay.
required_IPs = node_count + 5| Nodes | Required IPs | Min Prefix | Usable IPs |
|---|---|---|---|
| 10 | 15 | /28 (16) | 11 |
| 50 | 55 | /26 (64) | 59 |
| 100 | 105 | /25 (128) | 123 |
| 400 | 405 | /23 (512) | 507 |
CNI Overlay / CNI + Cilium
Like Kubenet for node IPs -- only nodes consume VNet addresses. Pods get overlay IPs.
required_IPs = node_count + 5Same table as Kubenet, but with higher max_pods_per_node (250 vs 110) and no UDR requirement.
| Nodes | Required IPs | Min Prefix | Usable IPs |
|---|---|---|---|
| 10 | 15 | /28 (16) | 11 |
| 50 | 55 | /26 (64) | 59 |
| 100 | 105 | /25 (128) | 123 |
| 500 | 505 | /23 (512) | 507 |
Pod CIDR Planning
The pod CIDR defines the address space for pod IPs in overlay models (Kubenet, CNI Overlay, CNI + Cilium).
Defaults
| Parameter | Default Value |
|---|---|
| Pod CIDR | 10.244.0.0/16 (65,536 IPs) |
| Per-node pod CIDR | /24 (256 IPs per node, drawn from pod CIDR) |
Sizing the Pod CIDR
pod_cidr_size = nodes * (2^(32 - per_node_prefix))With default /24 per node:
- 256 nodes need 256 * 256 = 65,536 IPs = /16
- 512 nodes need a /15
- 1,000 nodes need a /14
Overlap Rules
- Pod CIDR must not overlap with:
- VNet address space (e.g., 10.248.0.0/20)
- Service CIDR
- Docker bridge CIDR (172.17.0.0/16 by default)
- Any peered VNet address space
- Pod CIDR can overlap with:
- Pod CIDRs in other, non-peered clusters (they are isolated)
- The default 10.244.0.0/16 is safe for this project's 10.248.0.0/20 VNet (no overlap)
Service CIDR Planning
The service CIDR provides ClusterIP addresses for Kubernetes services.
Defaults
| Parameter | Default Value |
|---|---|
| Service CIDR | 10.0.0.0/16 |
| DNS Service IP | 10.0.0.10 (must be within service CIDR) |
Rules
- Service CIDR must not overlap with VNet address space, pod CIDR, or peered networks
- DNS service IP must be within the service CIDR but not the first IP (network address)
- By convention, DNS IP is at the
.10offset (e.g., 10.0.0.10 for 10.0.0.0/16) - A /16 supports 65,531 services (far exceeding most cluster needs)
- For smaller clusters, /20 (4,091 services) is sufficient
Recommended Values for This Project
| Parameter | Value | Rationale |
|---|---|---|
| VNet | 10.248.0.0/20 | Project allocation |
| Pod CIDR | 10.244.0.0/16 | Default, no overlap with VNet |
| Service CIDR | 10.245.0.0/16 | Adjacent to pod CIDR, no overlap |
| DNS Service IP | 10.245.0.10 | Convention: .10 offset |
Private AKS Cluster Requirements
A private AKS cluster exposes the API server only via a private endpoint inside the VNet.
Mandatory Components
1. Private DNS Zone for the API server
- Zone name:
privatelink.<region>.azmk8s.io - Must be linked to the VNet where the cluster lives
- Must be linked to any VNet that needs to resolve the API server (e.g., CI/CD agents)
2. VNet Link from the private DNS zone to the cluster VNet
- Auto-registration not required (AKS manages the A record)
- Additional VNet links needed for hub VNets in hub-spoke topologies
3. No Public API Endpoint
public_network_access_enabled = false- API server accessible only from within the VNet or via peered/VPN-connected networks
- CI/CD pipelines must run from within the network (self-hosted agents, Bastion, VPN)
Network Flow for Private AKS
Developer/CI Agent
└─> VPN / Bastion / Private Network
└─> Private Endpoint (10.248.x.x)
└─> AKS API Server
└─> Node Pool (VNet subnet)
└─> Pods (overlay or VNet IPs)Terraform Configuration Pattern
resource "azurerm_kubernetes_cluster" "aks" {
private_cluster_enabled = true
public_network_access_enabled = false
private_dns_zone_id = azurerm_private_dns_zone.aks.id
network_profile {
network_plugin = "azure" # or "none" for CNI Overlay
network_plugin_mode = "overlay" # for CNI Overlay
pod_cidr = "10.244.0.0/16"
service_cidr = "10.245.0.0/16"
dns_service_ip = "10.245.0.10"
}
}AKS-Specific NSG Rules
Required Outbound Rules
| Priority | Name | Destination | Port | Protocol | Purpose |
|---|---|---|---|---|---|
| 100 | AllowAzureMonitor | AzureMonitor | 443 | TCP | Metrics and logs |
| 110 | AllowMCR | MicrosoftContainerRegistry | 443 | TCP | Pull system images |
| 120 | AllowACR | AzureContainerRegistry | 443 | TCP | Pull application images |
| 130 | AllowAzureAD | AzureActiveDirectory | 443 | TCP | Authentication |
| 140 | AllowNTP | * | 123 | UDP | Time synchronization |
| 150 | AllowAzureCloud | AzureCloud | 443 | TCP | Azure platform services |
| 160 | AllowDNS | * | 53 | TCP/UDP | DNS resolution |
Required Inbound Rules
| Priority | Name | Source | Port | Protocol | Purpose |
|---|---|---|---|---|---|
| 100 | AllowLBProbes | AzureLoadBalancer | * | * | Health probes |
| 110 | AllowVNetInbound | VirtualNetwork | * | * | Intra-VNet communication |
Service Tags for AKS
| Service Tag | Purpose | When Required |
|---|---|---|
| AzureCloud | All Azure platform traffic | Always |
| AzureContainerRegistry | ACR image pulls | When using ACR |
| MicrosoftContainerRegistry | MCR system image pulls | Always |
| AzureMonitor | Monitoring and diagnostics | Always (for Container Insights) |
| AzureActiveDirectory | AAD authentication | Always |
| AzureLoadBalancer | Load balancer health probes | Always |
| VirtualNetwork | Intra-VNet traffic | Always |
| AzureKeyVault | Key Vault access | When using CSI secret store |
Integration with This Project (10.248.0.0/20)
Available Space for AKS
Based on current allocation analysis:
VNet: 10.248.0.0/20 (4,096 IPs total)
Currently allocated:
GatewaySubnet: 10.248.0.0/22 (1,024 IPs)
PublicSubnet: 10.248.4.0/22 (1,024 IPs)
AzureBastionSubnet: 10.248.8.0/26 (64 IPs)
PrivateSubnet: 10.248.9.0/24 (256 IPs)
Best fit for AKS: 10.248.10.0/23 (512 IPs, 507 usable)What 10.248.10.0/23 Supports
| CNI Model | Max Nodes | Max Pods (total) | Notes |
|---|---|---|---|
| Azure CNI (30 pods/node) | 16 | 480 | IP-hungry: 31 IPs per node |
| Azure CNI (10 pods/node) | 46 | 460 | Reduced pod density |
| Kubenet | 507 | 55,770 | Only node IPs from VNet |
| CNI Overlay | 507 | 126,750 | Best density, overlay pods |
| CNI + Cilium | 507 | 126,750 | Same density + eBPF benefits |
Recommendation for This Project
- CNI Overlay at 10.248.10.0/23 supports up to 507 nodes with 250 pods each
- Pod CIDR: 10.244.0.0/16 (no overlap with 10.248.0.0/20)
- Service CIDR: 10.245.0.0/16 (no overlap)
- DNS Service IP: 10.245.0.10
Sizing Examples
Small Cluster (10 Nodes)
| CNI Model | Node Subnet | Pod CIDR | Service CIDR | Total VNet IPs |
|---|---|---|---|---|
| Azure CNI | /26 (64 IPs) | N/A (VNet) | 10.245.0.0/20 | 315 (node+pod) |
| Kubenet | /28 (16 IPs) | 10.244.0.0/20 | 10.245.0.0/20 | 15 |
| CNI Overlay | /28 (16 IPs) | 10.244.0.0/20 | 10.245.0.0/20 | 15 |
| CNI + Cilium | /28 (16 IPs) | 10.244.0.0/20 | 10.245.0.0/20 | 15 |
Medium Cluster (50 Nodes)
| CNI Model | Node Subnet | Pod CIDR | Service CIDR | Total VNet IPs |
|---|---|---|---|---|
| Azure CNI | /21 (2,048 IPs) | N/A (VNet) | 10.245.0.0/18 | 1,555 |
| Kubenet | /26 (64 IPs) | 10.244.0.0/18 | 10.245.0.0/20 | 55 |
| CNI Overlay | /26 (64 IPs) | 10.244.0.0/18 | 10.245.0.0/20 | 55 |
| CNI + Cilium | /26 (64 IPs) | 10.244.0.0/18 | 10.245.0.0/20 | 55 |
Large Cluster (100 Nodes)
| CNI Model | Node Subnet | Pod CIDR | Service CIDR | Total VNet IPs |
|---|---|---|---|---|
| Azure CNI | /20 (4,096 IPs) | N/A (VNet) | 10.245.0.0/17 | 3,105 |
| Kubenet | /25 (128 IPs) | 10.244.0.0/17 | 10.245.0.0/20 | 105 |
| CNI Overlay | /25 (128 IPs) | 10.244.0.0/17 | 10.245.0.0/20 | 105 |
| CNI + Cilium | /25 (128 IPs) | 10.244.0.0/17 | 10.245.0.0/20 | 105 |
AKS Anti-Patterns
1. Using Azure CNI When You Do Not Need VNet-Routable Pods
Azure CNI consumes one VNet IP per pod. A 100-node cluster with 30 pods/node needs 3,105 IPs. CNI Overlay needs 105. Unless you require pods to be directly addressable from the VNet (e.g., legacy integration), use CNI Overlay.
2. Undersizing the Node Subnet
Leaving no room for node pool scaling causes autoscaler failures. Always size for 2x expected peak nodes.
3. Overlapping Pod CIDR with VNet or Peered Networks
The cluster will fail to deploy or pods will have unreachable IPs. Validate all CIDRs with network-calc.py validate before applying.
4. Using Kubenet for Production
Kubenet requires manual UDR management, has lower max pod counts (110), does not support Windows nodes, and lacks dual-stack. Use CNI Overlay instead.
5. Public API Server in Production
Exposing the API server to the internet is a critical security risk. Always use private clusters for production.
6. Hardcoding Service Tag IPs
Azure service tag IP ranges change. Always use service tags (e.g., AzureCloud) in NSG rules rather than hardcoded IP addresses.
7. Ignoring Cluster DNS Resolution
Private clusters need DNS resolution for the API server. Without a properly linked private DNS zone, kubectl commands fail from outside the cluster VNet.
8. Single Subnet for Multiple Node Pools
Using one subnet for system and user node pools prevents independent NSG rules and scaling. Use separate subnets for different pool types when security isolation is required.
9. No Egress Control
Allowing unrestricted outbound from AKS nodes is a data exfiltration risk. Use NAT Gateway for controlled egress and NSG rules with service tags to restrict destinations.
10. Skipping Network Policy
Running without network policy means any pod can reach any other pod. Enable Calico or Cilium network policy from day one -- retrofitting is painful.
Azure Network Constraints Reference
Quick reference for Azure networking hard limits, reserved addresses, and naming rules.
Reserved IP Addresses Per Subnet
Azure reserves 5 IP addresses in every subnet:
| Address | Purpose |
|---|---|
| x.x.x.0 | Network address |
| x.x.x.1 | Default gateway |
| x.x.x.2 | Azure DNS mapping |
| x.x.x.3 | Azure DNS mapping |
| x.x.x.255 | Broadcast (last address) |
Formula: usable_hosts = 2^(32 - prefix_length) - 5
Usable Hosts by Prefix Length
| Prefix | Total IPs | Usable (Azure) | Typical Use |
|---|---|---|---|
| /16 | 65,536 | 65,531 | Large VNet |
| /17 | 32,768 | 32,763 | Multi-workload VNet |
| /18 | 16,384 | 16,379 | Medium VNet |
| /19 | 8,192 | 8,187 | Large subnet |
| /20 | 4,096 | 4,091 | Standard VNet (this project) |
| /21 | 2,048 | 2,043 | Large workload subnet |
| /22 | 1,024 | 1,019 | AKS nodes (Azure CNI) |
| /23 | 512 | 507 | AKS nodes (CNI Overlay) |
| /24 | 256 | 251 | Standard workload subnet |
| /25 | 128 | 123 | Small workload |
| /26 | 64 | 59 | Min: Bastion, Firewall |
| /27 | 32 | 27 | Min: Gateway, RouteServer |
| /28 | 16 | 11 | Minimal workload |
| /29 | 8 | 3 | Azure minimum subnet |
Minimum Subnet Sizes by Purpose
| Subnet Name | Min Prefix | Min IPs | Required Name |
|---|---|---|---|
| AzureBastionSubnet | /26 | 64 | Exact (mandatory) |
| GatewaySubnet | /27 (/26 for ExpressRoute) | 32 | Exact (mandatory) |
| AzureFirewallSubnet | /26 | 64 | Exact (mandatory) |
| AzureFirewallManagementSubnet | /26 | 64 | Exact (mandatory) |
| RouteServerSubnet | /27 | 32 | Exact (mandatory) |
| AKS node subnet (Azure CNI) | /24 recommended | 256 | User-defined |
| AKS node subnet (CNI Overlay) | /27 minimum | 32 | User-defined |
| General purpose | /29 minimum | 8 | User-defined |
Azure Resource Limits
| Resource | Limit |
|---|---|
| Subnets per VNet | 3,000 |
| VNets per subscription per region | 1,000 |
| NSG rules per NSG | 1,000 |
| Address spaces per VNet | 500 |
| VNet peerings per VNet | 500 |
| Route tables per subscription | 200 |
| Routes per route table | 400 |
| DNS servers per VNet | 25 |
| Private endpoints per VNet | 1,000 |
| Service endpoints per subnet | 25 (service limit) |
| Network interfaces per VNet | 65,536 |
| Public IPs per subscription | 1,000 (Standard) |
Naming Constraints
| Rule | Details |
|---|---|
| Mandatory names | AzureBastionSubnet, GatewaySubnet, AzureFirewallSubnet must use exact names |
| Subnet name length | 1-80 characters |
| Allowed characters | Alphanumeric, hyphen, underscore, period |
| VNet name length | 2-64 characters |
| NSG name length | 1-80 characters |
VNet Peering Constraints
- Address spaces of peered VNets must not overlap
- Peering is non-transitive (A peers B, B peers C does not mean A reaches C without NVA/firewall)
- Hub-spoke requires UDRs or Azure Firewall for spoke-to-spoke traffic
- Maximum 500 peerings per VNet
Private DNS Zone Limits
- 25 linked VNets per private DNS zone (with auto-registration)
- 1,000 linked VNets per private DNS zone (without auto-registration)
- Private AKS clusters require a private DNS zone for the API server
CIDR Calculation Guide
Comprehensive reference for IP address planning in Azure environments.
CIDR Notation Fundamentals
A CIDR block 10.248.0.0/20 means:
- Network address: 10.248.0.0
- Prefix length: /20 (20 bits for network, 12 bits for hosts)
- Total addresses: 2^12 = 4,096
- Subnet mask: 255.255.240.0
- Broadcast: 10.248.15.255
- Usable range: 10.248.0.1 - 10.248.15.254
Subnet Sizing Formula
Given N hosts needed:
min_prefix = 32 - ceil(log2(N + 5))The +5 accounts for Azure's 5 reserved addresses per subnet.
Growth planning: Always plan for 2x current need. If you need 100 hosts today, plan for 200 (use /24 = 251 usable, not /25 = 123 usable).
Examples
| Hosts Needed | Add Azure Reserved | Total | Min Prefix | Usable Hosts |
|---|---|---|---|---|
| 10 | +5 = 15 | 16 | /28 | 11 |
| 30 | +5 = 35 | 64 | /26 | 59 |
| 100 | +5 = 105 | 128 | /25 | 123 |
| 250 | +5 = 255 | 256 | /24 | 251 |
| 500 | +5 = 505 | 512 | /23 | 507 |
| 1000 | +5 = 1005 | 1024 | /22 | 1,019 |
Gap Analysis
To find unallocated space within a VNet: 1. List all subnet CIDRs 2. Sort by network address (ascending) 3. Check for gaps between each subnet's broadcast+1 and the next subnet's network address 4. Report gaps as valid CIDR blocks using summarize_address_range()
Real Example: This Project (10.248.0.0/20)
VNet: 10.248.0.0/20 (4,096 IPs)
Allocated:
GatewaySubnet: 10.248.0.0/22 (1,024 IPs)
PublicSubnet: 10.248.4.0/22 (1,024 IPs)
AzureBastionSubnet: 10.248.8.0/26 (64 IPs)
PrivateSubnet: 10.248.9.0/24 (256 IPs)
─────────────────────────────────────────────
Total allocated: 2,368 IPs (57.8%)
Gaps (unallocated):
Gap 1: 10.248.8.64 - 10.248.8.255 (192 IPs)
= 10.248.8.64/26 + 10.248.8.128/25
Gap 2: 10.248.10.0 - 10.248.15.255 (1,536 IPs)
= 10.248.10.0/23 + 10.248.12.0/22
─────────────────────────────────────────────
Total unallocated: 1,728 IPs (42.2%)Overlap Detection
Two CIDRs overlap when either contains the other's network address.
OVERLAP: 10.248.8.0/24 and 10.248.8.0/26
10.248.8.0/24 range: 10.248.8.0 - 10.248.8.255
10.248.8.0/26 range: 10.248.8.0 - 10.248.8.63
Overlap: 64 IPs in conflictPython: ipaddress.IPv4Network('10.248.8.0/24').overlaps(IPv4Network('10.248.8.0/26')) returns True.
Address Space Planning Best Practices
RFC 1918 Private Address Ranges
| Range | CIDR | Total IPs | Typical Use |
|---|---|---|---|
| 10.0.0.0 - 10.255.255.255 | 10.0.0.0/8 | 16.7M | Enterprise networks |
| 172.16.0.0 - 172.31.255.255 | 172.16.0.0/12 | 1M | Medium networks |
| 192.168.0.0 - 192.168.255.255 | 192.168.0.0/16 | 65K | Small networks |
Azure-Specific Ranges to Avoid
| Range | Reason |
|---|---|
| 168.63.129.16/32 | Azure platform health monitoring |
| 169.254.0.0/16 | APIPA / link-local |
| 100.64.0.0/10 | CGN (Carrier-Grade NAT) |
Recommended Allocation Strategy
1. Assign a /16 per major site or region (65K IPs) 2. Subdivide into /20 per environment (4K IPs per env) 3. Leave /24 gaps between VNets for future peering 4. Document every allocation in your network address inventory
Subnet Alignment Rules
CIDR blocks must be naturally aligned. A /24 must start on a 256-address boundary, a /22 on a 1024-address boundary, etc.
Alignment formula: network_address % (2^(32 - prefix)) == 0
Common Alignment Mistakes
| Attempted CIDR | Problem | Correct Alternative |
|---|---|---|
| 10.248.1.0/22 | /22 must start at .0.0, .4.0, .8.0, etc. | 10.248.0.0/22 or 10.248.4.0/22 |
| 10.248.3.0/23 | /23 must start at even third octet | 10.248.2.0/23 or 10.248.4.0/23 |
| 10.248.8.64/25 | /25 must start at .0 or .128 | 10.248.8.0/25 or 10.248.8.128/25 |
Supernetting and Summarization
When multiple contiguous subnets can be expressed as a single larger block:
10.248.0.0/24 + 10.248.1.0/24 = 10.248.0.0/23
10.248.0.0/23 + 10.248.2.0/23 = 10.248.0.0/22Rule: Two adjacent CIDRs of the same size can be summarized only if the first starts on a boundary that is a multiple of the combined size.
Conversion Quick Reference
| Prefix | Mask | Wildcard | Block Size |
|---|---|---|---|
| /16 | 255.255.0.0 | 0.0.255.255 | 65,536 |
| /17 | 255.255.128.0 | 0.0.127.255 | 32,768 |
| /18 | 255.255.192.0 | 0.0.63.255 | 16,384 |
| /19 | 255.255.224.0 | 0.0.31.255 | 8,192 |
| /20 | 255.255.240.0 | 0.0.15.255 | 4,096 |
| /21 | 255.255.248.0 | 0.0.7.255 | 2,048 |
| /22 | 255.255.252.0 | 0.0.3.255 | 1,024 |
| /23 | 255.255.254.0 | 0.0.1.255 | 512 |
| /24 | 255.255.255.0 | 0.0.0.255 | 256 |
| /25 | 255.255.255.128 | 0.0.0.127 | 128 |
| /26 | 255.255.255.192 | 0.0.0.63 | 64 |
| /27 | 255.255.255.224 | 0.0.0.31 | 32 |
| /28 | 255.255.255.240 | 0.0.0.15 | 16 |
| /29 | 255.255.255.248 | 0.0.0.7 | 8 |
| /30 | 255.255.255.252 | 0.0.0.3 | 4 |
| /31 | 255.255.255.254 | 0.0.0.1 | 2 |
| /32 | 255.255.255.255 | 0.0.0.0 | 1 |
Using the Calculator Script
# Basic CIDR info
python3 scripts/network-calc.py calculate 10.248.0.0/20
# How many hosts fit in a /23?
python3 scripts/network-calc.py calculate 10.0.0.0/23
# What prefix do I need for 500 hosts?
python3 scripts/network-calc.py calculate --from-hosts 500
# Split a /20 into /22 subnets
python3 scripts/network-calc.py calculate 10.248.0.0/20 --split 22
# Analyze current VNet utilization
python3 scripts/network-calc.py analyze --vnet 10.248.0.0/20 \
--subnets "10.248.0.0/22,10.248.4.0/22,10.248.8.0/26,10.248.9.0/24"
# Find first available gap for 500 hosts
python3 scripts/network-calc.py first-fit --vnet 10.248.0.0/20 \
--subnets "10.248.0.0/22,10.248.4.0/22,10.248.8.0/26,10.248.9.0/24" \
--hosts 500
# Validate for overlaps (pre-commit hook)
python3 scripts/network-calc.py validate --vnet 10.248.0.0/20 \
--subnets "10.248.0.0/22,10.248.4.0/22,10.248.8.0/26,10.248.9.0/24"Network Segmentation Patterns
Comprehensive guide to Azure VNet segmentation, NSG design, and subnet architecture.
Hub-Spoke vs Flat VNet
When to Use Hub-Spoke
- Multiple workloads or teams sharing centralized services (firewall, DNS, VPN)
- Spoke-to-spoke traffic must be inspected or filtered
- Separate billing or RBAC per spoke
- Multiple environments (dev/staging/prod) sharing a single egress point
- Enterprise environments with 5+ VNets
When to Use Flat VNet (Single VNet)
- Single-purpose infrastructure (this project)
- All subnets serve one workload or one environment
- No spoke-to-spoke routing requirements
- Simpler operations and lower cost (no NVA/firewall required)
- Fewer than 5 distinct network segments
This Project Uses Flat VNet -- Correctly
The project deploys a single /20 VNet with 4 subnets serving one environment. There is no cross-workload routing requirement, no shared services hub, and no multi-team isolation need. A hub-spoke topology would add unnecessary Azure Firewall cost (~$1,000/mo) and operational complexity for zero security benefit.
Subnet Purpose Isolation
Each subnet should serve exactly one purpose. Mixing purposes defeats NSG isolation.
Standard Subnet Taxonomy
| Subnet | Purpose | NSG Profile | NAT GW | Typical Prefix |
|---|---|---|---|---|
| GatewaySubnet | VPN/ExpressRoute termination | No NSG allowed | No | /27-/26 |
| AzureBastionSubnet | Bastion jump host | Bastion-specific rules | No | /26 |
| PublicSubnet | Internet-facing workloads (LB, App GW) | Allow inbound 80/443 | Yes | /24-/22 |
| PrivateSubnet | Backend services, APIs, workers | Deny all inbound from internet | Yes | /24-/22 |
| DataSubnet | Databases, storage, caches | Allow only from PrivateSubnet | Optional | /24-/26 |
| AKSSubnet | Kubernetes node pools | AKS-specific (see AKS guide) | Yes | /23-/21 |
| ManagementSubnet | CI/CD agents, monitoring, admin | Restricted inbound, broad outbound | Yes | /26-/24 |
Isolation Principles
1. Gateway never mixes with workloads -- GatewaySubnet has Azure-imposed restrictions (no NSG, no UDR) 2. Bastion is always isolated -- its NSG rules are unique and must not leak to other subnets 3. Public and private are always separate -- different inbound exposure profiles 4. Data tier is behind private -- databases should never be in the same subnet as app servers 5. AKS gets its own subnet -- node autoscaling can consume unpredictable IPs
NSG Rule Design Patterns
Priority Spacing
Always space priorities by 10 to allow future insertions:
100 AllowHTTPS
110 AllowHTTP
120 AllowSSHFromBastion
...
4000 DenyAllInboundAvoid using consecutive priorities (100, 101, 102) -- inserting a rule between them requires renumbering.
Least Privilege Template
Inbound Rules:
100 Allow [specific source] to [specific port] TCP
110 Allow [specific source] to [specific port] TCP
...
4000 DenyAllInbound ← explicit deny-all catchall
Outbound Rules:
100 Allow [service tag] on [specific port] TCP
110 Allow [service tag] on [specific port] TCP
...
4000 DenyAllOutbound ← explicit deny-all catchallService Tag Usage
Always prefer service tags over IP addresses:
| Instead of... | Use Service Tag |
|---|---|
| Azure DC IP ranges | AzureCloud |
| Container registry IPs | AzureContainerRegistry |
| Load balancer probe IPs | AzureLoadBalancer |
| Key Vault endpoints | AzureKeyVault |
| Monitor endpoints | AzureMonitor |
| AAD endpoints | AzureActiveDirectory |
| Storage account IPs | Storage |
| SQL Server IPs | Sql |
NSG Per Subnet, Not Per NIC
Associate NSGs at the subnet level, not individual NICs. Per-NIC NSGs create management complexity and make it easy to miss a VM.
Bastion Subnet NSG Pattern
Inbound:
100 Allow Internet → 443/TCP (Bastion portal access)
110 Allow GatewayManager → 443/TCP (Azure management)
120 Allow AzureLoadBalancer → 443/TCP (Health probes)
130 Allow VirtualNetwork → 8080,5701/TCP (Bastion data plane)
4000 DenyAllInbound
Outbound:
100 Allow VirtualNetwork → 22/TCP (SSH to targets)
110 Allow VirtualNetwork → 3389/TCP (RDP to targets)
120 Allow AzureCloud → 443/TCP (Azure diagnostics)
130 Allow VirtualNetwork → 8080,5701/TCP (Bastion data plane)
4000 DenyAllOutboundPrivate Subnet NSG Pattern
Inbound:
100 Allow VirtualNetwork → 443/TCP (Internal HTTPS)
110 Allow VirtualNetwork → 22/TCP (SSH from Bastion)
4000 DenyAllInbound
Outbound:
100 Allow AzureCloud → 443/TCP (Azure services)
110 Allow Internet → 443/TCP (Package repos, APIs)
120 Allow VirtualNetwork → 1433/TCP (SQL to DataSubnet)
4000 DenyAllOutboundNAT Gateway Placement
Where to Associate NAT Gateway
| Subnet | NAT GW | Reason |
|---|---|---|
| PublicSubnet | Yes | Workloads need stable egress IPs |
| PrivateSubnet | Yes | Backend services need outbound (updates, APIs) |
| AKSSubnet | Yes | Nodes need outbound for image pulls, monitoring |
| ManagementSubnet | Yes | CI agents need outbound for package downloads |
| GatewaySubnet | Never | Azure restriction -- not compatible |
| AzureBastionSubnet | Never | Bastion manages its own connectivity |
NAT Gateway Sizing
- 1 NAT GW supports up to 64,000 concurrent SNAT connections per public IP
- Assign 1-16 public IPs per NAT GW
- Each public IP adds 64,000 SNAT connections
- A single NAT GW can serve multiple subnets in the same VNet
Service Endpoint vs Private Endpoint
Decision Matrix
| Factor | Service Endpoint | Private Endpoint |
|---|---|---|
| Traffic path | Microsoft backbone (optimized route) | Private IP in your VNet |
| Cost | Free | ~$7.30/month per endpoint + data processing |
| DNS complexity | None | Requires private DNS zone |
| NSG compatibility | Yes (service tags) | Yes (private IP) |
| Cross-VNet access | No (VNet-scoped) | Yes (via peering/VPN) |
| On-premises access | No | Yes (via VPN/ExpressRoute) |
| Data exfiltration protection | Limited (entire service) | Strong (specific resource) |
| Setup complexity | Low (subnet-level flag) | Medium (DNS + NIC + approval) |
| Supported services | ~20 services | 100+ services |
When to Use Service Endpoints
- Budget-constrained environments
- Single-VNet deployments with no hybrid connectivity
- Services that only need VNet-to-Azure-service access
- Quick wins: Storage, SQL, Key Vault, Cosmos DB
When to Use Private Endpoints
- Regulatory requirements for private connectivity
- Hybrid environments (on-prem needs access)
- Multi-VNet architectures (hub-spoke)
- Data exfiltration prevention is a requirement
- Services not supported by service endpoints
This Project's Approach
Service endpoints are appropriate because:
- Single VNet deployment (no cross-VNet access needed)
- No hybrid/on-premises connectivity requirement
- Cost optimization (no per-endpoint charges)
- Simpler DNS (no private DNS zones to manage)
Anti-Patterns
1. 0.0.0.0/0 Ingress on Any Rule Except Bastion HTTPS
Why it is wrong: Opening all internet traffic to any subnet is the most common misconfiguration in Azure. Only AzureBastionSubnet should allow inbound from Internet on port 443.
Fix: Use specific source service tags (VirtualNetwork, AzureLoadBalancer) or source IP ranges. Never use * or 0.0.0.0/0 as source for inbound rules on workload subnets.
2. Wildcard Port Ranges on Inbound Rules
Why it is wrong: * as destination port range allows every port. Attackers scan for open ports and exploit any service they find.
Fix: Specify exact ports (443, 22, 3389) or narrow ranges (8080-8090). If you cannot enumerate the ports, the architecture needs redesign.
3. NSG with No Explicit Deny Rules
Why it is wrong: Azure has an implicit deny at priority 65500, but relying on it means you cannot distinguish "intentionally blocked" from "never considered." Auditors flag this.
Fix: Add explicit DenyAllInbound at priority 4000 and DenyAllOutbound at priority 4000. This documents intent and makes rule ordering visible.
4. Overlapping Subnets
Why it is wrong: Azure rejects overlapping subnets at deployment time, but overlapping CIDRs in Terraform variables cause confusing plan errors.
Fix: Run network-calc.py validate before every terraform plan. Add overlap checks to pre-commit hooks.
5. Bastion Subnet Smaller Than /26
Why it is wrong: Azure requires a minimum /26 (64 IPs) for AzureBastionSubnet. Deployment fails with a cryptic error if undersized.
Fix: Always allocate /26 for Bastion. Do not try to save IPs here.
6. Gateway Subnet Smaller Than /27
Why it is wrong: Azure requires a minimum /27 (32 IPs) for GatewaySubnet. ExpressRoute gateways need /26.
Fix: Allocate /27 minimum, /26 if ExpressRoute is possible in the future.
7. Missing NAT Gateway on Subnets Needing Outbound
Why it is wrong: Without NAT Gateway, VMs use ephemeral SNAT IPs that Azure can reassign. This causes intermittent outbound failures under load and makes IP allow-listing impossible.
Fix: Associate a NAT Gateway with every subnet that needs outbound internet access (public, private, AKS, management).
8. Public IPs on VMs When Bastion Is Available
Why it is wrong: Public IPs expose VMs directly to the internet. Bastion provides authenticated, audited, browser-based access without public IP exposure.
Fix: Remove public IPs from all VMs. Use Bastion for SSH/RDP. Use NAT Gateway for outbound.
9. Flat /16 Without Segmentation
Why it is wrong: A single /16 subnet with all resources has no network isolation. Any compromised VM can reach every other VM on any port.
Fix: Segment into purpose-specific subnets with dedicated NSGs. Even a minimal deployment benefits from gateway/bastion/public/private separation.
10. Hardcoded IPs Instead of Service Tags
Why it is wrong: Azure service IPs change without notice. Hardcoded IPs break when Azure updates its IP ranges, causing outages.
Fix: Always use service tags (AzureCloud, AzureMonitor, Storage, etc.). Azure updates service tag definitions automatically.
This Project's Segmentation Design
Current Layout
10.248.0.0/20 (4,096 IPs)
├── GatewaySubnet 10.248.0.0/22 (1,024 IPs) - VPN termination
├── PublicSubnet 10.248.4.0/22 (1,024 IPs) - Internet-facing, NAT GW
├── AzureBastionSubnet 10.248.8.0/26 (64 IPs) - Bastion jump host
├── PrivateSubnet 10.248.9.0/24 (256 IPs) - Backend services, NAT GW
├── [gap] 10.248.8.64/26 (192 IPs) - Available
└── [gap] 10.248.10.0/23 (1,536 IPs) - Available (AKS candidate)Why This Design Is Correct
1. Proper isolation: Gateway, Bastion, public, and private subnets are fully separated with independent NSGs 2. Mandatory names honored: GatewaySubnet and AzureBastionSubnet use Azure's required exact names 3. Minimum sizes met: Bastion is /26 (meets /26 minimum), Gateway is /22 (exceeds /27 minimum) 4. NAT Gateway correctly placed: Associated with PublicSubnet and PrivateSubnet, not with GatewaySubnet or Bastion 5. Growth room preserved: 42% of address space (1,728 IPs) remains unallocated for future AKS or additional workloads 6. Flat topology appropriate: Single-purpose infrastructure with no cross-workload routing needs does not benefit from hub-spoke overhead 7. No anti-patterns present: No public IPs on VMs, no wildcard port rules, no overlapping subnets, explicit deny rules in all NSGs
#!/usr/bin/env python3
# /// script
# requires-python = ">=3.8"
# dependencies = []
# ///
"""
Azure Network Calculator — CIDR planning, gap analysis, overlap detection.
Uses only Python stdlib (ipaddress, json, argparse). Zero external deps.
Azure reserves 5 IPs per subnet: .0 (network), .1 (gateway), .2-.3 (DNS), broadcast.
Usage:
python3 network-calc.py calculate 10.248.0.0/20
python3 network-calc.py calculate --from-hosts 500
python3 network-calc.py analyze --vnet 10.248.0.0/20 --subnets 10.248.0.0/22,10.248.4.0/22
python3 network-calc.py validate --vnet 10.248.0.0/20 --subnets 10.248.0.0/22,10.248.4.0/22
python3 network-calc.py plan-multi --base 10.248.0.0/16 --envs 3 --prefix 20
"""
from __future__ import annotations
import argparse
import ipaddress
import json
import math
import re
import sys
from pathlib import Path
from typing import Dict, List, Optional
AZURE_RESERVED_IPS = 5
# Azure minimum subnet sizes by purpose
AZURE_MIN_PREFIX = {
"AzureBastionSubnet": 26,
"GatewaySubnet": 27,
"AzureFirewallSubnet": 26,
"AzureFirewallManagementSubnet": 26,
"RouteServerSubnet": 27,
}
def azure_usable_hosts(prefix_len: int) -> int:
"""Calculate usable hosts in an Azure subnet (total - 5 reserved)."""
total = 2 ** (32 - prefix_len)
return max(total - AZURE_RESERVED_IPS, 0)
def min_prefix_for_hosts(hosts_needed: int) -> int:
"""Calculate minimum CIDR prefix length for N usable hosts (Azure-adjusted)."""
total_needed = hosts_needed + AZURE_RESERVED_IPS
bits = math.ceil(math.log2(total_needed))
return 32 - bits
def cidr_info(cidr_str: str) -> dict:
"""Return comprehensive info about a CIDR block."""
net = ipaddress.IPv4Network(cidr_str, strict=False)
prefix = net.prefixlen
total = net.num_addresses
usable = azure_usable_hosts(prefix)
hosts = list(net.hosts())
return {
"cidr": str(net),
"network": str(net.network_address),
"broadcast": str(net.broadcast_address),
"netmask": str(net.netmask),
"prefix_length": prefix,
"total_ips": total,
"azure_reserved": min(AZURE_RESERVED_IPS, total),
"usable_hosts": usable,
"first_usable": str(hosts[0]) if hosts else None,
"last_usable": str(hosts[-1]) if hosts else None,
"wildcard": str(net.hostmask),
}
def find_gaps(vnet_cidr: str, subnet_cidrs: list[str]) -> list[dict]:
"""Find unallocated address ranges within a VNet."""
vnet = ipaddress.IPv4Network(vnet_cidr, strict=False)
subnets = sorted(
[ipaddress.IPv4Network(s, strict=False) for s in subnet_cidrs],
key=lambda n: n.network_address,
)
gaps = []
current = int(vnet.network_address)
vnet_end = int(vnet.broadcast_address)
for subnet in subnets:
subnet_start = int(subnet.network_address)
subnet_end = int(subnet.broadcast_address)
if subnet_start > current:
gap_start = ipaddress.IPv4Address(current)
gap_end = ipaddress.IPv4Address(subnet_start - 1)
gap_size = subnet_start - current
# Break into valid CIDR blocks
gap_cidrs = list(
ipaddress.summarize_address_range(gap_start, gap_end)
)
gaps.append({
"start": str(gap_start),
"end": str(gap_end),
"size": gap_size,
"cidrs": [str(c) for c in gap_cidrs],
"usable_hosts": sum(azure_usable_hosts(c.prefixlen) for c in gap_cidrs),
})
current = max(current, subnet_end + 1)
# Check for gap after last subnet
if current <= vnet_end:
gap_start = ipaddress.IPv4Address(current)
gap_end = ipaddress.IPv4Address(vnet_end)
gap_size = vnet_end - current + 1
gap_cidrs = list(
ipaddress.summarize_address_range(gap_start, gap_end)
)
gaps.append({
"start": str(gap_start),
"end": str(gap_end),
"size": gap_size,
"cidrs": [str(c) for c in gap_cidrs],
"usable_hosts": sum(azure_usable_hosts(c.prefixlen) for c in gap_cidrs),
})
return gaps
def find_overlaps(subnet_cidrs: list[str]) -> list[dict]:
"""Detect overlapping CIDR ranges."""
nets = [(s, ipaddress.IPv4Network(s, strict=False)) for s in subnet_cidrs]
overlaps = []
for i, (name_a, net_a) in enumerate(nets):
for j, (name_b, net_b) in enumerate(nets):
if i >= j:
continue
if net_a.overlaps(net_b):
overlap_start = max(int(net_a.network_address), int(net_b.network_address))
overlap_end = min(int(net_a.broadcast_address), int(net_b.broadcast_address))
overlaps.append({
"subnet_a": name_a,
"subnet_b": name_b,
"overlap_ips": overlap_end - overlap_start + 1,
"overlap_range": f"{ipaddress.IPv4Address(overlap_start)} - {ipaddress.IPv4Address(overlap_end)}",
})
return overlaps
def check_azure_constraints(named_subnets: dict[str, str]) -> list[dict]:
"""Validate Azure-specific subnet constraints."""
violations = []
for name, cidr in named_subnets.items():
net = ipaddress.IPv4Network(cidr, strict=False)
if name in AZURE_MIN_PREFIX:
min_pf = AZURE_MIN_PREFIX[name]
if net.prefixlen > min_pf:
violations.append({
"subnet": name,
"cidr": cidr,
"violation": f"Prefix /{net.prefixlen} too small. {name} requires minimum /{min_pf} ({2**(32-min_pf)} IPs)",
"severity": "ERROR",
})
# General minimum: /29
if net.prefixlen > 29:
violations.append({
"subnet": name,
"cidr": cidr,
"violation": f"Prefix /{net.prefixlen} below Azure minimum /29 (8 IPs, 3 usable)",
"severity": "ERROR",
})
return violations
def detect_anti_patterns(named_subnets: dict[str, str], security_rules: list[dict] | None = None) -> list[dict]:
"""Detect network anti-patterns."""
warnings = []
# Check for subnets without standard naming
for name, cidr in named_subnets.items():
net = ipaddress.IPv4Network(cidr, strict=False)
# Warn on very large subnets that waste space
if net.prefixlen < 20 and name not in ("GatewaySubnet",):
warnings.append({
"type": "oversized_subnet",
"subnet": name,
"cidr": cidr,
"message": f"Subnet /{net.prefixlen} allocates {net.num_addresses} IPs. Consider if this is necessary.",
"severity": "WARNING",
})
if security_rules:
for rule in security_rules:
src = rule.get("source_address_prefix", "")
dst_port = rule.get("destination_port_range", "")
direction = rule.get("direction", "")
if src == "0.0.0.0/0" and direction == "Inbound":
warnings.append({
"type": "open_ingress",
"rule": rule.get("name", "unknown"),
"message": "Inbound rule allows traffic from 0.0.0.0/0 (entire internet). Restrict to specific CIDRs.",
"severity": "CRITICAL",
})
if dst_port == "*" and direction == "Inbound":
warnings.append({
"type": "wildcard_ports",
"rule": rule.get("name", "unknown"),
"message": "Inbound rule allows all destination ports. Restrict to specific ports.",
"severity": "HIGH",
})
return warnings
def parse_tfvars_network(tfvars_path: str) -> dict:
"""Parse network-related variables from a terraform.tfvars file."""
content = Path(tfvars_path).read_text()
result = {"subnets": {}, "vnet": None}
# Parse list variables: var_name = ["value1", "value2"]
list_pattern = re.compile(r'(\w+)\s*=\s*\[([^\]]*)\]', re.DOTALL)
for match in list_pattern.finditer(content):
var_name = match.group(1)
values = re.findall(r'"([^"]*)"', match.group(2))
if var_name == "vnet_address_space" and values:
result["vnet"] = values[0]
elif var_name.startswith("vnet_subnet_") or var_name.startswith("vnet_azure_"):
# Map variable name to subnet name
subnet_name = var_name.replace("vnet_subnet_", "").replace("vnet_azure_subnet_", "")
if values:
result["subnets"][subnet_name] = values[0]
return result
def split_cidr(cidr_str: str, new_prefix: int) -> list[dict]:
"""Split a CIDR into smaller subnets of a given prefix length."""
net = ipaddress.IPv4Network(cidr_str, strict=False)
if new_prefix <= net.prefixlen:
return [{"error": f"New prefix /{new_prefix} must be longer than /{net.prefixlen}"}]
subnets = list(net.subnets(new_prefix=new_prefix))
return [cidr_info(str(s)) for s in subnets]
def first_fit(vnet_cidr: str, existing_cidrs: list[str], hosts_needed: int) -> dict | None:
"""Find the first available gap that fits the required number of hosts."""
prefix = min_prefix_for_hosts(hosts_needed)
subnet_size = 2 ** (32 - prefix)
gaps = find_gaps(vnet_cidr, existing_cidrs)
for gap in gaps:
if gap["size"] >= subnet_size:
# Align to subnet boundary within gap
gap_start = int(ipaddress.IPv4Address(gap["start"]))
aligned_start = ((gap_start + subnet_size - 1) // subnet_size) * subnet_size
if aligned_start + subnet_size - 1 <= int(ipaddress.IPv4Address(gap["end"])):
new_cidr = f"{ipaddress.IPv4Address(aligned_start)}/{prefix}"
info = cidr_info(new_cidr)
info["placement"] = f"first-fit in gap {gap['start']} - {gap['end']}"
return info
return None
# ── Subcommand handlers ──────────────────────────────────────────────────
def cmd_calculate(args):
"""Handle 'calculate' subcommand."""
if args.from_hosts:
prefix = min_prefix_for_hosts(args.from_hosts)
print(json.dumps({
"hosts_requested": args.from_hosts,
"minimum_prefix": prefix,
"subnet_mask": str(ipaddress.IPv4Network(f"0.0.0.0/{prefix}").netmask),
"total_ips": 2 ** (32 - prefix),
"usable_hosts": azure_usable_hosts(prefix),
"recommendation": f"Use /{prefix} for {args.from_hosts} hosts ({azure_usable_hosts(prefix)} usable with Azure 5-IP reservation)",
}, indent=2))
return
if args.split:
results = split_cidr(args.cidr, args.split)
print(json.dumps({"source": args.cidr, "split_prefix": args.split, "subnets": results}, indent=2))
return
if args.cidr:
print(json.dumps(cidr_info(args.cidr), indent=2))
else:
print("Error: provide a CIDR (e.g., 10.0.0.0/20) or --from-hosts N", file=sys.stderr)
sys.exit(1)
def cmd_analyze(args):
"""Handle 'analyze' subcommand."""
vnet = args.vnet
subnets = {}
if args.from_tfvars:
parsed = parse_tfvars_network(args.from_tfvars)
if parsed["vnet"]:
vnet = parsed["vnet"]
subnets = parsed["subnets"]
elif args.subnets:
for i, s in enumerate(args.subnets.split(",")):
subnets[f"subnet_{i}"] = s.strip()
if not vnet:
print("Error: provide --vnet CIDR or --from-tfvars PATH", file=sys.stderr)
sys.exit(1)
vnet_info = cidr_info(vnet)
subnet_list = list(subnets.values())
# Allocation table
allocations = []
total_allocated = 0
for name, cidr in subnets.items():
info = cidr_info(cidr)
total_allocated += info["total_ips"]
allocations.append({
"name": name,
"cidr": cidr,
"total_ips": info["total_ips"],
"usable_hosts": info["usable_hosts"],
})
# Gaps
gaps = find_gaps(vnet, subnet_list) if subnet_list else []
total_unallocated = sum(g["size"] for g in gaps)
utilization = (total_allocated / vnet_info["total_ips"] * 100) if vnet_info["total_ips"] > 0 else 0
result = {
"vnet": vnet_info,
"allocations": allocations,
"total_allocated_ips": total_allocated,
"total_unallocated_ips": total_unallocated,
"utilization_percent": round(utilization, 1),
"gaps": gaps,
"subnet_count": len(allocations),
}
print(json.dumps(result, indent=2))
def cmd_validate(args):
"""Handle 'validate' subcommand."""
vnet = args.vnet
subnets = {}
if args.from_tfvars:
parsed = parse_tfvars_network(args.from_tfvars)
if parsed["vnet"]:
vnet = parsed["vnet"]
subnets = parsed["subnets"]
elif args.subnets:
for i, s in enumerate(args.subnets.split(",")):
subnets[f"subnet_{i}"] = s.strip()
subnet_list = list(subnets.values())
issues = []
# Overlap detection
overlaps = find_overlaps(subnet_list)
for o in overlaps:
issues.append({"type": "overlap", "severity": "CRITICAL", **o})
# VNet containment check
if vnet:
vnet_net = ipaddress.IPv4Network(vnet, strict=False)
for name, cidr in subnets.items():
subnet_net = ipaddress.IPv4Network(cidr, strict=False)
if not subnet_net.subnet_of(vnet_net):
issues.append({
"type": "out_of_vnet",
"severity": "CRITICAL",
"subnet": name,
"cidr": cidr,
"message": f"Subnet {cidr} is not within VNet {vnet}",
})
# Azure constraints
constraints = check_azure_constraints(subnets)
issues.extend([{"type": "azure_constraint", **c} for c in constraints])
# Anti-patterns
anti = detect_anti_patterns(subnets)
issues.extend(anti)
result = {
"valid": len([i for i in issues if i["severity"] in ("CRITICAL", "ERROR")]) == 0,
"issues_count": len(issues),
"critical": len([i for i in issues if i["severity"] == "CRITICAL"]),
"errors": len([i for i in issues if i["severity"] == "ERROR"]),
"warnings": len([i for i in issues if i["severity"] in ("WARNING", "HIGH")]),
"issues": issues,
}
print(json.dumps(result, indent=2))
if not result["valid"]:
sys.exit(1)
def cmd_plan_multi(args):
"""Handle 'plan-multi' subcommand."""
base = ipaddress.IPv4Network(args.base, strict=False)
env_count = args.envs
env_prefix = args.prefix
env_subnet_size = 2 ** (32 - env_prefix)
total_needed = env_subnet_size * env_count
if total_needed > base.num_addresses:
print(json.dumps({
"error": f"Cannot fit {env_count} /{env_prefix} VNets ({total_needed} IPs) in {args.base} ({base.num_addresses} IPs)",
}, indent=2))
sys.exit(1)
env_names = args.names.split(",") if args.names else [f"env_{i}" for i in range(env_count)]
environments = []
current = int(base.network_address)
for i, name in enumerate(env_names[:env_count]):
env_cidr = f"{ipaddress.IPv4Address(current)}/{env_prefix}"
info = cidr_info(env_cidr)
info["environment"] = name.strip()
environments.append(info)
current += env_subnet_size
# Validate no overlaps
env_cidrs = [e["cidr"] for e in environments]
overlaps = find_overlaps(env_cidrs)
result = {
"base_cidr": args.base,
"environments": environments,
"env_count": env_count,
"env_prefix": env_prefix,
"total_allocated": env_subnet_size * env_count,
"remaining_in_base": base.num_addresses - total_needed,
"overlaps": overlaps,
"peering_safe": len(overlaps) == 0,
}
print(json.dumps(result, indent=2))
def cmd_first_fit(args):
"""Handle 'first-fit' subcommand — find optimal placement for a new subnet."""
subnets = []
if args.subnets:
subnets = [s.strip() for s in args.subnets.split(",")]
elif args.from_tfvars:
parsed = parse_tfvars_network(args.from_tfvars)
if parsed["vnet"]:
args.vnet = parsed["vnet"]
subnets = list(parsed["subnets"].values())
result = first_fit(args.vnet, subnets, args.hosts)
if result:
result["requested_hosts"] = args.hosts
print(json.dumps(result, indent=2))
else:
print(json.dumps({
"error": f"No gap large enough for {args.hosts} hosts in {args.vnet}",
"requested_hosts": args.hosts,
"gaps": find_gaps(args.vnet, subnets),
}, indent=2))
sys.exit(1)
# ── CLI ───────────────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description="Azure Network Calculator — CIDR planning, gap analysis, overlap detection",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
sub = parser.add_subparsers(dest="command", required=True)
# calculate
calc = sub.add_parser("calculate", help="CIDR info, host sizing, subnet splitting")
calc.add_argument("cidr", nargs="?", help="CIDR to analyze (e.g., 10.0.0.0/20)")
calc.add_argument("--from-hosts", type=int, help="Calculate minimum prefix for N hosts")
calc.add_argument("--split", type=int, help="Split CIDR into subnets of this prefix")
# analyze
analyze = sub.add_parser("analyze", help="Analyze VNet layout: utilization, gaps")
analyze.add_argument("--vnet", help="VNet CIDR (e.g., 10.248.0.0/20)")
analyze.add_argument("--subnets", help="Comma-separated subnet CIDRs")
analyze.add_argument("--from-tfvars", help="Read from terraform.tfvars file")
# validate
validate = sub.add_parser("validate", help="Validate for overlaps, Azure constraints")
validate.add_argument("--vnet", help="VNet CIDR")
validate.add_argument("--subnets", help="Comma-separated subnet CIDRs")
validate.add_argument("--from-tfvars", help="Read from terraform.tfvars file")
# plan-multi
plan = sub.add_parser("plan-multi", help="Plan multi-environment VNet allocation")
plan.add_argument("--base", required=True, help="Base CIDR to divide")
plan.add_argument("--envs", type=int, default=3, help="Number of environments (default: 3)")
plan.add_argument("--prefix", type=int, default=20, help="Prefix per environment (default: /20)")
plan.add_argument("--names", help="Comma-separated env names (default: env_0,env_1,...)")
# first-fit
ff = sub.add_parser("first-fit", help="Find optimal placement for a new subnet")
ff.add_argument("--vnet", help="VNet CIDR")
ff.add_argument("--subnets", help="Comma-separated existing subnet CIDRs")
ff.add_argument("--from-tfvars", help="Read from terraform.tfvars file")
ff.add_argument("--hosts", type=int, required=True, help="Number of usable hosts needed")
args = parser.parse_args()
commands = {
"calculate": cmd_calculate,
"analyze": cmd_analyze,
"validate": cmd_validate,
"plan-multi": cmd_plan_multi,
"first-fit": cmd_first_fit,
}
commands[args.command](args)
if __name__ == "__main__":
main()
# Multi-Environment Network Plan
**Generated:** {{date}}
**Base CIDR:** {{base_cidr}}
**Environments:** {{env_count}}
## Address Allocation
| Environment | VNet CIDR | Total IPs | Usable IPs | Status |
|-------------|-----------|-----------|------------|--------|
{{#each environments}}
| {{name}} | {{cidr}} | {{total_ips}} | {{usable_hosts}} | Planned |
{{/each}}
## Subnet Layout Per Environment
Each environment follows this standard layout:
| Subnet | Prefix | Hosts | Purpose |
|--------|--------|-------|---------|
| GatewaySubnet | /27 | 27 | VPN/ExpressRoute |
| AzureBastionSubnet | /26 | 59 | Bastion access |
| PublicSubnet | /24 | 251 | NAT GW outbound |
| PrivateSubnet | /24 | 251 | Workloads |
| AKSSubnet | /23 | 507 | Kubernetes nodes |
| DataSubnet | /24 | 251 | Databases, PE |
## Peering Matrix
| From | To | Overlap Check |
|------|----|---------------|
{{#each peering_pairs}}
| {{from}} | {{to}} | {{status}} |
{{/each}}
## Validation Results
- Overlap check: {{overlap_status}}
- Address space remaining: {{remaining}} IPs
- Peering safe: {{peering_safe}}
# AKS Subnet NSG Rules
# Generated by Azure Network Calculator
# These rules follow Azure AKS networking best practices
aks_security_rules = [
# Allow Bastion SSH access to AKS nodes
{
name = "Allow-Bastion-SSH"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = "{{bastion_cidr}}"
destination_address_prefix = "*"
description = "Allow SSH from Bastion subnet to AKS nodes"
},
# Allow internal VNet traffic
{
name = "Allow-VNet-Internal"
priority = 110
direction = "Inbound"
access = "Allow"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "VirtualNetwork"
destination_address_prefix = "VirtualNetwork"
description = "Allow VNet internal communication for pod-to-pod and node-to-node"
},
# Allow Azure Load Balancer health probes
{
name = "Allow-AzureLoadBalancer"
priority = 120
direction = "Inbound"
access = "Allow"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "AzureLoadBalancer"
destination_address_prefix = "*"
description = "Allow Azure Load Balancer health probes for AKS services"
},
# Deny all other inbound
{
name = "Deny-All-Inbound"
priority = 4096
direction = "Inbound"
access = "Deny"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "*"
destination_address_prefix = "*"
description = "Deny all other inbound traffic (least privilege)"
}
]
# VNet Layout Configuration
# Generated by Azure Network Calculator
# Date: {{date}}
# VNet: {{vnet_cidr}}
# Network Configuration
vnet_address_space = ["{{vnet_cidr}}"]
vnet_subnet_gateway = ["{{gateway_cidr}}"]
vnet_subnet_public = ["{{public_cidr}}"]
vnet_azure_subnet_bastion = ["{{bastion_cidr}}"]
vnet_subnet_private = ["{{private_cidr}}"]
vm_private_ip_address = ["{{first_usable_private}}"]
# Subnet Allocation Summary
# ┌─────────────────────────┬──────────────────────┬───────────┬────────────┐
# │ Subnet │ CIDR │ Total IPs │ Usable IPs │
# ├─────────────────────────┼──────────────────────┼───────────┼────────────┤
# │ GatewaySubnet │ {{gateway_cidr}} │ {{gw_t}} │ {{gw_u}} │
# │ PublicSubnet │ {{public_cidr}} │ {{pub_t}} │ {{pub_u}} │
# │ AzureBastionSubnet │ {{bastion_cidr}} │ {{bas_t}} │ {{bas_u}} │
# │ PrivateSubnet │ {{private_cidr}} │ {{prv_t}} │ {{prv_u}} │
# ├─────────────────────────┼──────────────────────┼───────────┼────────────┤
# │ Total │ │ {{total}} │ {{usable}} │
# │ Utilization │ │ {{util}}% │ │
# └─────────────────────────┴──────────────────────┴───────────┴────────────┘
Network Calculator Workflow
Initialization
Detect the user's request type from their input and route to the appropriate action.
Command Dispatch
Calculate (CIDR info, host sizing)
1. Run: python3 ./scripts/network-calc.py calculate <CIDR> or --from-hosts N 2. Present results in a formatted table 3. If --split requested, show all resulting subnets
Analyze (VNet utilization, gaps)
1. Run: python3 ./scripts/network-calc.py analyze --vnet <CIDR> --subnets <list> 2. Or: python3 ./scripts/network-calc.py analyze --from-tfvars <path> 3. Present allocation table, utilization %, and gap analysis 4. Highlight gaps suitable for new workloads (AKS, containers, databases)
Validate (overlap detection, Azure constraints)
1. Run: python3 ./scripts/network-calc.py validate --vnet <CIDR> --subnets <list> 2. Report all violations with severity (CRITICAL, ERROR, WARNING) 3. If valid, confirm with green status 4. Exit code 1 = violations found (compatible with pre-commit hooks)
First-Fit (find optimal subnet placement)
1. Run: python3 ./scripts/network-calc.py first-fit --vnet <CIDR> --subnets <list> --hosts N 2. Returns the optimal CIDR for the requested host count 3. Shows placement within available gaps
Plan AKS Network
1. Read references/aks-networking-guide.md for CNI comparison and sizing formulas 2. Ask user for: CNI type, max nodes, max pods per node 3. Calculate node subnet size, pod CIDR, service CIDR 4. Run first-fit against current VNet to place AKS subnet 5. Generate NSG rules from templates/nsg-rules-aks.tfvars.tpl
Plan Multi-Environment
1. Run: python3 ./scripts/network-calc.py plan-multi --base <CIDR> --envs N --prefix P 2. Present environment allocation table 3. Fill templates/multi-env-plan.md.tpl with results 4. Validate no overlaps between environments
Best Practices Query
Route to the appropriate reference file:
- CIDR math →
references/cidr-calculation-guide.md - AKS networking →
references/aks-networking-guide.md - Segmentation →
references/segmentation-patterns.md - Azure limits →
references/azure-constraints.md
Output Rules
- Always present CIDR calculations in structured tables
- When generating Terraform output, follow
terraform/variables.tfvariable naming - Always run validate after any calculation before presenting final results
- Include utilization percentage in all analysis outputs
Integration
Users can add these targets to their justfile:
net-calc *ARGS:
python3 .claude/skills/devops-network-calculator-for-azure/scripts/network-calc.py {{ARGS}}
net-validate:
python3 .claude/skills/devops-network-calculator-for-azure/scripts/network-calc.py validate --from-tfvars terraform/terraform.tfvars