diff --git a/README.md b/README.md
index 96e8267a0..61b58ab65 100644
--- a/README.md
+++ b/README.md
@@ -241,13 +241,13 @@ Full contributing [guidelines are covered here](.github/contributing.md).
| Name | Version |
|------|---------|
| [terraform](#requirement\_terraform) | >= 1.0 |
-| [aws](#requirement\_aws) | >= 6.0 |
+| [aws](#requirement\_aws) | >= 6.24.0 |
## Providers
| Name | Version |
|------|---------|
-| [aws](#provider\_aws) | >= 6.0 |
+| [aws](#provider\_aws) | >= 6.24.0 |
## Modules
@@ -272,6 +272,7 @@ No modules.
| [aws_iam_role.vpc_flow_log_cloudwatch](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
| [aws_iam_role_policy_attachment.vpc_flow_log_cloudwatch](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
| [aws_internet_gateway.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/internet_gateway) | resource |
+| [aws_nat_gateway.regional](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/nat_gateway) | resource |
| [aws_nat_gateway.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/nat_gateway) | resource |
| [aws_network_acl.database](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/network_acl) | resource |
| [aws_network_acl.elasticache](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/network_acl) | resource |
@@ -487,6 +488,7 @@ No modules.
| [map\_public\_ip\_on\_launch](#input\_map\_public\_ip\_on\_launch) | Specify true to indicate that instances launched into the subnet should be assigned a public IP address. Default is `false` | `bool` | `false` | no |
| [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\_connectivity\_type](#input\_nat\_gateway\_connectivity\_type) | Connectivity type for the NAT Gateway. Valid values are:
- 'zonal' (default): Traditional AZ-specific NAT gateways that require public subnets
- 'regional': A single NAT Gateway that automatically scales across all AZs (does not require public subnets)
Regional NAT Gateway support requires Terraform AWS provider >= 6.24.0.
When using 'regional' mode, only one NAT Gateway is created for the entire VPC. | `string` | `"zonal"` | 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\_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 |
diff --git a/examples/regional-nat/README.md b/examples/regional-nat/README.md
new file mode 100644
index 000000000..5a595a0fd
--- /dev/null
+++ b/examples/regional-nat/README.md
@@ -0,0 +1,86 @@
+# Regional NAT Gateway Example
+
+This example demonstrates how to use the **Regional NAT Gateway** feature in the Terraform AWS VPC module. Regional NAT Gateways provide a highly available NAT solution that automatically scales across multiple Availability Zones within your VPC.
+
+## Key Features of Regional NAT Gateway
+
+- **Single NAT Gateway**: One NAT Gateway serves all Availability Zones in your VPC
+- **Automatic High Availability**: Automatically expands and contracts across AZs based on workload distribution
+- **No Public Subnets Required**: Regional NAT Gateways operate without requiring public subnets (though we include them here for demonstration)
+- **Simplified Management**: Single NAT Gateway ID for consistent route entries across all subnets
+- **Increased Capacity**: Supports up to 32 Elastic IP addresses per AZ (compared to 8 for zonal NAT Gateways)
+
+## Architecture
+
+This example creates:
+
+- **VPC**: Single VPC with CIDR block `10.0.0.0/16`
+- **Private Subnets**: 3 private subnets (one per Availability Zone)
+- **Public Subnets**: 3 public subnets (one per Availability Zone)
+- **Database Subnets**: 3 database subnets (one per Availability Zone)
+- **Regional NAT Gateway**: Single NAT Gateway that automatically scales across all AZs
+- **Internet Gateway**: For outbound internet connectivity
+
+## Usage
+
+To run this example you need to execute:
+
+```bash
+$ terraform init
+$ terraform plan
+$ terraform apply
+```
+
+Note that this example may create resources which can cost money (AWS Elastic IP, NAT Gateway, etc.). Run `terraform destroy` when you don't need these resources.
+
+## Configuration
+
+The key configuration for Regional NAT Gateway is:
+
+```hcl
+enable_nat_gateway = true
+nat_gateway_connectivity_type = "regional"
+```
+
+## Comparison: Regional vs Zonal NAT Gateway
+
+### Regional NAT Gateway (This Example)
+- **Count**: 1 NAT Gateway for entire VPC
+- **Route Tables**: One route table per private subnet (all route to the same NAT Gateway)
+- **Subnet Requirement**: No public subnets required
+- **Use Case**: Simplified management, automatic scaling, high availability across all AZs
+
+### Zonal NAT Gateway (Traditional)
+- **Count**: 1 NAT Gateway per AZ (or per subnet)
+- **Route Tables**: Route tables match NAT Gateway count
+- **Subnet Requirement**: Requires public subnets
+- **Use Case**: Fine-grained control, per-AZ NAT Gateways
+
+## Important Notes
+
+1. **Expansion Timing**: When deploying workloads in a new AZ, the regional NAT Gateway typically takes 15-20 minutes (up to 60 minutes) to expand to that AZ. During this period, traffic may be temporarily routed through existing AZs.
+
+2. **Private Connectivity**: Regional NAT Gateways do not support private connectivity. For workloads requiring private connectivity, continue using zonal NAT Gateways.
+
+3. **Availability**: This feature is available in all commercial AWS Regions, except for AWS GovCloud (US) Regions and China Regions.
+
+4. **Cost Considerations**: Regional NAT Gateways are charged per hour and per GB processed, similar to zonal NAT Gateways, but you only pay for one NAT Gateway instead of multiple.
+
+## Outputs
+
+After applying this configuration, you can see:
+- Single NAT Gateway ID in `natgw_ids` output (list with one element)
+- All private route tables route to the same NAT Gateway
+- One Elastic IP allocated for the regional NAT Gateway
+
+## Requirements
+
+| Name | Version |
+|------|---------|
+| terraform | >= 1.0 |
+| aws | >= 6.24.0 (required for regional NAT gateway support) |
+
+## References
+
+- [AWS Regional NAT Gateway Documentation](https://docs.aws.amazon.com/vpc/latest/userguide/nat-gateways-regional.html)
+- [AWS Blog: Introducing Amazon VPC Regional NAT Gateway](https://aws.amazon.com/blogs/networking-and-content-delivery/introducing-amazon-vpc-regional-nat-gateway/)
diff --git a/examples/regional-nat/main.tf b/examples/regional-nat/main.tf
new file mode 100644
index 000000000..80c93547f
--- /dev/null
+++ b/examples/regional-nat/main.tf
@@ -0,0 +1,37 @@
+provider "aws" {
+ region = local.region
+}
+
+data "aws_availability_zones" "available" {}
+
+locals {
+ region = "ap-south-1"
+ name = "ex-${basename(path.cwd)}"
+
+ vpc_cidr = "10.0.0.0/16"
+ azs = slice(data.aws_availability_zones.available.names, 0, 3)
+
+ tags = {
+ Example = local.name
+ }
+}
+
+module "vpc" {
+ source = "../../"
+ 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)]
+ database_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 8)]
+
+ enable_dns_hostnames = true
+ enable_dns_support = true
+
+ # Regional NAT Gateway Configuration
+ # Requires Terraform AWS provider >= 6.24.0
+ enable_nat_gateway = true
+ nat_gateway_connectivity_type = "regional"
+ tags = local.tags
+}
diff --git a/examples/regional-nat/outputs.tf b/examples/regional-nat/outputs.tf
new file mode 100644
index 000000000..8a478e9ef
--- /dev/null
+++ b/examples/regional-nat/outputs.tf
@@ -0,0 +1,75 @@
+################################################################################
+# VPC Outputs
+################################################################################
+
+output "vpc_id" {
+ description = "The ID of the VPC"
+ value = module.vpc.vpc_id
+}
+
+output "vpc_cidr_block" {
+ description = "The CIDR block of the VPC"
+ value = module.vpc.vpc_cidr_block
+}
+
+################################################################################
+# Subnet Outputs
+################################################################################
+
+output "private_subnets" {
+ description = "List of IDs of private subnets"
+ value = module.vpc.private_subnets
+}
+
+output "private_subnets_cidr_blocks" {
+ description = "List of cidr_blocks of private subnets"
+ value = module.vpc.private_subnets_cidr_blocks
+}
+
+output "private_route_table_ids" {
+ description = "List of IDs of private route tables"
+ value = module.vpc.private_route_table_ids
+}
+
+output "public_subnets" {
+ description = "List of IDs of public subnets"
+ value = module.vpc.public_subnets
+}
+
+output "database_subnets" {
+ description = "List of IDs of database subnets"
+ value = module.vpc.database_subnets
+}
+
+################################################################################
+# NAT Gateway Outputs
+################################################################################
+
+output "natgw_ids" {
+ description = "List of NAT Gateway IDs (will contain a single regional NAT Gateway)"
+ value = module.vpc.natgw_ids
+}
+
+output "nat_public_ips" {
+ description = "List of public Elastic IPs created for AWS NAT Gateway"
+ value = module.vpc.nat_public_ips
+}
+
+output "nat_ids" {
+ description = "List of allocation ID of Elastic IPs created for AWS NAT Gateway"
+ value = module.vpc.nat_ids
+}
+
+output "private_nat_gateway_route_ids" {
+ description = "List of IDs of the private nat gateway route (all route to the same regional NAT Gateway)"
+ value = module.vpc.private_nat_gateway_route_ids
+}
+
+################################################################################
+# Internet Gateway Outputs
+################################################################################
+
+output "igw_id" {
+ description = "The ID of the Internet Gateway"
+ value = module.vpc.igw_id
+}
diff --git a/examples/regional-nat/variables.tf b/examples/regional-nat/variables.tf
new file mode 100644
index 000000000..e69de29bb
diff --git a/examples/regional-nat/version.tf b/examples/regional-nat/version.tf
new file mode 100644
index 000000000..66bc86cfb
--- /dev/null
+++ b/examples/regional-nat/version.tf
@@ -0,0 +1,10 @@
+terraform {
+ required_version = ">= 1.0"
+
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = ">= 6.24.0"
+ }
+ }
+}
diff --git a/main.tf b/main.tf
index 31deb5988..456df7e41 100644
--- a/main.tf
+++ b/main.tf
@@ -320,9 +320,13 @@ resource "aws_subnet" "private" {
)
}
-# There are as many routing tables as the number of NAT gateways
+# There are as many routing tables as the number of NAT gateways (or private subnets for regional NAT gateway)
+locals {
+ private_route_table_count = local.create_private_subnets && local.max_subnet_length > 0 ? (local.nat_gateway_is_regional ? local.len_private_subnets : local.nat_gateway_count) : 0
+}
+
resource "aws_route_table" "private" {
- count = local.create_private_subnets && local.max_subnet_length > 0 ? local.nat_gateway_count : 0
+ count = local.private_route_table_count
region = var.region
@@ -330,7 +334,10 @@ resource "aws_route_table" "private" {
tags = merge(
{
- "Name" = var.single_nat_gateway ? "${var.name}-${var.private_subnet_suffix}" : format(
+ "Name" = local.nat_gateway_is_regional ? format(
+ "${var.name}-${var.private_subnet_suffix}-%s",
+ element(var.azs, count.index),
+ ) : var.single_nat_gateway ? "${var.name}-${var.private_subnet_suffix}" : format(
"${var.name}-${var.private_subnet_suffix}-%s",
element(var.azs, count.index),
)
@@ -348,7 +355,7 @@ resource "aws_route_table_association" "private" {
subnet_id = element(aws_subnet.private[*].id, count.index)
route_table_id = element(
aws_route_table.private[*].id,
- var.single_nat_gateway ? 0 : count.index,
+ local.nat_gateway_is_regional ? count.index : (var.single_nat_gateway ? 0 : count.index),
)
}
@@ -515,13 +522,13 @@ resource "aws_route" "database_internet_gateway" {
}
resource "aws_route" "database_nat_gateway" {
- count = local.create_database_route_table && !var.create_database_internet_gateway_route && var.create_database_nat_gateway_route && var.enable_nat_gateway ? var.single_nat_gateway ? 1 : local.len_database_subnets : 0
+ count = local.create_database_route_table && !var.create_database_internet_gateway_route && var.create_database_nat_gateway_route && var.enable_nat_gateway ? (local.nat_gateway_is_regional ? length(aws_route_table.database[*].id) : (var.single_nat_gateway ? 1 : local.len_database_subnets)) : 0
region = var.region
route_table_id = element(aws_route_table.database[*].id, count.index)
destination_cidr_block = "0.0.0.0/0"
- nat_gateway_id = element(aws_nat_gateway.this[*].id, count.index)
+ nat_gateway_id = local.nat_gateway_is_regional ? aws_nat_gateway.regional[0].id : element(aws_nat_gateway.this[*].id, count.index)
timeouts {
create = "5m"
@@ -529,13 +536,13 @@ resource "aws_route" "database_nat_gateway" {
}
resource "aws_route" "database_dns64_nat_gateway" {
- count = local.create_database_route_table && !var.create_database_internet_gateway_route && var.create_database_nat_gateway_route && var.enable_nat_gateway && var.enable_ipv6 && var.private_subnet_enable_dns64 ? var.single_nat_gateway ? 1 : local.len_database_subnets : 0
+ count = local.create_database_route_table && !var.create_database_internet_gateway_route && var.create_database_nat_gateway_route && var.enable_nat_gateway && var.enable_ipv6 && var.private_subnet_enable_dns64 ? (local.nat_gateway_is_regional ? length(aws_route_table.database[*].id) : (var.single_nat_gateway ? 1 : local.len_database_subnets)) : 0
region = var.region
route_table_id = element(aws_route_table.database[*].id, count.index)
destination_ipv6_cidr_block = "64:ff9b::/96"
- nat_gateway_id = element(aws_nat_gateway.this[*].id, count.index)
+ nat_gateway_id = local.nat_gateway_is_regional ? aws_nat_gateway.regional[0].id : element(aws_nat_gateway.this[*].id, count.index)
timeouts {
create = "5m"
@@ -1186,7 +1193,7 @@ resource "aws_egress_only_internet_gateway" "this" {
}
resource "aws_route" "private_ipv6_egress" {
- count = local.create_vpc && var.create_egress_only_igw && var.enable_ipv6 && local.len_private_subnets > 0 ? local.nat_gateway_count : 0
+ count = local.create_vpc && var.create_egress_only_igw && var.enable_ipv6 && local.len_private_subnets > 0 ? (local.nat_gateway_is_regional ? local.private_route_table_count : local.nat_gateway_count) : 0
region = var.region
@@ -1200,8 +1207,9 @@ resource "aws_route" "private_ipv6_egress" {
################################################################################
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
+ nat_gateway_is_regional = var.nat_gateway_connectivity_type == "regional"
+ nat_gateway_count = local.nat_gateway_is_regional ? 1 : 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
}
resource "aws_eip" "nat" {
@@ -1213,7 +1221,7 @@ resource "aws_eip" "nat" {
tags = merge(
{
- "Name" = format(
+ "Name" = local.nat_gateway_is_regional ? var.name : format(
"${var.name}-%s",
element(var.azs, var.single_nat_gateway ? 0 : count.index),
)
@@ -1226,7 +1234,7 @@ resource "aws_eip" "nat" {
}
resource "aws_nat_gateway" "this" {
- count = local.create_vpc && var.enable_nat_gateway ? local.nat_gateway_count : 0
+ count = local.create_vpc && var.enable_nat_gateway && !local.nat_gateway_is_regional ? local.nat_gateway_count : 0
region = var.region
@@ -1253,14 +1261,32 @@ resource "aws_nat_gateway" "this" {
depends_on = [aws_internet_gateway.this]
}
+resource "aws_nat_gateway" "regional" {
+ count = local.create_vpc && var.enable_nat_gateway && local.nat_gateway_is_regional ? 1 : 0
+
+ region = var.region
+ vpc_id = aws_vpc.this[0].id
+
+ connectivity_type = "public"
+ availability_mode = "regional"
+
+ tags = merge(
+ {
+ "Name" = var.name
+ },
+ var.tags,
+ var.nat_gateway_tags,
+ )
+}
+
resource "aws_route" "private_nat_gateway" {
- count = local.create_vpc && var.enable_nat_gateway && var.create_private_nat_gateway_route ? local.nat_gateway_count : 0
+ count = local.create_vpc && var.enable_nat_gateway && var.create_private_nat_gateway_route ? (local.nat_gateway_is_regional ? local.private_route_table_count : local.nat_gateway_count) : 0
region = var.region
route_table_id = element(aws_route_table.private[*].id, count.index)
destination_cidr_block = var.nat_gateway_destination_cidr_block
- nat_gateway_id = element(aws_nat_gateway.this[*].id, count.index)
+ nat_gateway_id = local.nat_gateway_is_regional ? aws_nat_gateway.regional[0].id : element(aws_nat_gateway.this[*].id, count.index)
timeouts {
create = "5m"
@@ -1268,13 +1294,13 @@ resource "aws_route" "private_nat_gateway" {
}
resource "aws_route" "private_dns64_nat_gateway" {
- count = local.create_vpc && var.enable_nat_gateway && var.enable_ipv6 && var.private_subnet_enable_dns64 ? local.nat_gateway_count : 0
+ count = local.create_vpc && var.enable_nat_gateway && var.enable_ipv6 && var.private_subnet_enable_dns64 ? (local.nat_gateway_is_regional ? local.private_route_table_count : local.nat_gateway_count) : 0
region = var.region
route_table_id = element(aws_route_table.private[*].id, count.index)
destination_ipv6_cidr_block = "64:ff9b::/96"
- nat_gateway_id = element(aws_nat_gateway.this[*].id, count.index)
+ nat_gateway_id = local.nat_gateway_is_regional ? aws_nat_gateway.regional[0].id : element(aws_nat_gateway.this[*].id, count.index)
timeouts {
create = "5m"
diff --git a/outputs.tf b/outputs.tf
index 1d1d2783a..4b016f9a8 100644
--- a/outputs.tf
+++ b/outputs.tf
@@ -521,12 +521,12 @@ output "nat_public_ips" {
output "natgw_ids" {
description = "List of NAT Gateway IDs"
- value = aws_nat_gateway.this[*].id
+ value = concat(aws_nat_gateway.this[*].id, aws_nat_gateway.regional[*].id)
}
output "natgw_interface_ids" {
description = "List of Network Interface IDs assigned to NAT Gateways"
- value = aws_nat_gateway.this[*].network_interface_id
+ value = concat(aws_nat_gateway.this[*].network_interface_id, aws_nat_gateway.regional[*].network_interface_id)
}
################################################################################
diff --git a/variables.tf b/variables.tf
index ea23a3e52..3cd6f9776 100644
--- a/variables.tf
+++ b/variables.tf
@@ -1234,6 +1234,23 @@ variable "one_nat_gateway_per_az" {
default = false
}
+variable "nat_gateway_connectivity_type" {
+ description = <<-EOT
+ Connectivity type for the NAT Gateway. Valid values are:
+ - 'zonal' (default): Traditional AZ-specific NAT gateways that require public subnets
+ - 'regional': A single NAT Gateway that automatically scales across all AZs (does not require public subnets)
+
+ Regional NAT Gateway support requires Terraform AWS provider >= 6.24.0.
+ When using 'regional' mode, only one NAT Gateway is created for the entire VPC.
+ EOT
+ type = string
+ default = "zonal"
+ validation {
+ condition = contains(["zonal", "regional"], var.nat_gateway_connectivity_type)
+ error_message = "The nat_gateway_connectivity_type must be either 'zonal' or 'regional'."
+ }
+}
+
variable "reuse_nat_ips" {
description = "Should be true if you don't want EIPs to be created for your NAT Gateways and will instead pass them in via the 'external_nat_ip_ids' variable"
type = bool
diff --git a/versions.tf b/versions.tf
index aaf26b899..66bc86cfb 100644
--- a/versions.tf
+++ b/versions.tf
@@ -4,7 +4,7 @@ terraform {
required_providers {
aws = {
source = "hashicorp/aws"
- version = ">= 6.0"
+ version = ">= 6.24.0"
}
}
}
diff --git a/wrappers/main.tf b/wrappers/main.tf
index bef0c73fc..5d87c013e 100644
--- a/wrappers/main.tf
+++ b/wrappers/main.tf
@@ -232,6 +232,7 @@ module "wrapper" {
map_public_ip_on_launch = try(each.value.map_public_ip_on_launch, var.defaults.map_public_ip_on_launch, false)
name = try(each.value.name, var.defaults.name, "")
nat_eip_tags = try(each.value.nat_eip_tags, var.defaults.nat_eip_tags, {})
+ nat_gateway_connectivity_type = try(each.value.nat_gateway_connectivity_type, var.defaults.nat_gateway_connectivity_type, "zonal")
nat_gateway_destination_cidr_block = try(each.value.nat_gateway_destination_cidr_block, var.defaults.nat_gateway_destination_cidr_block, "0.0.0.0/0")
nat_gateway_tags = try(each.value.nat_gateway_tags, var.defaults.nat_gateway_tags, {})
one_nat_gateway_per_az = try(each.value.one_nat_gateway_per_az, var.defaults.one_nat_gateway_per_az, false)
diff --git a/wrappers/versions.tf b/wrappers/versions.tf
index aaf26b899..66bc86cfb 100644
--- a/wrappers/versions.tf
+++ b/wrappers/versions.tf
@@ -4,7 +4,7 @@ terraform {
required_providers {
aws = {
source = "hashicorp/aws"
- version = ">= 6.0"
+ version = ">= 6.24.0"
}
}
}