Skip to content

Commit 181e084

Browse files
author
Cristobal
committed
feat: default calculated_subnet_az_capacity to 3 AZ slots
Three AZs is the most common regional layout; the previous default of 6 reserved slots most deployments never use and broke small VPCs (e.g. a /24 inspection VPC with three /28 groups). Regions with more AZs can raise the value up to 6. The value is fixed at creation time: changing it on an existing VPC renumbers calculated subnets. Documented in the variable description, docs/addressing.md, and the CHANGELOG. Tests: existing slot-stability runs pin the value they were written against (6); two new runs exercise the new default (full three-slot allocation within a /24, and 1->2 AZ scale-up without renumbering). Suite: 182 passed, 0 failed. Mutation check: reverting the default to 6 fails 4 runs.
1 parent 2f7d2ea commit 181e084

8 files changed

Lines changed: 127 additions & 44 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ All notable changes to this module are documented in this file. The format follo
2525
- Moved NAT configuration to a VPC-wide contract and made subnet groups opt into NAT routing independently.
2626
- Made isolated subnet roles fail closed for Internet, NAT, TGW, Cloud WAN, and generic routes; injected isolated route tables require an explicit caller-responsibility opt-in.
2727
- Split outputs into stable Tier 1 handles, deprecated Tier 2 v4 compatibility aliases, and unstable Tier 3 provider-shaped collections.
28+
- Set `calculated_subnet_az_capacity` to a default of 3 reserved AZ slots per calculated subnet group, matching the most common regional layout; regions with more AZs can raise it up to 6. The value is fixed at creation time: changing it on an existing VPC renumbers calculated subnets.
2829

2930
### Deprecated
3031

README.md

Lines changed: 2 additions & 1 deletion
Large diffs are not rendered by default.

docs/addressing.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ Each subnet group selects exactly one IPv4 mode:
8686
| Calculated | `netmask`, optionally `cidr_index` | Deterministic; pin groups that must not move. |
8787
| IPAM | `ipam_pool_id` and `netmask_length` | CIDR may remain unknown until apply. |
8888

89-
Calculated allocation reserves six AZ slots per group. A pinned `cidr_index` is an absolute group slot and does not move when other groups or AZs are added. Unpinned groups pack largest-first and then alphabetically, so adding or removing an earlier group can move later unpinned ranges.
89+
Calculated allocation reserves three AZ slots per group by default (`calculated_subnet_az_capacity`, raisable to six). A pinned `cidr_index` is an absolute group slot and does not move when other groups or AZs are added. Unpinned groups pack largest-first and then alphabetically, so adding or removing an earlier group can move later unpinned ranges.
9090

9191
`ipv4.secondary_cidr_key` selects the parent secondary IPv4 association. Omitting it selects the primary VPC CIDR.
9292

locals.tf

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,9 +85,10 @@ locals {
8585
for k, v in var.subnets : k => v if v.ipv4 != null && v.ipv4.cidrs_by_az != null
8686
}
8787
# ─── Deterministic IPv4 CIDR calculation by selected parent ──────────
88-
# Calculated groups reserve the caller-selected AZ capacity. Six remains the
89-
# module default; constrained wrappers can reserve fewer fixed slots when
90-
# their supported maximum is lower.
88+
# Calculated groups reserve the caller-selected AZ capacity. Three is the
89+
# module default (the most common regional layout); deployments in regions
90+
# with more AZs can raise it, and established VPCs must keep their original
91+
# value to avoid renumbering.
9192
cidr_az_stride = var.calculated_subnet_az_capacity
9293

