diff --git a/README.md b/README.md
index 5fdcd6e..45fa708 100644
--- a/README.md
+++ b/README.md
@@ -341,11 +341,13 @@ Please see our [developer documentation](https://github.com/aws-ia/terraform-aws
| [aws_route.ipv6_private_to_tgw](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route) | resource |
| [aws_route.ipv6_public_to_cwan](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route) | resource |
| [aws_route.ipv6_public_to_tgw](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route) | resource |
+| [aws_route.private_routes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route) | resource |
| [aws_route.private_to_cwan](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route) | resource |
| [aws_route.private_to_egress_only](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route) | resource |
| [aws_route.private_to_nat](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route) | resource |
| [aws_route.private_to_tgw](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route) | resource |
| [aws_route.public_ipv6_to_igw](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route) | resource |
+| [aws_route.public_routes](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route) | resource |
| [aws_route.public_to_cwan](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route) | resource |
| [aws_route.public_to_igw](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route) | resource |
| [aws_route.public_to_tgw](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route) | resource |
@@ -374,7 +376,7 @@ Please see our [developer documentation](https://github.com/aws-ia/terraform-aws
|------|-------------|------|---------|:--------:|
| [az\_count](#input\_az\_count) | Searches region for # of AZs to use and takes a slice based on count. Assume slice is sorted a-z. | `number` | n/a | yes |
| [name](#input\_name) | Name to give VPC. Note: does not effect subnet names, which get assigned name based on name\_prefix. | `string` | n/a | yes |
-| [subnets](#input\_subnets) | Configuration of subnets to build in VPC. 1 Subnet per AZ is created. Subnet types are defined as maps with the available keys: "private", "public", "transit\_gateway", "core\_network". Each Subnet type offers its own set of available arguments detailed below.
**Attributes shared across subnet types:**
- `cidrs` = (Optional\|list(string)) **Cannot set if `netmask` is set.** List of IPv4 CIDRs to set to subnets. Count of CIDRs defined must match quantity of azs in `az_count`.
- `netmask` = (Optional\|Int) **Cannot set if `cidrs` is set.** Netmask of the `var.cidr_block` to calculate for each subnet.
- `assign_ipv6_cidr` = (Optional\|bool) **Cannot set if `ipv6_cidrs` is set.** If true, it will calculate a /64 block from the IPv6 VPC CIDR to set in the subnets.
- `ipv6_cidrs` = (Optional\|list(string)) **Cannot set if `assign_ipv6_cidr` is set.** List of IPv6 CIDRs to set to subnets. The subnet size must use a /64 prefix length. Count of CIDRs defined must match quantity of azs in `az_count`.
- `name_prefix` = (Optional\|String) A string prefix to use for the name of your subnet and associated resources. Subnet type key name is used if omitted (aka private, public, transit\_gateway). Example `name_prefix = "private"` for `var.subnets.private` is redundant.
- `tags` = (Optional\|map(string)) Tags to set on the subnet and associated resources.
**Any private subnet type options:**
- All shared keys above
- `connect_to_public_natgw` = (Optional\|bool) Determines if routes to NAT Gateways should be created. Must also set `var.subnets.public.nat_gateway_configuration` in public subnets.
- `ipv6_native` = (Optional\|bool) Indicates whether to create an IPv6-ony subnet. Either `var.assign_ipv6_cidr` or `var.ipv6_cidrs` should be defined to allocate an IPv6 CIDR block.
- `connect_to_eigw` = (Optional\|bool) Determines if routes to the Egress-only Internet gateway should be created. Must also set `var.vpc_egress_only_internet_gateway`.
**public subnet type options:**
- All shared keys above
- `nat_gateway_configuration` = (Optional\|string) Determines if NAT Gateways should be created and in how many AZs. Valid values = `"none"`, `"single_az"`, `"all_azs"`. Default = "none". Must also set `var.subnets.private.connect_to_public_natgw = true`.
- `connect_to_igw` = (Optional\|bool) Determines if the default route (0.0.0.0/0 or ::/0) is created in the public subnets with destination the Internet gateway. Defaults to `true`.
- `ipv6_native` = (Optional\|bool) Indicates whether to create an IPv6-ony subnet. Either `var.assign_ipv6_cidr` or `var.ipv6_cidrs` should be defined to allocate an IPv6 CIDR block.
**transit\_gateway subnet type options:**
- All shared keys above
- `connect_to_public_natgw` = (Optional\|string) Determines if routes to NAT Gateways should be created. Specify the CIDR range or a prefix-list-id that you want routed to nat gateway. Usually `0.0.0.0/0`. Must also set `var.subnets.public.nat_gateway_configuration`.
- `transit_gateway_default_route_table_association` = (Optional\|bool) Boolean whether the VPC Attachment should be associated with the EC2 Transit Gateway association default route table. This cannot be configured or perform drift detection with Resource Access Manager shared EC2 Transit Gateways.
- `transit_gateway_default_route_table_propagation` = (Optional\|bool) Boolean whether the VPC Attachment should propagate routes with the EC2 Transit Gateway propagation default route table. This cannot be configured or perform drift detection with Resource Access Manager shared EC2 Transit Gateways.
- `transit_gateway_appliance_mode_support` = (Optional\|string) Whether Appliance Mode is enabled. If enabled, a traffic flow between a source and a destination uses the same Availability Zone for the VPC attachment for the lifetime of that flow. Valid values: `disable` (default) and `enable`.
- `transit_gateway_dns_support` = (Optional\|string) DNS Support is used if you need the VPC to resolve public IPv4 DNS host names to private IPv4 addresses when queried from instances in another VPC attached to the transit gateway. Valid values: `enable` (default) and `disable`.
**core\_network subnet type options:**
- All shared keys abovce
- `connect_to_public_natgw` = (Optional\|string) Determines if routes to NAT Gateways should be created. Specify the CIDR range or a prefix-list-id that you want routed to nat gateway. Usually `0.0.0.0/0`. Must also set `var.subnets.public.nat_gateway_configuration`.
- `appliance_mode_support` = (Optional\|bool) Indicates whether appliance mode is supported. If enabled, traffic flow between a source and destination use the same Availability Zone for the VPC attachment for the lifetime of that flow. Defaults to `false`.
- `require_acceptance` = (Optional\|bool) Boolean whether the core network VPC attachment to create requires acceptance or not. Defaults to `false`.
- `accept_attachment` = (Optional\|bool) Boolean whether the core network VPC attachment is accepted or not in the segment. Only valid if `require_acceptance` is set to `true`. Defaults to `true`.
Example:
subnets = {| `any` | n/a | yes | +| [subnets](#input\_subnets) | Configuration of subnets to build in VPC. 1 Subnet per AZ is created. Subnet types are defined as maps with the available keys: "private", "public", "transit\_gateway", "core\_network". Each Subnet type offers its own set of available arguments detailed below.
# Dual-stack subnet
public = {
netmask = 24
assign_ipv6_cidr = true
nat_gateway_configuration = "single_az"
}
# IPv4 only subnet
private = {
netmask = 24
connect_to_public_natgw = true
}
# IPv6 only subnet
ipv6 = {
ipv6_native = true
assign_ipv6_cidr = true
connect_to_eigw = true
}
# Transit gateway subnets (dual-stack)
transit_gateway = {
netmask = 24
assign_ipv6_cidr = true
connect_to_public_natgw = true
transit_gateway_default_route_table_association = true
transit_gateway_default_route_table_propagation = true
}
# Core Network subnets (dual-stack)
core_network = {
netmask = 24
assign_ipv6_cidr = true
connect_to_public_natgw = true
appliance_mode_support = true
require_acceptance = true
accept_attachment = true
}
}
subnets = {| `any` | n/a | yes | | [cidr\_block](#input\_cidr\_block) | IPv4 CIDR range to assign to VPC if creating VPC or to associate as a secondary IPv6 CIDR. Overridden by var.vpc\_id output from data.aws\_vpc. | `string` | `null` | no | | [core\_network](#input\_core\_network) | AWS Cloud WAN's core network information - to create a VPC attachment. Required when `cloud_wan` subnet is defined. Two attributes are required: the `id` and `arn` of the resource. |
# Dual-stack subnet
public = {
netmask = 24
assign_ipv6_cidr = true
nat_gateway_configuration = "single_az"
}
# IPv4 only subnet
private = {
netmask = 24
connect_to_public_natgw = true
}
# IPv6 only subnet
ipv6 = {
ipv6_native = true
assign_ipv6_cidr = true
connect_to_eigw = true
}
# Additional routes
private = {
netmask = 24
routes = [{
destination_cidr_block = "0.0.0.0/0"
transit_gateway_id = "tgw-01238768912345678"
}]
}
# Transit gateway subnets (dual-stack)
transit_gateway = {
netmask = 24
assign_ipv6_cidr = true
connect_to_public_natgw = true
transit_gateway_default_route_table_association = true
transit_gateway_default_route_table_propagation = true
}
# Core Network subnets (dual-stack)
core_network = {
netmask = 24
assign_ipv6_cidr = true
connect_to_public_natgw = true
appliance_mode_support = true
require_acceptance = true
accept_attachment = true
}
}
object({|
id = string
arn = string
})
{| no | | [core\_network\_ipv6\_routes](#input\_core\_network\_ipv6\_routes) | Configuration of IPv6 route(s) to AWS Cloud WAN's core network.
"arn": null,
"id": null
}
core_network_ivp6_routes = {| `any` | `{}` | no | diff --git a/data.tf b/data.tf index 7cf5efe..419a845 100644 --- a/data.tf +++ b/data.tf @@ -113,6 +113,37 @@ locals { # private subnets with cidrs per az if connect_to_public_eigw = true ... "privatetwo/us-east-1a" private_subnet_names_egress_routed = [for subnet in local.private_per_az : subnet if contains(local.private_subnets_egress_routed, split("/", subnet)[0])] + # Configure additional routes. + # List of private subnets with additional routes. + private_subnets_routes = [for type in local.private_subnet_names : type if length(lookup(var.subnets[type], "routes", [])) > 0] + # List of routes for the private subnets. + # The tricky part here is that we have to identify the subnet names where a route + # should be added because the subnet name has suffix /az_name e.g. subnet is created for each zone. + private_subnets_az_routes = flatten([ + for subnet_name in local.private_subnets_routes : + [ + for subnet_name_az in local.private_per_az : + [ + for route in var.subnets[subnet_name].routes : + merge(route, { "route_table_name" : subnet_name_az }) + ] if startswith(subnet_name_az, "${subnet_name}/") + ] + ]) + # List of routes for the public subnets. + # There is just 1 public subnet and route table names are based on AZ names. + public_subnet_az_routes = (length(try(var.subnets.public.routes, [])) > 0 + ? flatten( + [ + for az in local.azs : + [ + for route in var.subnets.public.routes : + merge(route, { "route_table_name" : az }) + ] + ] + ) + : [] + ) + # VPC LATTICE ############################################################ # If var.vpc_lattice is defined (default = {}), the VPC association is created. lattice_association = length(keys(var.vpc_lattice)) > 0 diff --git a/main.tf b/main.tf index 5bf91a7..c5bef62 100644 --- a/main.tf +++ b/main.tf @@ -210,6 +210,28 @@ resource "aws_route" "public_to_cwan" { ] } +# Configure routes provided in the input variable `subnets` for the public subnet(s). +resource "aws_route" "public_routes" { + count = length(local.public_subnet_az_routes) + + destination_cidr_block = lookup(local.public_subnet_az_routes[count.index], "destination_cidr_block", null) + destination_prefix_list_id = lookup(local.public_subnet_az_routes[count.index], "destination_prefix_list_id", null) + destination_ipv6_cidr_block = lookup(local.public_subnet_az_routes[count.index], "destination_ipv6_cidr_block", null) + + carrier_gateway_id = lookup(local.public_subnet_az_routes[count.index], "carrier_gateway_id", null) + core_network_arn = lookup(local.public_subnet_az_routes[count.index], "core_network_arn", null) + egress_only_gateway_id = lookup(local.public_subnet_az_routes[count.index], "egress_only_gateway_id", null) + gateway_id = lookup(local.public_subnet_az_routes[count.index], "gateway_id", null) + nat_gateway_id = lookup(local.public_subnet_az_routes[count.index], "nat_gateway_id", null) + local_gateway_id = lookup(local.public_subnet_az_routes[count.index], "local_gateway_id", null) + network_interface_id = lookup(local.public_subnet_az_routes[count.index], "network_interface_id", null) + transit_gateway_id = lookup(local.public_subnet_az_routes[count.index], "transit_gateway_id", null) + vpc_endpoint_id = lookup(local.public_subnet_az_routes[count.index], "vpc_endpoint_id", null) + vpc_peering_connection_id = lookup(local.public_subnet_az_routes[count.index], "vpc_peering_connection_id", null) + + route_table_id = aws_route_table.private[local.public_subnet_az_routes[count.index].route_table_name].id +} + # Route: IPv6 routes from public subnets to AWS Cloud WAN's core network (if configured in var.core_network_routes) resource "aws_route" "ipv6_public_to_cwan" { for_each = (contains(local.subnet_keys, "public") && contains(local.ipv6_subnets_cwan_routed, "public") && local.create_cwan_routes) ? toset(local.azs) : toset([]) @@ -290,6 +312,28 @@ resource "aws_route" "private_to_egress_only" { egress_only_gateway_id = aws_egress_only_internet_gateway.eigw[0].id } +# Configure routes provided in the input variable `subnets` for the private subnet(s). +resource "aws_route" "private_routes" { + count = length(local.private_subnets_az_routes) + + destination_cidr_block = lookup(local.private_subnets_az_routes[count.index], "destination_cidr_block", null) + destination_prefix_list_id = lookup(local.private_subnets_az_routes[count.index], "destination_prefix_list_id", null) + destination_ipv6_cidr_block = lookup(local.private_subnets_az_routes[count.index], "destination_ipv6_cidr_block", null) + + carrier_gateway_id = lookup(local.private_subnets_az_routes[count.index], "carrier_gateway_id", null) + core_network_arn = lookup(local.private_subnets_az_routes[count.index], "core_network_arn", null) + egress_only_gateway_id = lookup(local.private_subnets_az_routes[count.index], "egress_only_gateway_id", null) + gateway_id = lookup(local.private_subnets_az_routes[count.index], "gateway_id", null) + nat_gateway_id = lookup(local.private_subnets_az_routes[count.index], "nat_gateway_id", null) + local_gateway_id = lookup(local.private_subnets_az_routes[count.index], "local_gateway_id", null) + network_interface_id = lookup(local.private_subnets_az_routes[count.index], "network_interface_id", null) + transit_gateway_id = lookup(local.private_subnets_az_routes[count.index], "transit_gateway_id", null) + vpc_endpoint_id = lookup(local.private_subnets_az_routes[count.index], "vpc_endpoint_id", null) + vpc_peering_connection_id = lookup(local.private_subnets_az_routes[count.index], "vpc_peering_connection_id", null) + + route_table_id = aws_route_table.private[local.private_subnets_az_routes[count.index].route_table_name].id +} + # Route: IPv4 routes from private subnets to the Transit Gateway (if configured in var.transit_gateway_routes) resource "aws_route" "private_to_tgw" { for_each = toset(local.private_subnet_key_names_tgw_routed) @@ -521,4 +565,4 @@ resource "aws_vpclattice_service_network_vpc_association" "vpc_lattice_service_n module.tags.tags_aws, module.vpc_lattice_tags.tags_aws ) -} \ No newline at end of file +} diff --git a/variables.tf b/variables.tf index 95c9936..6b106f0 100644 --- a/variables.tf +++ b/variables.tf @@ -112,6 +112,7 @@ variable "subnets" { - `assign_ipv6_cidr` = (Optional|bool) **Cannot set if `ipv6_cidrs` is set.** If true, it will calculate a /64 block from the IPv6 VPC CIDR to set in the subnets. - `ipv6_cidrs` = (Optional|list(string)) **Cannot set if `assign_ipv6_cidr` is set.** List of IPv6 CIDRs to set to subnets. The subnet size must use a /64 prefix length. Count of CIDRs defined must match quantity of azs in `az_count`. - `name_prefix` = (Optional|String) A string prefix to use for the name of your subnet and associated resources. Subnet type key name is used if omitted (aka private, public, transit_gateway). Example `name_prefix = "private"` for `var.subnets.private` is redundant. + - `routes` = (optional|list(map(string)) List of maps, where each map represents an `aws_route` resource. All `aws_route` attributes are supported for both the `destination` and `target` arguments. - `tags` = (Optional|map(string)) Tags to set on the subnet and associated resources. **Any private subnet type options:** @@ -161,6 +162,14 @@ variable "subnets" { assign_ipv6_cidr = true connect_to_eigw = true } + # Additional routes + private = { + netmask = 24 + routes = [{ + destination_cidr_block = "0.0.0.0/0" + transit_gateway_id = "tgw-01238768912345678" + }] + } # Transit gateway subnets (dual-stack) transit_gateway = { netmask = 24 @@ -248,6 +257,24 @@ EOF error_message = "Any subnet type `name_prefix` must not contain \"/\"." condition = alltrue([for _, v in var.subnets : !can(regex("/", try(v.name_prefix, "")))]) } + + # We check here if there exists at least one subnet that meets the following criteria: + # a. The subnet has a route with the destination CIDR block of "0.0.0.0/0". + # b. The subnet has the 'connect_to_public_natgw' attribute set to true. + validation { + error_message = "Route with CIDR '0.0.0.0/0' is mutually exclusive with 'connect_to_public_natgw'." + condition = !anytrue( + [ + for name, subnet in var.subnets : + anytrue( + [ + for route in lookup(subnet, "routes", []) : + lookup(route, "destination_cidr_block", "") == "0.0.0.0/0" + ] + ) && lookup(subnet, "connect_to_public_natgw", false) + ] + ) + } } variable "tags" {
public = "::/0"
private = "pl-123"
}