Releases: cloudposse/terraform-aws-dynamic-subnets
v3.0.1
🚀 Enhancements
Fix NAT routing when max_nats limits NATs to fewer AZs @aknysh (#227)
## what- Fixed critical bug in NAT Gateway routing when
max_natsis set to fewer than the number of Availability Zones - Added modulo operation to route table mapping formulas to clamp NAT indices to available NATs
- Created new example
limited-nat-gatewaysdemonstrating themax_natsfeature - Added 3 new test functions providing 100% test coverage for
max_natsfeature - Added comprehensive documentation including PRD with diagrams and decision tree
why
Critical Bug: When max_nats < num_azs, Terraform failed with "Invalid index" error because route tables in AZs without NATs attempted to reference non-existent NAT Gateway indices.
Example Failure:
Configuration: 3 AZs, max_nats=1 (only 1 NAT in AZ-a)
Error: aws_nat_gateway.default[1] - Invalid index
Route tables in AZ-b and AZ-c tried to access NAT[1] and NAT[2] which don't exist
Root Cause: The route table mapping formula calculated:
az_index * nats_per_az + subnet_offset
This produced indices [0, 1, 2] but only NAT[0] existed.
Fix: Added modulo operation to wrap indices to available NATs:
(az_index * nats_per_az + subnet_offset) % total_nats
Now produces [0, 0, 0] - all route to the single NAT.
Test Coverage Gap: The max_nats feature had ZERO test coverage. None of the 6 existing examples tested this scenario. The bug was discovered by the aws-vpc component test suite, not by this module's own tests.
Changes Include:
-
Bug Fix (
main.tf):- Fixed
private_route_table_to_nat_mapcalculation - Fixed
public_route_table_to_nat_mapcalculation - Added explanatory comments and example scenarios
- Fixed
-
New Test Example (
examples/limited-nat-gateways):- Tests 3 AZs with max_nats=1 (the failing scenario)
- Tests 3 AZs with max_nats=2 (between scenario)
- Includes comprehensive README with cost analysis
- Documents use case: Dev/test cost optimization
-
Test Coverage (
test/src/examples_limited_nat_gateways_test.go):TestExamplesLimitedNatGateways- Tests max_nats=1TestExamplesLimitedNatGatewaysTwoNats- Tests max_nats=2TestExamplesLimitedNatGatewaysDisabled- Tests enabled=false- Brings max_nats test coverage from 0% to 100%
-
Documentation:
- Test Coverage Analysis: Comprehensive audit of all tests, identifies gaps
- PRD: Detailed problem statement, solution, cost analysis
- NAT Placement Diagrams: 4 strategy diagrams with ASCII art
- Decision Tree: Guides users to optimal configuration
- Best Practices: Recommendations by environment type
Cost Implications:
The max_nats feature enables significant cost savings in non-production environments:
- Standard (3 NATs): $97.20/month
- Limited (1 NAT): $32.40/month
- Savings: $64.80/month per environment (67% reduction)
- 10 dev environments: $7,776/year savings
This bug blocked users from utilizing this cost optimization feature.
references
- Related to #226 (Separate Public/Private Subnet Configuration)
- Discovered by: cloudposse-terraform-components/aws-vpc test suite
- Affects: All users attempting to use
max_nats < num_azsfor cost optimization - Test Coverage Analysis:
/docs/test-coverage-analysis.md - Detailed PRD:
/docs/prd/fix-max-nats-routing.md
v3.0.0
Separate Public/Private Subnet Configuration and Enhance NAT Gateway Placement @aknysh (#226)
## what- Add ability to configure different numbers of public and private subnets per Availability Zone independently
- Add controlled NAT Gateway placement by subnet index to reduce costs
- Add intuitive NAT Gateway placement by subnet name for better usability
- Fix critical NAT Gateway placement bug causing wrong AZ distribution
- Fix cross-AZ routing issue where private subnets routed to NATs in different AZs
- Add comprehensive examples demonstrating cost-optimized and high-availability configurations
- Add full test coverage with Terratest for all new features
- Maintain 100% backward compatibility with existing configurations
why
User Pain Points:
- Users were forced to create equal numbers of public and private subnets, even when workloads didn't require it
- NAT Gateways were created in every public subnet, resulting in unnecessarily high AWS costs (~$32/month per NAT)
- No control over which public subnets received NAT Gateways
- Index-based configuration was not intuitive for users who assigned names to subnets
- Critical bugs caused NAT Gateways to be placed in wrong AZs and private subnets to route across AZ boundaries
Business Impact:
- Cost Optimization: Reducing from 6 NATs to 3 NATs saves $96/month (50% reduction)
- Flexibility: Users can now match subnet configuration to their actual workload requirements
- Reliability: Fixes ensure NAT Gateways are correctly distributed across AZs and routing stays within same AZ
- Usability: Name-based placement is more intuitive and maintainable than index-based placement
Key Features:
-
Separate Public/Private Subnet Counts: New variables
public_subnets_per_az_count,public_subnets_per_az_names,private_subnets_per_az_count,private_subnets_per_az_namesallow independent control while falling back to original variables for backward compatibility -
Controlled NAT Placement by Index: Variable
nat_gateway_public_subnet_indices(default[0]) specifies which subnet position(s) in each AZ receive NAT Gateways, enabling cost optimization -
Named NAT Placement: Variable
nat_gateway_public_subnet_namesallows intuitive placement like["loadbalancer"]instead of remembering indices -
Bug Fixes: Corrected NAT Gateway global index calculation and route table mapping to ensure proper AZ distribution and same-AZ routing
Examples Included:
examples/separate-public-private-subnets/: Cost-optimized with 1 NAT per AZ (~$110/month)examples/redundant-nat-gateways/: High-availability with 2 NATs per AZ (~$140/month)
Test Coverage:
- Full Terratest coverage for both examples
- Tests for name-based and index-based NAT placement
- Tests for disabled state (no resources created)
- Verification of all outputs, subnet counts, NAT counts, and route table mappings
references
- Comprehensive PRD:
docs/prd/separate-public-private-subnets-and-nat-placement.md
🤖 Automatic Updates
Fix go version in tests @osterman (#222)
## what - Update go `1.24`why
- Error loading shared library libresolv.so.2 in Go 1.20
References
Replace Makefile with atmos.yaml @osterman (#221)
## what - Remove `Makefile` - Add `atmos.yaml`why
- Replace
build-harnesswithatmosfor readme genration
References
- DEV-3229 Migrate from build-harness to atmos
Migrate new test account @osterman (#215)
## what - Update `.github/settings.yml` - Update `.github/chatops.yml` fileswhy
- Re-apply
.github/settings.ymlfrom org level to getterratestenvironment - Migrate to new
testaccount
References
- DEV-388 Automate clean up of test account in new organization
- DEV-387 Update terratest to work on a shared workflow instead of a dispatch action
- DEV-386 Update terratest to use new testing account with GitHub OIDC
Update .github/settings.yml @osterman (#214)
## what - Update `.github/settings.yml` - Drop `.github/auto-release.yml` fileswhy
- Re-apply
.github/settings.ymlfrom org level - Use organization level auto-release settings
references
- DEV-1242 Add protected tags with Repository Rulesets on GitHub
Update release workflow to allow pull-requests: write @osterman (#211)
## what - Update workflow (`.github/workflows/release.yaml`) to have permission to comment on PRwhy
- So we can support commenting on PRs with a link to the release
Update GitHub Workflows to use shared workflows from '.github' repo @osterman (#210)
## what - Update workflows (`.github/workflows`) to use shared workflows from `.github` repowhy
- Reduce nested levels of reusable workflows
Update GitHub Workflows to Fix ReviewDog TFLint Action @osterman (#209)
## what - Update workflows (`.github/workflows`) to add `issue: write` permission needed by ReviewDog `tflint` actionwhy
- The ReviewDog action will comment with line-level suggestions based on linting failures
Update GitHub workflows @osterman (#208)
## what - Update workflows (`.github/workflows/settings.yaml`)why
- Support new readme generation workflow.
- Generate banners
Use GitHub Action Workflows from `cloudposse/.github` Repo @osterman (#202)
## what- Install latest GitHub Action Workflows
why
- Use shared workflows from
cldouposse/.githubrepository - Simplify management of workflows from centralized hub of configuration
Bump google.golang.org/grpc from 1.51.0 to 1.56.3 in /test/src @[dependabot[bot]](https://github.com/apps/dependabot) (#200)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.51.0 to 1.56.3.Release notes
Sourced from google.golang.org/grpc's releases.
Release 1.56.3
Security
server: prohibit more than MaxConcurrentStreams handlers from running at once (CVE-2023-44487)
In addition to this change, applications should ensure they do not leave running tasks behind related to the RPC before returning from method handlers, or should enforce appropriate limits on any such work.
Release 1.56.2
- status: To fix a panic,
status.FromErrornow returns an error withcodes.Unknownwhen the error implements theGRPCStatus()method, and callingGRPCStatus()returnsnil. (#6374)Release 1.56.1
- client: handle empty address lists correctly in addrConn.updateAddrs
Release 1.56.0
New Features
- client: support channel idleness using
WithIdleTimeoutdial option (#6263)
- This feature is currently disabled by default, but will be enabled with a 30 minute default in the future.
- client: when using pickfirst, keep channel state in TRANSIENT_FAILURE until it becomes READY (gRFC A62) (#6306)
- xds: Add support for Custom LB Policies (gRFC A52) (#6224)
- xds: support pick_first Custom LB policy (gRFC A62) (#6314) (#6317)
- client: add support for pickfirst address shuffling (gRFC A62) (#6311)
- xds: Add support for String Matcher Header Matcher in RDS (#6313)
- xds/outlierdetection: Add Channelz Logger to Outlier Detection LB (#6145)
- Special Thanks:
@s-matyukevich- xds: enable RLS in xDS by default (#6343)
- orca: add support for application_utilization field and missing range checks on several metrics setters
- balancer/weightedroundrobin: add new LB policy for balancing between backends based on their load reports (gRFC A58) (#6241)
- authz: add conversion of json to RBAC Audit Logging config (#6192)
- authz: add support for stdout logger (#6230 and
v2.4.2
🚀 Enhancements
chore(deps): update terraform cloudposse/utils/aws to v1.4.0 (main) @renovate (#191)
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
| cloudposse/utils/aws (source) | module | minor | 1.3.0 -> 1.4.0 |
Release Notes
cloudposse/terraform-aws-utils (cloudposse/utils/aws)
v1.4.0
Add il-central-1 region @jasonmk (#31)
what
Add new Tel Aviv (il-central-1) region
why
Provide full coverage
references
Sync github @max-lobur (#27)
Rebuild github dir from the template
🤖 Automatic Updates
chore(deps): update terraform cloudposse/utils/aws to v1.4.0 (main) @renovate (#191)
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
| cloudposse/utils/aws (source) | module | minor | 1.3.0 -> 1.4.0 |
Release Notes
cloudposse/terraform-aws-utils (cloudposse/utils/aws)
v1.4.0
Add il-central-1 region @jasonmk (#31)
what
Add new Tel Aviv (il-central-1) region
why
Provide full coverage
references
Sync github @max-lobur (#27)
Rebuild github dir from the template
Update README.md and docs @cloudpossebot (#189)
what
This is an auto-generated PR that updates the README.md and docs
why
To have most recent changes of README.md and doc from origin templates
v2.4.1
v2.4.0
Update dependencies, remove deprecation, add NACL example @Nuru (#184)
Note
Dropping support for deprecated EC2-Classic
With this release, EIPs allocated for NAT ingress are allocated in the default domain. This most likely does not affect you, but for accounts created before 2013-12-04 (almost 10 years ago as of this writing), the default domain could be EC2-Classic rather than the current VPC. Previously this module forced the EIPs to be in the VPC domain, but the breaking changes between AWS Provider v4 and v5 make that difficult.
If you find yourself in the rare situation where the EIPs allocated by this module are in EC2-Classic but you want them in VPC, then create the EIPs outside of this module and supply them to this module via nat_elastic_ips.
Custom NACLs
This release includes an example (examples/nacls/) showing how to create custom NACLs in conjunction with this module. Note that by default, this module creates wide-open NACLs, and subnets can only have one NACL associated with them. If you try to add a NACL to a subnet without disabling the default NACLs, you may get a possibly confusing error like:
│ Error: creating EC2 Network ACL: creating EC2 Network ACL (acl-0376c5f12dd9d784d) Association: InvalidAssociationID.NotFound: The association ID 'aclassoc-0818d5a9e3876a2bb' does not exist
See hashicorp/terraform-provider-aws#31888
what
- Make appropriate inputs non-nullable (treat an input of
nullas meaning "default") - Remove
aws_eipvpc = true - Update terraform cloudposse/utils/aws to v1.3.0 (Supersedes and closes #182)
- Add example of how to add custom NACLs to subnets created by this module (Supersedes and closes #176)
- Update tests and test framework
why
- Allow better, more consistent configuration
- Deprecated
- Include support for new AWS regions
- Encourage composition of modules and resources rather than aggregation of functionality into bloated modules (c.f. #176)
- Stay current with features, bug fixes, and security updates
references
- Terraform AWS Provider Version 5 Upgrade Guide: aws_eip
- Disallowing Null Input Values
- EC2 Classic
v2.3.0
tfsec ignores added/fixed @davenicoll (#177)
what
- Changed tfsec ignore comments to use the rule name, rather than deprecated IDs
- Added ignores to public and private so that tfsec passes the module without CRITICAL issues
why
- tfsec no longer supports
#tfsec:ignore:AWS012style comments - False positives generated by this module have been ignored
Sync github @max-lobur (#179)
Rebuild github dir from the template
v2.2.0
- No changes
v2.1.0
Multiple subnets per AZ. Named subnets @aknysh (#174)
what
- Allow provisioning multiple subnets per AZ (the number of subnets per AZ is specified in the
subnets_per_az_countvariable). Ifsubnets_per_az_countis set to1(default), it's backwards compatible with the previous functionality (one subnet of each type, private and public, per AZ) - Allow named subnets (specified in the
subnets_per_az_namesvariable)
why
- Multiple subnets per AZ are useful in many cases:
- In a VPC, provision a dedicated subnet for services, backend and database
- For AWS Network Firewall, a dedicated subnet in each AZ is required. When a Transit Gateway is used, we provision
tgwsubnet andfirewallsubnets in each AZ
- Named subnets are useful to easily find particular subnets IDs and route table IDs (both public and private) from the module outputs, e.g. to find all subnets and route tables for
tgw,firewall,database,services,backend, etc.
test
Using the following settings:
availability_zones = ["us-east-2a", "us-east-2b"]
subnets_per_az_count = 3
subnets_per_az_names = ["services", "backend", "db"]The outputs:
az_private_route_table_ids_map = {
"us-east-2a" = [
"rtb-05cbce79950652f38",
"rtb-03a545f25ef6ce3f9",
"rtb-0ef8d1698f424e77b",
]
"us-east-2b" = [
"rtb-076348138f550ebab",
"rtb-0bd3baf8916948c3f",
"rtb-01533922e675db6b6",
]
}
az_private_subnets_map = {
"us-east-2a" = [
"subnet-02c63d0c0c2f84bf5",
"subnet-0393680d8ea3dd70f",
"subnet-0a7c4b117b2105a69",
]
"us-east-2b" = [
"subnet-0f6d042c659cc1346",
"subnet-06764c7316567eacc",
"subnet-074fd7ad2b902bec2",
]
}
az_public_route_table_ids_map = {
"us-east-2a" = [
"rtb-0046629cc751e775d",
"rtb-0046629cc751e775d",
"rtb-0046629cc751e775d",
]
"us-east-2b" = [
"rtb-0046629cc751e775d",
"rtb-0046629cc751e775d",
"rtb-0046629cc751e775d",
]
}
az_public_subnets_map = {
"us-east-2a" = [
"subnet-05647fc1f31a30896",
"subnet-03e27e41e0b818080",
"subnet-04e5d57b1e2035c7c",
]
"us-east-2b" = [
"subnet-01cc440339718014e",
"subnet-00155e6b64925ba51",
"subnet-0a326693cfee8e68d",
]
}
named_private_route_table_ids_map = {
"backend" = tolist([
"rtb-03a545f25ef6ce3f9",
"rtb-0bd3baf8916948c3f",
])
"db" = tolist([
"rtb-0ef8d1698f424e77b",
"rtb-01533922e675db6b6",
])
"services" = tolist([
"rtb-05cbce79950652f38",
"rtb-076348138f550ebab",
])
}
named_private_subnets_map = {
"backend" = tolist([
"subnet-0393680d8ea3dd70f",
"subnet-06764c7316567eacc",
])
"db" = tolist([
"subnet-0a7c4b117b2105a69",
"subnet-074fd7ad2b902bec2",
])
"services" = tolist([
"subnet-02c63d0c0c2f84bf5",
"subnet-0f6d042c659cc1346",
])
}
named_private_subnets_stats_map = {
"backend" = [
{
"az" = "us-east-2a"
"route_table_id" = "rtb-03a545f25ef6ce3f9"
"subnet_id" = "subnet-0393680d8ea3dd70f"
},
{
"az" = "us-east-2b"
"route_table_id" = "rtb-0bd3baf8916948c3f"
"subnet_id" = "subnet-06764c7316567eacc"
},
]
"db" = [
{
"az" = "us-east-2a"
"route_table_id" = "rtb-0ef8d1698f424e77b"
"subnet_id" = "subnet-0a7c4b117b2105a69"
},
{
"az" = "us-east-2b"
"route_table_id" = "rtb-01533922e675db6b6"
"subnet_id" = "subnet-074fd7ad2b902bec2"
},
]
"services" = [
{
"az" = "us-east-2a"
"route_table_id" = "rtb-05cbce79950652f38"
"subnet_id" = "subnet-02c63d0c0c2f84bf5"
},
{
"az" = "us-east-2b"
"route_table_id" = "rtb-076348138f550ebab"
"subnet_id" = "subnet-0f6d042c659cc1346"
},
]
}
named_public_route_table_ids_map = {
"backend" = tolist([
"rtb-0046629cc751e775d",
"rtb-0046629cc751e775d",
])
"db" = tolist([
"rtb-0046629cc751e775d",
"rtb-0046629cc751e775d",
])
"services" = tolist([
"rtb-0046629cc751e775d",
"rtb-0046629cc751e775d",
])
}
named_public_subnets_map = {
"backend" = tolist([
"subnet-03e27e41e0b818080",
"subnet-00155e6b64925ba51",
])
"db" = tolist([
"subnet-04e5d57b1e2035c7c",
"subnet-0a326693cfee8e68d",
])
"services" = tolist([
"subnet-05647fc1f31a30896",
"subnet-01cc440339718014e",
])
}
named_public_subnets_stats_map = {
"backend" = [
{
"az" = "us-east-2a"
"route_table_id" = "rtb-0046629cc751e775d"
"subnet_id" = "subnet-03e27e41e0b818080"
},
{
"az" = "us-east-2b"
"route_table_id" = "rtb-0046629cc751e775d"
"subnet_id" = "subnet-00155e6b64925ba51"
},
]
"db" = [
{
"az" = "us-east-2a"
"route_table_id" = "rtb-0046629cc751e775d"
"subnet_id" = "subnet-04e5d57b1e2035c7c"
},
{
"az" = "us-east-2b"
"route_table_id" = "rtb-0046629cc751e775d"
"subnet_id" = "subnet-0a326693cfee8e68d"
},
]
"services" = [
{
"az" = "us-east-2a"
"route_table_id" = "rtb-0046629cc751e775d"
"subnet_id" = "subnet-05647fc1f31a30896"
},
{
"az" = "us-east-2b"
"route_table_id" = "rtb-0046629cc751e775d"
"subnet_id" = "subnet-01cc440339718014e"
},
]
}
private_route_table_ids = [
"rtb-05cbce79950652f38",
"rtb-03a545f25ef6ce3f9",
"rtb-0ef8d1698f424e77b",
"rtb-076348138f550ebab",
"rtb-0bd3baf8916948c3f",
"rtb-01533922e675db6b6",
]
private_subnet_cidrs = tolist([
"172.16.0.0/21",
"172.16.8.0/21",
"172.16.16.0/21",
"172.16.24.0/21",
"172.16.32.0/21",
"172.16.40.0/21",
])
public_subnet_cidrs = tolist([
"172.16.72.0/21",
"172.16.80.0/21",
"172.16.88.0/21",
"172.16.96.0/21",
"172.16.104.0/21",
"172.16.112.0/21",
])v2.0.4
🚀 Enhancements
chore(deps): update terraform cloudposse/utils/aws to v1.1.0 @renovate (#169)
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
| cloudposse/utils/aws (source) | module | minor | 1.0.0 -> 1.1.0 |
🤖 Automatic Updates
chore(deps): update terraform cloudposse/utils/aws to v1.1.0 @renovate (#169)
This PR contains the following updates:
| Package | Type | Update | Change |
|---|---|---|---|
| cloudposse/utils/aws (source) | module | minor | 1.0.0 -> 1.1.0 |
v2.0.3
🚀 Enhancements
docs: update ipv4_cidr_block to a list @morremeyer (#167)
what
- Updates documentation for
ipv4_cidr_block
why
- The current documentation is wrong
additional info
I tried to run make init && make readme to generate the README, however make readme fails with:
❯ make readme
* Package gomplate already installed
* Package terraform-docs already installed
make: gomplate: No such file or directory
make: *** [readme/build] Error 1
on my machine. (MacBook Pro, macOS Monterey 12.4)