9394
ipv4_parent_key_by_group = {

tests/cidr_engine.tftest.hcl

Lines changed: 104 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ run "unpinned_before_group_add" {
2424
command = plan
2525

2626
variables {
27-
vpc = { name = "cidr-test" }
28-
addressing = { primary = { cidr_block = "10.0.0.0/16" } }
29-
availability_zones = { names = ["us-east-1a", "us-east-1b"] }
27+
calculated_subnet_az_capacity = 6
28+
vpc = { name = "cidr-test" }
29+
addressing = { primary = { cidr_block = "10.0.0.0/16" } }
30+
availability_zones = { names = ["us-east-1a", "us-east-1b"] }
3031
subnets = {
3132
alpha = { role = "private", ipv4 = { netmask = 24 } }
3233
gamma = { role = "private", ipv4 = { netmask = 24 } }
@@ -58,9 +59,10 @@ run "unpinned_after_group_add" {
5859
command = plan
5960

6061
variables {
61-
vpc = { name = "cidr-test" }
62-
addressing = { primary = { cidr_block = "10.0.0.0/16" } }
63-
availability_zones = { names = ["us-east-1a", "us-east-1b"] }
62+
calculated_subnet_az_capacity = 6
63+
vpc = { name = "cidr-test" }
64+
addressing = { primary = { cidr_block = "10.0.0.0/16" } }
65+
availability_zones = { names = ["us-east-1a", "us-east-1b"] }
6466
subnets = {
6567
alpha = { role = "private", ipv4 = { netmask = 24 } }
6668
beta = { role = "private", ipv4 = { netmask = 24 } }
@@ -89,9 +91,10 @@ run "unpinned_after_group_remove" {
8991
command = plan
9092

9193
variables {
92-
vpc = { name = "cidr-test" }
93-
addressing = { primary = { cidr_block = "10.0.0.0/16" } }
94-
availability_zones = { names = ["us-east-1a", "us-east-1b"] }
94+
calculated_subnet_az_capacity = 6
95+
vpc = { name = "cidr-test" }
96+
addressing = { primary = { cidr_block = "10.0.0.0/16" } }
97+
availability_zones = { names = ["us-east-1a", "us-east-1b"] }
9598
subnets = {
9699
alpha = { role = "private", ipv4 = { netmask = 24 } }
97100
gamma = { role = "private", ipv4 = { netmask = 24 } }
@@ -118,9 +121,10 @@ run "pinned_before_group_add" {
118121
command = plan
119122

120123
variables {
121-
vpc = { name = "cidr-test" }
122-
addressing = { primary = { cidr_block = "10.0.0.0/16" } }
123-
availability_zones = { names = ["us-east-1a", "us-east-1b"] }
124+
calculated_subnet_az_capacity = 6
125+
vpc = { name = "cidr-test" }
126+
addressing = { primary = { cidr_block = "10.0.0.0/16" } }
127+
availability_zones = { names = ["us-east-1a", "us-east-1b"] }
124128
subnets = {
125129
alpha = { role = "private", ipv4 = { netmask = 24, cidr_index = 0 } }
126130
gamma = { role = "private", ipv4 = { netmask = 24, cidr_index = 5 } }
@@ -147,9 +151,10 @@ run "pinned_after_group_add" {
147151
command = plan
148152

149153
variables {
150-
vpc = { name = "cidr-test" }
151-
addressing = { primary = { cidr_block = "10.0.0.0/16" } }
152-
availability_zones = { names = ["us-east-1a", "us-east-1b"] }
154+
calculated_subnet_az_capacity = 6
155+
vpc = { name = "cidr-test" }
156+
addressing = { primary = { cidr_block = "10.0.0.0/16" } }
157+
availability_zones = { names = ["us-east-1a", "us-east-1b"] }
153158
subnets = {
154159
alpha = { role = "private", ipv4 = { netmask = 24, cidr_index = 0 } }
155160
beta = { role = "private", ipv4 = { netmask = 24 } }
@@ -178,9 +183,10 @@ run "pinned_after_group_remove" {
178183
command = plan
179184

180185
variables {
181-
vpc = { name = "cidr-test" }
182-
addressing = { primary = { cidr_block = "10.0.0.0/16" } }
183-
availability_zones = { names = ["us-east-1a", "us-east-1b"] }
186+
calculated_subnet_az_capacity = 6
187+
vpc = { name = "cidr-test" }
188+
addressing = { primary = { cidr_block = "10.0.0.0/16" } }
189+
availability_zones = { names = ["us-east-1a", "us-east-1b"] }
184190
subnets = {
185191
alpha = { role = "private", ipv4 = { netmask = 24, cidr_index = 0 } }
186192
gamma = { role = "private", ipv4 = { netmask = 24, cidr_index = 5 } }
@@ -207,9 +213,10 @@ run "add_availability_zone" {
207213
command = plan
208214

209215
variables {
210-
vpc = { name = "cidr-test" }
211-
addressing = { primary = { cidr_block = "10.0.0.0/16" } }
212-
availability_zones = { names = ["us-east-1a", "us-east-1b", "us-east-1c"] }
216+
calculated_subnet_az_capacity = 6
217+
vpc = { name = "cidr-test" }
218+
addressing = { primary = { cidr_block = "10.0.0.0/16" } }
219+
availability_zones = { names = ["us-east-1a", "us-east-1b", "us-east-1c"] }
213220
subnets = {
214221
alpha = { role = "private", ipv4 = { netmask = 24 } }
215222
gamma = { role = "private", ipv4 = { netmask = 24 } }
@@ -334,9 +341,10 @@ run "mixed_netmasks_pack_without_overlap" {
334341
command = plan
335342

336343
variables {
337-
vpc = { name = "cidr-test" }
338-
addressing = { primary = { cidr_block = "10.0.0.0/16" } }
339-
availability_zones = { names = ["us-east-1a", "us-east-1b"] }
344+
calculated_subnet_az_capacity = 6
345+
vpc = { name = "cidr-test" }
346+
addressing = { primary = { cidr_block = "10.0.0.0/16" } }
347+
availability_zones = { names = ["us-east-1a", "us-east-1b"] }
340348
subnets = {
341349
application = { role = "private", ipv4 = { netmask = 22 } }
342350
public = { role = "public", ipv4 = { netmask = 24 } }
@@ -404,10 +412,9 @@ run "constrained_three_az_capacity_fits_three_pinned_28_groups_in_a_24" {
404412
command = plan
405413

406414
variables {
407-
vpc = { name = "inspection-24" }
408-
addressing = { primary = { cidr_block = "10.0.0.0/24" } }
409-
availability_zones = { names = ["eu-south-2a"] }
410-
calculated_subnet_az_capacity = 3
415+
vpc = { name = "inspection-24" }
416+
addressing = { primary = { cidr_block = "10.0.0.0/24" } }
417+
availability_zones = { names = ["eu-south-2a"] }
411418
subnets = {
412419
core_network = { role = "private", ipv4 = { netmask = 28, cidr_index = 0 } }
413420
firewall = { role = "private", ipv4 = { netmask = 28, cidr_index = 1 } }
@@ -424,3 +431,72 @@ run "constrained_three_az_capacity_fits_three_pinned_28_groups_in_a_24" {
424431
error_message = "A /24 must fit the three inspection /28 groups while reserving three stable AZ slots per group."
425432
}
426433
}
434+
435+
run "constrained_three_az_capacity_allocates_every_availability_zone" {
436+
command = plan
437+
438+
variables {
439+
vpc = { name = "inspection-24" }
440+
addressing = { primary = { cidr_block = "10.0.0.0/24" } }
441+
availability_zones = { names = ["eu-south-2a", "eu-south-2b", "eu-south-2c"] }
442+
subnets = {
443+
core_network = { role = "private", ipv4 = { netmask = 28, cidr_index = 0 } }
444+
firewall = { role = "private", ipv4 = { netmask = 28, cidr_index = 1 } }
445+
public = { role = "public", ipv4 = { netmask = 28, cidr_index = 2 } }
446+
}
447+
}
448+
449+
assert {
450+
condition = output.subnet_cidrs_by_group_by_az == {
451+
core_network = {
452+
eu-south-2a = "10.0.0.0/28"
453+
eu-south-2b = "10.0.0.16/28"
454+
eu-south-2c = "10.0.0.32/28"
455+
}
456+
firewall = {
457+
eu-south-2a = "10.0.0.48/28"
458+
eu-south-2b = "10.0.0.64/28"
459+
eu-south-2c = "10.0.0.80/28"
460+
}
461+
public = {
462+
eu-south-2a = "10.0.0.96/28"
463+
eu-south-2b = "10.0.0.112/28"
464+
eu-south-2c = "10.0.0.128/28"
465+
}
466+
}
467+
error_message = "Filling the three reserved AZ slots must allocate consecutive /28 blocks per group without exceeding the /24."
468+
}
469+
}
470+
471+
run "constrained_capacity_scale_up_preserves_existing_az_cidrs" {
472+
command = plan
473+
474+
variables {
475+
vpc = { name = "inspection-24" }
476+
addressing = { primary = { cidr_block = "10.0.0.0/24" } }
477+
availability_zones = { names = ["eu-south-2a", "eu-south-2b"] }
478+
subnets = {
479+
core_network = { role = "private", ipv4 = { netmask = 28, cidr_index = 0 } }
480+
firewall = { role = "private", ipv4 = { netmask = 28, cidr_index = 1 } }
481+
public = { role = "public", ipv4 = { netmask = 28, cidr_index = 2 } }
482+
}
483+
}
484+
485+
assert {
486+
condition = (
487+
output.subnet_cidrs_by_group_by_az.core_network["eu-south-2a"] == "10.0.0.0/28" &&
488+
output.subnet_cidrs_by_group_by_az.firewall["eu-south-2a"] == "10.0.0.48/28" &&
489+
output.subnet_cidrs_by_group_by_az.public["eu-south-2a"] == "10.0.0.96/28"
490+
)
491+
error_message = "Growing from one AZ to two must keep the first AZ on the same /28 blocks (scale-up without renumbering)."
492+
}
493+
494+
assert {
495+
condition = (
496+
output.subnet_cidrs_by_group_by_az.core_network["eu-south-2b"] == "10.0.0.16/28" &&
497+
output.subnet_cidrs_by_group_by_az.firewall["eu-south-2b"] == "10.0.0.64/28" &&
498+
output.subnet_cidrs_by_group_by_az.public["eu-south-2b"] == "10.0.0.112/28"
499+
)
500+
error_message = "The second AZ must take the next reserved slot of each group without touching neighbouring groups."
501+
}
502+
}

tests/ipv6.tftest.hcl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,8 @@ run "generated_dual_stack_dns64_eigw" {
7474
condition = (
7575
aws_subnet.main["public/us-east-1a"].ipv6_cidr_block == "2001:db8:4200::/64" &&
7676
aws_subnet.main["public/us-east-1b"].ipv6_cidr_block == "2001:db8:4200:1::/64" &&
77-
aws_subnet.main["app/us-east-1a"].ipv6_cidr_block == "2001:db8:4200:c::/64" &&
78-
aws_subnet.main["app/us-east-1b"].ipv6_cidr_block == "2001:db8:4200:d::/64" &&
77+
aws_subnet.main["app/us-east-1a"].ipv6_cidr_block == "2001:db8:4200:6::/64" &&
78+
aws_subnet.main["app/us-east-1b"].ipv6_cidr_block == "2001:db8:4200:7::/64" &&
7979
alltrue([for subnet in values(aws_subnet.main) : subnet.assign_ipv6_address_on_creation])
8080
)
8181
error_message = "auto_assign must plan deterministic, pinned /64s and enable address assignment."

tests/validations.tftest.hcl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -489,7 +489,7 @@ run "reject_calculated_cidr_pin_beyond_parent_capacity" {
489489
addressing = { primary = { cidr_block = "10.120.0.0/24" } }
490490
availability_zones = { names = ["us-east-1a"] }
491491
subnets = {
492-
app = { role = "private", ipv4 = { netmask = 28, cidr_index = 3 } }
492+
app = { role = "private", ipv4 = { netmask = 28, cidr_index = 6 } }
493493
}
494494
}
495495

variables.tf

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -328,13 +328,15 @@ variable "availability_zones" {
328328

329329
variable "calculated_subnet_az_capacity" {
330330
description = <<-EOT
331-
Number of AZ slots reserved per calculated subnet group. The default six
332-
preserves the v5 stability contract. A constrained wrapper may choose a
333-
lower fixed capacity when it explicitly supports fewer AZs; the value must
334-
still cover every configured AZ.
331+
Number of AZ slots reserved per calculated subnet group. The default of
332+
three matches the most common regional layout (three AZs). Deployments in
333+
regions with more AZs may raise it up to six; the value must always cover
334+
every configured AZ. Changing it on an existing VPC renumbers calculated
335+
subnets, so established deployments must keep the value they were created
336+
with.
335337
EOT
336338
type = number
337-
default = 6
339+
default = 3
338340

339341
validation {
340342
condition = (
@@ -417,7 +419,8 @@ variable "transit_gateway_attachments" {
417419
#
418420
# NETMASK STABILITY:
419421
# When using `ipv4.netmask`, CIDRs are calculated deterministically:
420-
# - Every group reserves six AZ slots, so appending AZs does not move CIDRs
422+
# - Every group reserves `calculated_subnet_az_capacity` AZ slots (default 3,
423+
# raisable to 6), so appending AZs within that capacity does not move CIDRs
421424
# - Pinned groups use absolute `cidr_index` slots and never move
422425
# - Unpinned groups pack largest-first, then alphabetically; adding/removing a
423426
# group affects unpinned groups that sort after it
@@ -489,8 +492,9 @@ variable "subnets" {
489492
cidrs_by_az = optional(map(string))
490493
ipam_pool_id = optional(string)
491494
netmask_length = optional(number)
492-
# Absolute CIDR group slot for pinning. Each slot reserves six
493-
# AZ-sized CIDRs at this netmask. Pinned ranges never move when groups or AZs
495+
# Absolute CIDR group slot for pinning. Each slot reserves
496+
# `calculated_subnet_az_capacity` AZ-sized CIDRs (default 3) at this netmask.
497+
# Pinned ranges never move when groups or AZs
494498
# are added/removed; overlapping pins across netmasks are rejected.
495499
cidr_index = optional(number)
496500
secondary_cidr_key = optional(string)

0 commit comments

Comments
 (0)