Guidance for the network module's subnet allocation, CIDR mapping, NSG rules, PE pool outputs, and delegation requirements in ai-hub-tracking. Use when adding subnets, modifying address allocation, changing NSG rules, updating PE pool logic, or debugging subnet delegation issues.
Use this skill profile when creating or modifying subnet allocation, CIDR mapping, NSG rules, PE pool outputs, or delegation configuration in the network module.
params/{env}/shared.tfvarsRequired context before changes:
subnet_allocation map in params/{env}/shared.tfvars (see Allocation Model below)Every network module change should deliver:
params/{env}/shared.tfvars subnet_allocation mapmodules/network/main.tf (if new subnet type)modules/network/outputs.tfstacks/shared/main.tf + stacks/shared/outputs.tfterraform fmt -recursive on modules/network/ and stacks/shared/README.md Folder Structure section in the same change. Do not add gitignored or local-only artifacts to that tree.| Component | Location | Purpose |
|---|---|---|
| CIDR mapping | infra-ai-hub/modules/network/locals.tf | Direct CIDR reads from subnet_allocation, PE pool derivation |
| Subnet variable | infra-ai-hub/modules/network/variables.tf | subnet_allocation map(map(string)) with validations |
| NSGs + subnets | infra-ai-hub/modules/network/main.tf | NSG resources, azapi_resource subnet definitions |
| Outputs | infra-ai-hub/modules/network/outputs.tf | Subnet IDs, CIDRs, NSG IDs, PE pool outputs |
| Shared stack wiring | infra-ai-hub/stacks/shared/main.tf → module "network" | Passes subnet_allocation to module |
| Shared stack outputs | infra-ai-hub/stacks/shared/outputs.tf | PE pool pass-through + backward-compat outputs |
| Per-env config | infra-ai-hub/params/{env}/shared.tfvars → subnet_allocation | Full CIDRs per subnet per address space |
| Tenant PE selection | infra-ai-hub/stacks/tenant/locals.tf | PE subnet resolution with 3-tier precedence |
| APIM PE selection | infra-ai-hub/stacks/apim/locals.tf | Pinned PE subnet resolution with fallback |
VNets are pre-provisioned by the BC Gov Landing Zone — the module only creates subnets within existing VNets. Subnets are created in the shared stack and consumed by downstream stacks via data.terraform_remote_state.shared.
All subnets use azapi_resource (not azurerm_subnet) because Landing Zone policy requires NSG at creation time — azapi_resource does this atomically.
subnet_allocation)The network module uses a single subnet_allocation variable of type map(map(string)):
"10.x.x.0/24")"privateendpoints-subnet")"10.x.x.0/27")There is no offset computation — all CIDRs are explicit in tfvars. The module reads them directly via merge().
| Subnet Name | Delegation | Purpose |
|---|---|---|
privateendpoints-subnet | None (privateEndpointNetworkPolicies = "Disabled") | Primary PE subnet |
privateendpoints-subnet-<n> | None | Additional PE pool subnets (<n> starts at 1: -1, -2, ...) |
apim-subnet | Microsoft.Web/serverFarms | APIM VNet injection |
appgw-subnet | None (dedicated, no delegation) | Application Gateway |
aca-subnet | Microsoft.App/environments | Container Apps Environment |
external_peered_projects)Optional map of external project names to their peered VNet config. When populated, the network module creates dynamic inbound NSG rules on the APIM subnet allowing direct HTTPS (443) traffic from these peered VNets — bypassing App Gateway. NSGs are stateful, so no outbound mirror rule is needed.
external_peered_projects = {
"forest-client" = { cidrs = ["10.x.x.0/20"], priority = 400 }
"nr-data-hub" = { cidrs = ["10.x.x.0/22", "10.x.x.0/22"], priority = 410 }
}
Priorities are caller-assigned (400–499) so adding/removing a project never shifts existing rules. Use gaps (400, 410, 420) for future growth.
Critical rules:
Dev — 1 address space:
| Subnet | CIDR | Size |
|---|---|---|
privateendpoints-subnet | 10.x.x.0/27 | 32 IPs |
apim-subnet | 10.x.x.32/27 | 32 IPs |
aca-subnet | 10.x.x.64/27 | 32 IPs |
appgw-subnet | Not deployed | App Gateway disabled |
Test — 2 address spaces:
| Space | Subnet | CIDR | Size |
|---|---|---|---|
| PE space /24 | privateendpoints-subnet | 10.x.x.0/24 | 256 IPs (dedicated PE space) |
| Workload /24 | apim-subnet | 10.x.x.0/27 | 32 IPs |
| Workload /24 | appgw-subnet | 10.x.x.32/27 | 32 IPs |
| Workload /24 | aca-subnet | 10.x.x.64/27 | 32 IPs |
Prod — 4 address spaces (placeholder CIDRs, not yet deployed):
| Space | Subnet | CIDR | Notes |
|---|---|---|---|
| Space 1 | privateendpoints-subnet | TBD /24 | PE pool space 1 |
| Space 2 | privateendpoints-subnet-1 | TBD /24 | PE pool space 2 |
| Space 3 | privateendpoints-subnet-2 | TBD /24 | PE pool space 3 |
| Space 4 | apim-subnet, appgw-subnet, aca-subnet | TBD /27s | Workload space |
The network module automatically derives a PE pool from all subnets whose name starts with privateendpoints-subnet:
privateendpoints-subnet, privateendpoints-subnet-1, privateendpoints-subnet-2, ...privateendpoints-subnet (the original, suffix-free name)private_endpoint_subnet_ids_by_key, private_endpoint_subnet_cidrs_by_key, private_endpoint_subnet_keys_orderedTenant stack — pe_subnet_key is mandatory for every enabled tenant:
pe_subnet_key in tenant config (var.tenants[key].pe_subnet_key) — ALWAYS set, validated at plan timeEach tenant creates up to 5 PEs but 6 IPs total (Cosmos DB PE = 2 IPs: sql global + canadacentral regional endpoint). All PEs for a tenant land on the same subnet ("tenant affinity"). Storage Account has no PE (public access in Landing Zone).
Shared stack PEs always use the primary privateendpoints-subnet — consuming exactly 5 IPs: AI Foundry Hub PE (3 IPs: cognitiveservices, openai, services.ai sub-resources), Language Service PE (1 IP), Hub Key Vault PE (1 IP).
Principle: assign-on-first-deploy, sticky forever. Changing pe_subnet_key after deployment destroys and recreates all 5 tenant PEs (service disruption + DNS re-propagation).
Capacity math:
/24 PE subnet holds ~251 usable IPs (Azure reserves 5)/24 subnetAssignment rules for new tenants:
az network vnet subnet show)pe_subnet_key field — it is immutable after first apply"privateendpoints-subnet"Tenant onboarding prerequisite:
Every new tenant tfvars must include pe_subnet_key inside the tenant = { ... } block. Terraform plan will fail validation if it is missing. Example:
pe_subnet_key = "privateendpoints-subnet" # or "privateendpoints-subnet-1", etc.
APIM stack — Pinned PE subnet:
var.apim_pe_subnet_key (if set, looks up from shared PE pool)private_endpoint_subnet_idKey-rotation / Foundry — Out of PE pool scope (no PE subnet references).
params/{env}/shared.tfvars): Add CIDR entry under subnet_allocation in the appropriate address spacemodules/network/locals.tf): Add xxx_enabled = contains(keys(local.subnet_cidrs), "xxx-subnet")modules/network/locals.tf): Add xxx_subnet_cidr = local.xxx_enabled ? local.subnet_cidrs["xxx-subnet"] : nullmodules/network/variables.tf): Add subnet name to allowed names list in validation blockmodules/network/main.tf): NSG with count, azapi_resource with delegation + depends_on all preceding subnetsmodules/network/outputs.tf): xxx_subnet_id, xxx_subnet_cidr, xxx_nsg_idstacks/shared/main.tf + outputs.tf): Wire variable + expose outputtry(data.terraform_remote_state.shared.outputs.xxx_subnet_id, null)subnet_allocation must be valid CIDRs (can(cidrhost(cidr, 0)))privateendpoints-subnet-<n> pattern where <n> starts at 1privateendpoints-subnet must existazapi_resource body, not a separate associationterraform fmt -recursive on modules/network/ and stacks/shared/For full CIDR calculation algorithm, visual allocation diagrams, NSG rule tables per subnet, depends_on chain details, AppGW route table special case, and common pitfalls, see references/REFERENCE.md.