Skip to content

Releases: cloudposse/terraform-aws-dynamic-subnets

v3.0.1

04 Nov 00:59

Choose a tag to compare

🚀 Enhancements

Fix NAT routing when max_nats limits NATs to fewer AZs @aknysh (#227) ## what
  • Fixed critical bug in NAT Gateway routing when max_nats is 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-gateways demonstrating the max_nats feature
  • Added 3 new test functions providing 100% test coverage for max_nats feature
  • 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:

  1. Bug Fix (main.tf):

    • Fixed private_route_table_to_nat_map calculation
    • Fixed public_route_table_to_nat_map calculation
    • Added explanatory comments and example scenarios
  2. 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
  3. Test Coverage (test/src/examples_limited_nat_gateways_test.go):

    • TestExamplesLimitedNatGateways - Tests max_nats=1
    • TestExamplesLimitedNatGatewaysTwoNats - Tests max_nats=2
    • TestExamplesLimitedNatGatewaysDisabled - Tests enabled=false
    • Brings max_nats test coverage from 0% to 100%
  4. 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_azs for cost optimization
  • Test Coverage Analysis: /docs/test-coverage-analysis.md
  • Detailed PRD: /docs/prd/fix-max-nats-routing.md

v3.0.0

02 Nov 22:53

Choose a tag to compare

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:

  1. 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_names allow independent control while falling back to original variables for backward compatibility

  2. 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

  3. Named NAT Placement: Variable nat_gateway_public_subnet_names allows intuitive placement like ["loadbalancer"] instead of remembering indices

  4. 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-harness with atmos for 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` files

why

  • Re-apply .github/settings.yml from org level to get terratest environment
  • Migrate to new test account

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` files

why

  • Re-apply .github/settings.yml from 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 PR

why

  • 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` repo

why

  • 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` action

why

  • 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/.github repository
  • 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.FromError now returns an error with codes.Unknown when the error implements the GRPCStatus() method, and calling GRPCStatus() returns nil. (#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 WithIdleTimeout dial 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)
  • 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
Read more

v2.4.2

07 Mar 23:03
6b3bb0f

Choose a tag to compare

🚀 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

Compare Source

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

Compare Source

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

11 Jun 23:00
236168c

Choose a tag to compare

🚀 Enhancements

Add subnet ARNs to outputs @Nuru (#188)

what

  • Add subnet ARNs to outputs

why

v2.4.0

11 Jun 21:59
fde090c

Choose a tag to compare

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 null as meaning "default")
  • Remove aws_eip vpc = 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

v2.3.0

18 May 18:11
ec00f45

Choose a tag to compare

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:AWS012 style comments
  • False positives generated by this module have been ignored
Fix tflint @max-lobur (#180)

what

Fix tflint

why

Maintenance

Sync github @max-lobur (#179)

Rebuild github dir from the template

v2.2.0

17 May 09:18
029080a

Choose a tag to compare

  • No changes

v2.1.0

27 Jan 04:37
029080a

Choose a tag to compare

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_count variable). If subnets_per_az_count is set to 1 (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_names variable)

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 tgw subnet and firewall subnets 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

04 Oct 02:38
b132e47

Choose a tag to compare

🚀 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

12 Aug 13:47
d11bd52

Choose a tag to compare

🚀 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)