diff --git a/README.md b/README.md index 96e8267a0..0531e4d88 100644 --- a/README.md +++ b/README.md @@ -67,9 +67,33 @@ Note that in the example we allocate 3 IPs because we will be provisioning 3 NAT If, on the other hand, `single_nat_gateway = true`, then `aws_eip.nat` would only need to allocate 1 IP. Passing the IPs into the module is done by setting two variables `reuse_nat_ips = true` and `external_nat_ip_ids = "${aws_eip.nat.*.id}"`. +**For Regional NAT Gateways:** +When using Regional NAT Gateway with `nat_gateway_connectivity_type.eip_allocation = "manual"`, the module will allocate one EIP per Availability Zone. For example, if you have 3 AZs: + +```hcl +resource "aws_eip" "regional_nat" { + count = 3 # One per AZ + + vpc = true +} + +module "vpc" { + source = "terraform-aws-modules/vpc/aws" + + enable_nat_gateway = true + nat_gateway_connectivity_type = { + availability_mode = "regional" + eip_allocation = "manual" + } + reuse_nat_ips = false +} +``` + +Alternatively, you can use `eip_allocation = "auto"` to let AWS automatically manage EIPs for the Regional NAT Gateway. + ## NAT Gateway Scenarios -This module supports three scenarios for creating NAT gateways. Each will be explained in further detail in the corresponding sections. +This module supports four scenarios for creating NAT gateways. Each will be explained in further detail in the corresponding sections. - One NAT Gateway per subnet (default behavior) - `enable_nat_gateway = true` @@ -83,9 +107,15 @@ This module supports three scenarios for creating NAT gateways. Each will be exp - `enable_nat_gateway = true` - `single_nat_gateway = false` - `one_nat_gateway_per_az = true` +- Regional NAT Gateway + - `enable_nat_gateway = true` + - `nat_gateway_connectivity_type.availability_mode = "regional"` + - `nat_gateway_connectivity_type.eip_allocation = "auto"` or `"manual"` If both `single_nat_gateway` and `one_nat_gateway_per_az` are set to `true`, then `single_nat_gateway` takes precedence. +> **Note**: Regional NAT Gateway requires Terraform AWS provider >= 6.24.0. + ### One NAT Gateway per subnet (default) By default, the module will determine the number of NAT Gateways to create based on the `max()` of the private subnet lists (`database_subnets`, `elasticache_subnets`, `private_subnets`, and `redshift_subnets`). The module **does not** take into account the number of `intra_subnets`, since the latter are designed to have no Internet access via NAT Gateway. For example, if your configuration looks like the following: @@ -111,6 +141,43 @@ If `one_nat_gateway_per_az = true` and `single_nat_gateway = false`, then the mo - The variable `var.azs` **must** be specified. - The number of public subnet CIDR blocks specified in `public_subnets` **must** be greater than or equal to the number of availability zones specified in `var.azs`. This is to ensure that each NAT Gateway has a dedicated public subnet to deploy to. +### Regional NAT Gateway + +Regional NAT Gateway is a highly available NAT solution that automatically scales across multiple Availability Zones within your VPC. It provides a single NAT Gateway that serves all Availability Zones, eliminating the need for multiple zonal NAT Gateways. + +**Key Features:** +- **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 public subnets can still be created for other purposes) +- **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) + +**Configuration:** + +```hcl +enable_nat_gateway = true +nat_gateway_connectivity_type = { + availability_mode = "regional" # "regional" or "zonal" + eip_allocation = "auto" # "auto" or "manual" +} +``` + +**EIP Allocation Options:** +- `"auto"`: AWS automatically provisions and manages EIPs for the Regional NAT Gateway +- `"manual"`: You provide EIPs via `external_nat_ip_ids` (one EIP per AZ). The module will create EIPs based on the number of AZs if `reuse_nat_ips = false` + +**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. + +**Requirements:** +- Terraform AWS provider >= 6.24.0 +- The variable `var.azs` **must** be specified + +See the [regional-nat example](examples/regional-nat/) for a complete working example. + ## "private" versus "intra" subnets By default, if NAT Gateways are enabled, private subnets will be configured with routes for Internet traffic that point at the NAT Gateways configured by use of the above options. @@ -241,13 +308,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 @@ -266,12 +333,14 @@ No modules. | [aws_default_vpc.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/default_vpc) | resource | | [aws_egress_only_internet_gateway.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/egress_only_internet_gateway) | resource | | [aws_eip.nat](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eip) | resource | +| [aws_eip.regional_nat](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/eip) | resource | | [aws_elasticache_subnet_group.elasticache](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/elasticache_subnet_group) | resource | | [aws_flow_log.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/flow_log) | resource | | [aws_iam_policy.vpc_flow_log_cloudwatch](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | | [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 | @@ -437,7 +506,7 @@ No modules. | [enable\_public\_redshift](#input\_enable\_public\_redshift) | Controls if redshift should have public routing table | `bool` | `false` | no | | [enable\_vpn\_gateway](#input\_enable\_vpn\_gateway) | Should be true if you want to create a new VPN Gateway resource and attach it to the VPC | `bool` | `false` | no | | [external\_nat\_ip\_ids](#input\_external\_nat\_ip\_ids) | List of EIP IDs to be assigned to the NAT Gateways (used in combination with reuse\_nat\_ips) | `list(string)` | `[]` | no | -| [external\_nat\_ips](#input\_external\_nat\_ips) | List of EIPs to be used for `nat_public_ips` output (used in combination with reuse\_nat\_ips and external\_nat\_ip\_ids) | `list(string)` | `[]` | no | +| [external\_nat\_ips](#input\_external\_nat\_ips) | List of EIPs to be used for `nat_public_ips` output (used in combination with reuse\_nat\_ips and external\_nat\_ip\_ids). For regional NAT gateways, EIPs will be mapped to availability zones in order. | `list(string)` | `[]` | no | | [flow\_log\_cloudwatch\_iam\_role\_arn](#input\_flow\_log\_cloudwatch\_iam\_role\_arn) | The ARN for the IAM role that's used to post flow logs to a CloudWatch Logs log group. When flow\_log\_destination\_arn is set to ARN of Cloudwatch Logs, this argument needs to be provided | `string` | `""` | no | | [flow\_log\_cloudwatch\_iam\_role\_conditions](#input\_flow\_log\_cloudwatch\_iam\_role\_conditions) | Additional conditions of the CloudWatch role assumption policy |
list(object({
test = string
variable = string
values = list(string)
})) | `[]` | no |
| [flow\_log\_cloudwatch\_log\_group\_class](#input\_flow\_log\_cloudwatch\_log\_group\_class) | Specified the log class of the log group. Possible values are: STANDARD or INFREQUENT\_ACCESS | `string` | `null` | no |
@@ -487,6 +556,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) | Configuration block for NAT Gateway connectivity type.object({
availability_mode = string # "zonal" or "regional"
eip_allocation = string # "auto" or "manual"
}) | {
"availability_mode": null,
"eip_allocation": null
} | 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..6bc9b4d56
--- /dev/null
+++ b/examples/regional-nat/README.md
@@ -0,0 +1,89 @@
+# 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 = {
+ availability_mode = "regional" # "regional" or "zonal"
+ eip_allocation = "auto" # "auto" or "manual"
+}
+```
+
+## 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..04a0d7a54
--- /dev/null
+++ b/examples/regional-nat/main.tf
@@ -0,0 +1,40 @@
+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 = {
+ availability_mode = "regional" # "regional" or "zonal"
+ eip_allocation = "auto" # "auto" or "manual", for availablility_mode = "zonal", eip_allocation won't be used
+ }
+ 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..768ebaec7 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,12 +1207,17 @@ 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.availability_mode == "regional"
+ nat_gateway_count = local.nat_gateway_is_regional ? 1 : var.single_nat_gateway || var.nat_gateway_connectivity_type.availability_mode == "zonal" ? 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
+
+ # Regional NAT Gateway EIP handling
+ # Always create EIPs automatically based on the number of AZs
+ regional_nat_gateway_eip_count = local.nat_gateway_is_regional ? length(var.azs) : 0
}
resource "aws_eip" "nat" {
- count = local.create_vpc && var.enable_nat_gateway && !var.reuse_nat_ips ? local.nat_gateway_count : 0
+ count = local.create_vpc && var.enable_nat_gateway && !local.nat_gateway_is_regional && !var.reuse_nat_ips && (var.nat_gateway_connectivity_type.availability_mode == "zonal" || var.nat_gateway_connectivity_type.availability_mode == null) ? local.nat_gateway_count : 0
region = var.region
@@ -1225,8 +1237,29 @@ resource "aws_eip" "nat" {
depends_on = [aws_internet_gateway.this]
}
+resource "aws_eip" "regional_nat" {
+ count = local.create_vpc && var.enable_nat_gateway && local.nat_gateway_is_regional && var.nat_gateway_connectivity_type.eip_allocation == "manual" && var.nat_gateway_connectivity_type.availability_mode == "regional" ? local.regional_nat_gateway_eip_count : 0
+
+ region = var.region
+
+ domain = "vpc"
+
+ tags = merge(
+ {
+ "Name" = format(
+ "${var.name}-%s",
+ element(var.azs, count.index),
+ )
+ },
+ var.tags,
+ var.nat_eip_tags,
+ )
+
+ depends_on = [aws_internet_gateway.this]
+}
+
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 +1286,42 @@ 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 = var.nat_gateway_connectivity_type.availability_mode
+
+ dynamic "availability_zone_address" {
+ for_each = var.nat_gateway_connectivity_type.eip_allocation == "manual" && var.nat_gateway_connectivity_type.availability_mode == "regional" ? {
+ for idx, az in var.azs : az => aws_eip.regional_nat[idx].id
+ } : {}
+ content {
+ allocation_ids = toset([availability_zone_address.value])
+ availability_zone = availability_zone_address.key
+ }
+ }
+
+ 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 +1329,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..7e84c4698 100644
--- a/outputs.tf
+++ b/outputs.tf
@@ -511,22 +511,25 @@ output "intra_network_acl_arn" {
output "nat_ids" {
description = "List of allocation ID of Elastic IPs created for AWS NAT Gateway"
- value = aws_eip.nat[*].id
+ value = concat(aws_eip.nat[*].id, aws_eip.regional_nat[*].id)
}
output "nat_public_ips" {
description = "List of public Elastic IPs created for AWS NAT Gateway"
- value = var.reuse_nat_ips ? var.external_nat_ips : aws_eip.nat[*].public_ip
+ value = concat(
+ var.reuse_nat_ips ? var.external_nat_ips : aws_eip.nat[*].public_ip,
+ var.nat_gateway_connectivity_type.availability_mode == "regional" ? aws_eip.regional_nat[*].public_ip : []
+ )
}
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..33278afe1 100644
--- a/variables.tf
+++ b/variables.tf
@@ -1234,6 +1234,31 @@ variable "one_nat_gateway_per_az" {
default = false
}
+variable "nat_gateway_connectivity_type" {
+ description = <<-EOT
+ Configuration block for NAT Gateway connectivity type.
+ - availability_mode: "zonal" (default) or "regional"
+ - 'zonal': 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)
+ - eip_allocation: "auto" (default) or "manual"
+ - 'auto': Automatically provision EIPs for the NAT Gateway
+ - 'manual': Will create the set of EIPs based on the number of AZs
+ EOT
+ type = object({
+ availability_mode = string # "zonal" or "regional"
+ eip_allocation = string # "auto" or "manual"
+ })
+ default = { availability_mode = null, eip_allocation = null }
+ # validation {
+ # condition = contains(["zonal", "regional"], var.nat_gateway_connectivity_type.availability_mode)
+ # error_message = "The availability_mode must be either 'zonal' or 'regional'."
+ # }
+ # validation {
+ # condition = contains(["auto", "manual"], var.nat_gateway_connectivity_type.eip_allocation)
+ # error_message = "The eip_allocation must be either 'auto' or 'manual'."
+ # }
+}
+
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
@@ -1247,7 +1272,7 @@ variable "external_nat_ip_ids" {
}
variable "external_nat_ips" {
- description = "List of EIPs to be used for `nat_public_ips` output (used in combination with reuse_nat_ips and external_nat_ip_ids)"
+ description = "List of EIPs to be used for `nat_public_ips` output (used in combination with reuse_nat_ips and external_nat_ip_ids). For regional NAT gateways, EIPs will be mapped to availability zones in order."
type = list(string)
default = []
}
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..0eb2cc95b 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, { availability_mode = null, eip_allocation = null })
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"
}
}
}