From 4d37b2b629430bb9dc928b36d7a723cb6e432927 Mon Sep 17 00:00:00 2001 From: Manoj-Kumar-Selvaraj Date: Fri, 7 Nov 2025 23:39:26 +0530 Subject: [PATCH 1/2] fix(nat): ensure NAT gateways are created in correct availability zones (#1257) --- CHANGELOG.md | 6 ++++++ main.tf | 32 ++++++++++++++++++++++++++++---- variables.tf | 25 +++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b67ad701c..a8177e529 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## [Unreleased] + +### Bug Fixes + +* fix: Ensure NAT gateways are created in the correct availability zones ([#1257](https://github.com/terraform-aws-modules/terraform-aws-vpc/issues/1257)) + ## [6.5.0](https://github.com/terraform-aws-modules/terraform-aws-vpc/compare/v6.4.1...v6.5.0) (2025-10-21) ### Features diff --git a/main.tf b/main.tf index 31deb5988..9fdeb986c 100644 --- a/main.tf +++ b/main.tf @@ -1199,9 +1199,33 @@ resource "aws_route" "private_ipv6_egress" { # NAT Gateway ################################################################################ +data "aws_availability_zones" "available" { + count = length(var.azs) == 0 ? 1 : 0 + + state = "available" +} + locals { - nat_gateway_count = var.single_nat_gateway ? 1 : var.one_nat_gateway_per_az ? length(var.azs) : local.max_subnet_length - nat_gateway_ips = var.reuse_nat_ips ? var.external_nat_ip_ids : aws_eip.nat[*].id + # Use provided AZs or fetch from data source, then sort for consistency + sorted_azs = sort(length(var.azs) > 0 ? var.azs : data.aws_availability_zones.available[0].names) + + # Build mapping AZ -> first public subnet ID found for that AZ + az_to_public_subnet = { + for az in local.sorted_azs : az => try( + # Sort subnets within each AZ for consistent selection + sort([for s in aws_subnet.public : s.id if s.availability_zone == az])[0], + aws_subnet.public[0].id + ) + } + + nat_gateway_count = var.single_nat_gateway ? 1 : var.one_nat_gateway_per_az ? length(local.sorted_azs) : local.max_subnet_length + nat_gateway_ips = var.reuse_nat_ips ? sort(var.external_nat_ip_ids) : aws_eip.nat[*].id + + # Determine NAT gateway subnet IDs to use + nat_gateway_subnet_ids = length(var.nat_gateway_subnet_ids) > 0 ? sort(var.nat_gateway_subnet_ids) : [ + for i, az in local.sorted_azs : local.az_to_public_subnet[az] + if i < local.nat_gateway_count + ] } resource "aws_eip" "nat" { @@ -1235,8 +1259,8 @@ resource "aws_nat_gateway" "this" { var.single_nat_gateway ? 0 : count.index, ) subnet_id = element( - aws_subnet.public[*].id, - var.single_nat_gateway ? 0 : count.index, + local.nat_gateway_subnet_ids, + var.single_nat_gateway ? 0 : count.index ) tags = merge( diff --git a/variables.tf b/variables.tf index ea23a3e52..209cfd30d 100644 --- a/variables.tf +++ b/variables.tf @@ -1210,6 +1210,17 @@ variable "igw_tags" { # NAT Gateway ################################################################################ +variable "nat_gateway_subnet_ids" { + description = "List of subnet IDs to use for NAT Gateways. If provided, these subnet IDs will be used (in order). If empty, the module will automatically select the public subnet that matches each Availability Zone (AZ)." + type = list(string) + default = [] + + validation { + condition = alltrue([for id in var.nat_gateway_subnet_ids : can(regex("^subnet-", id)) || id == ""]) + error_message = "NAT Gateway subnet IDs must be valid subnet IDs starting with 'subnet-' or be empty strings." + } +} + variable "enable_nat_gateway" { description = "Should be true if you want to provision NAT Gateways for each of your private networks" type = bool @@ -1234,6 +1245,20 @@ variable "one_nat_gateway_per_az" { default = false } +variable "nat_gateway_subnet_ids" { + description = < Date: Sat, 8 Nov 2025 23:55:51 +0530 Subject: [PATCH 2/2] fix(vpc): correct NAT gateway AZ mapping logic (#1257) --- README.md | 2 ++ examples/complete/main.tf | 21 ++++++++++++--------- variables.tf | 14 -------------- wrappers/main.tf | 1 + 4 files changed, 15 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 96e8267a0..e26bb2f64 100644 --- a/README.md +++ b/README.md @@ -336,6 +336,7 @@ No modules. | [aws_vpn_gateway_route_propagation.intra](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpn_gateway_route_propagation) | resource | | [aws_vpn_gateway_route_propagation.private](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpn_gateway_route_propagation) | resource | | [aws_vpn_gateway_route_propagation.public](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpn_gateway_route_propagation) | resource | +| [aws_availability_zones.available](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/availability_zones) | data source | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | | [aws_iam_policy_document.flow_log_cloudwatch_assume_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.vpc_flow_log_cloudwatch](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | @@ -488,6 +489,7 @@ No modules. | [name](#input\_name) | Name to be used on all the resources as identifier | `string` | `""` | no | | [nat\_eip\_tags](#input\_nat\_eip\_tags) | Additional tags for the NAT EIP | `map(string)` | `{}` | no | | [nat\_gateway\_destination\_cidr\_block](#input\_nat\_gateway\_destination\_cidr\_block) | Used to pass a custom destination route for private NAT Gateway. If not specified, the default 0.0.0.0/0 is used as a destination route | `string` | `"0.0.0.0/0"` | no | +| [nat\_gateway\_subnet\_ids](#input\_nat\_gateway\_subnet\_ids) | List of subnet IDs to use for NAT Gateways. If provided, these subnet IDs will be used (in order). If empty, the module will automatically select the public subnet that matches each Availability Zone (AZ). | `list(string)` | `[]` | no | | [nat\_gateway\_tags](#input\_nat\_gateway\_tags) | Additional tags for the NAT gateways | `map(string)` | `{}` | no | | [one\_nat\_gateway\_per\_az](#input\_one\_nat\_gateway\_per\_az) | Should be true if you want only one NAT Gateway per availability zone. Requires `var.azs` to be set, and the number of `public_subnets` created to be greater than or equal to the number of availability zones specified in `var.azs` | `bool` | `false` | no | | [outpost\_acl\_tags](#input\_outpost\_acl\_tags) | Additional tags for the outpost subnets network ACL | `map(string)` | `{}` | no | diff --git a/examples/complete/main.tf b/examples/complete/main.tf index b4a8c012c..6ebacfd89 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -9,8 +9,8 @@ locals { region = "eu-west-1" vpc_cidr = "10.0.0.0/16" - azs = slice(data.aws_availability_zones.available.names, 0, 3) - + azs = slice(sort(data.aws_availability_zones.available.names), 0, 3) + #azs = slice(sort(data.aws_availability_zones.available.names), 0, 2) #Tested with 2 AZs tags = { Example = local.name GithubRepo = "terraform-aws-vpc" @@ -28,9 +28,11 @@ module "vpc" { name = local.name cidr = local.vpc_cidr - azs = local.azs - private_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k)] - public_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 4)] + azs = local.azs + private_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k)] + #4 Subnets created for 3 AZs to test the NAT gateways are created in correct availability zone + public_subnets = [for i in range(4) : cidrsubnet(local.vpc_cidr, 8, i + 4)] + #public_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 4)] database_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 8)] elasticache_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 12)] redshift_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 16)] @@ -41,19 +43,20 @@ module "vpc" { database_subnet_names = ["DB Subnet One"] elasticache_subnet_names = ["Elasticache Subnet One", "Elasticache Subnet Two"] redshift_subnet_names = ["Redshift Subnet One", "Redshift Subnet Two", "Redshift Subnet Three"] - intra_subnet_names = [] create_database_subnet_group = false manage_default_network_acl = false manage_default_route_table = false manage_default_security_group = false + # NAT Gateway Configuration + enable_nat_gateway = true + one_nat_gateway_per_az = true # Module will automatically map NAT Gateways to first public subnet in each AZ + single_nat_gateway = false + enable_dns_hostnames = true enable_dns_support = true - enable_nat_gateway = true - single_nat_gateway = true - customer_gateways = { IP1 = { bgp_asn = 65112 diff --git a/variables.tf b/variables.tf index 209cfd30d..34c9c7250 100644 --- a/variables.tf +++ b/variables.tf @@ -1245,20 +1245,6 @@ variable "one_nat_gateway_per_az" { default = false } -variable "nat_gateway_subnet_ids" { - description = <