Dynamic routes in Terraform - amazon-web-services

My simplified setup is something like this:
2 AZs and each consists of 2 subnets (Subnet1-AZ-A, Subnet2-AZ-A, Subnet1-AZ-B, Subnet2-AZ-B)
I want to create routing table that will fetch CIDR blocks from Subnet1 (from both AZs) and create routes:
CIDR_BLOCK of Subnet-1-AZ-A -> VPC_ENDPOINT in Subnet-2-AZ-A
CIDR_BLOCK of Subnet 1-AZ-B -> VPC_ENDPOINT in Subnet-2-AZ-B
I don't want to hardcode values, because number of AZs (and subnets) may change.
Here's code that I'm using:
Subnet-1A
resource "aws_subnet" "Subnet1" {
count = length(var.az)
vpc_id = aws_vpc.app-vpc.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, 4+count.index)
availability_zone = var.az[count.index]
}
VPC_Endpoints:
resource "aws_vpc_endpoint" "endpoint-spoke" {
count = length(var.az)
service_name = var.glbe_service_name
subnet_ids = [aws_subnet.endpoint-subnet[count.index].id]
vpc_endpoint_type = var.glbe_endpoint_type
vpc_id = aws_vpc.app-vpc.id
}
Route-Table
resource "aws_route_table" "IGWRT" {
vpc_id = aws_vpc.app-vpc.id
dynamic "route" {
for_each = aws_subnet.Subnet1[*].cidr_block
content {
cidr_block = route.value
vpc_endpoint_id = aws_vpc_endpoint.endpoint-spoke[*].id
}
}
}
The code in Route-Table is not working. I know aws_subnet.Subnet1[*].cidr_block will be a list that contains Subnet1 CIDR blocks for example: [10.0.0.0/20, 10.0.1.0/24], but how I can create dynamic routes where:
10.0.0.0/0 will be pointing to VPC_Endpoint in respective AZ
10.0.0.1/0 will be pointing to VPC_Endpoint in respective AZ

Update the dynamic block in the aws_route_table resource:
resource "aws_route_table" "IGWRT" {
vpc_id = aws_vpc.app-vpc.id
dynamic "route" {
for_each = aws_subnet.Subnet1
content {
cidr_block = route.value.cidr_block
vpc_endpoint_id = aws_vpc_endpoint.endpoint-spoke[route.key].id
}
}
}
However, I strongly recommend you to use a for_each instead of the count, if the elements of the list change (the variable az), it will affect all of the resources as their index will be updated.

Related

In Terraform, how can I create an iterative list out of two aws_subnet objects?

New to Terraform. I have two aws_subnet objects which I want to associate with route tables. As I understand it, each AZ will need it's own route table. The easiest thing to do would be just declare two route tables, one for each subnet but would like to know if there is a better way to do it instead of just settling for things thrown together.
I have declared my subnets as a list in variables.tf:
variable "my_public_subnets" {
type = list
description = "public subnet within vpc cidr block"
default = ["10.1.2.0/24", "10.1.1.0/24"]
}
And have two public subnets in main.tf
resource "aws_subnet" "pub_1" {
vpc_id = aws_vpc.vpc.id
cidr_block = var.my_public_subnets[0]
availability_zone = "us-east-1a"
}
resource "aws_subnet" "pub_2" {
vpc_id = aws_vpc.vpc.id
cidr_block = var.my_public_subnets[1]
availability_zone = "us-east-1b"
}
Instead of:
resource "aws_route_table_association" "pub_ra_1" {
subnet_id = aws_subnet.pub_1.id
route_table_id = aws_route_table.bar.id
}
resource "aws_route_table_association" "pub2_ra_2" {
subnet_id = aws_subnet.pub_2.id
route_table_id = aws_route_table.bar.id
}
Is there way to do something like this? Create a list/array/map of those two subnets so I don't have to declare a aws_route_table_association for both of them? Maybe there's a better way to set this up in general?
locals {
my_pub_subnets = [aws_subnet.pub_1, aws_subnet.pub_2]
}
resource "aws_route_table_association" "pub_rt_a" {
for_each = locals.my_pub_subnets
subnet_id = each.value
route_table_id = aws_route_table.some_public_route_table.id
depends_on = [aws_subnet.pub_1]
}
Modules are how you create repeatable procedures in TF.
Something like:
locals{
subnets = {
public = "10.1.2.0/24",
private = "10.1.1.0/24"
}
module "subnets" {
source = "./modules/subnets"
for_each = subnets
name = each.key
cidr = each.value
}
for the AZ names, you could also use data.aws_availability_zones.available.names
I would guess that most of you want is really well done inside the VPC module.
You would have to import the VPC into your state to start, but this is how I do my subnets with it.
locals {
subnets = chunklist(cidrsubnets("10.2.8.0/24", 3, 3, 3, 3, 3, 3), 2)
public_subnets = local.subnets[1]
private_subnets = local.subnets[2]
}
data "aws_availability_zones" "available" {
}
resource "aws_eip" "nat" {
count = length(local.private_subnets)
vpc = true
}
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "3.14.0"
name = "foo"
cidr = "10.2.8.0/24"
azs = data.aws_availability_zones.available.names
private_subnets = local.private_subnets
public_subnets = local.public_subnets
enable_nat_gateway = true
single_nat_gateway = true
enable_dns_hostnames = true
reuse_nat_ips = true # <= Skip creation of EIPs for the NAT Gateways
external_nat_ip_ids = aws_eip.nat.*.id
public_subnet_tags = {
"Tier" = "Public"
}
private_subnet_tags = {
"Tier" = "Private"
}
}
output "public_subnets" {
value = module.vpc.public_subnets
}
output "public_subnets_cidr" {
value = module.vpc.public_subnets_cidr_blocks
}
output "private_subnets" {
value = module.vpc.private_subnets
}
output "private_subnets_cidr" {
value = module.vpc.private_subnets_cidr_blocks
}

Configure Multiple NAT gateways

I have two public & private subnets across two AZs. I want to create a NAT in both public subnets. How do I route my private route table to use both NATs? Case three in this accepted answer. I know it's possible but I'm unsure on how to implement it. Should the destination cidr_block be the public subnet's cidr block? I also tried 0.0.0.0/0 and errored out.
resource "aws_route" "r" {
count = length(aws_nat_gateway.this)
route_table_id = aws_vpc.vpc.default_route_table_id
destination_cidr_block = "aws_subnet.public_subnets.${count.index}.cidr_block"
nat_gateway_id = "aws_nat_gateway.this.${count.index}.id"
depends_on = [aws_vpc.vpc,aws_subnet.public_subnets,aws_nat_gateway.this]
}
When I tried 0.0.0.0/0
error creating Route in Route Table (rtb-059eee10e310ade77) with destination (0.0.0.0/0): RouteAlreadyExists: The route identified by 0.0.0.0/0 already exists.
Short answer: you need to have a separate route table for each private subnet.
Long answer: I would start by defining a map that contains configuration for each availability zone. I avoid count in modern Terraform configs because it's too easy to cause incompatibilities down the road (especially if you're using it to index an array).
locals {
subnets = {
"1a" = { "public_cidr": "172.31.0.0/24", private_cidr: "172.31.1.0/24" }
"1b" = { "public_cidr": "172.31.2.0/24", private_cidr: "172.31.3.0/24" }
}
}
Now, you can use for_each to create public and private subnets for each of these availability zones:
resource "aws_subnet" "public" {
for_each = local.subnets
vpc_id = aws_vpc.example.id
cidr_block = each.value["public_cidr"]
tags = {
Name = "public-${each.key}"
}
}
resource "aws_subnet" "private" {
for_each = local.subnets
vpc_id = aws_vpc.example.id
cidr_block = each.value["private_cidr"]
tags = {
Name = "private-${each.key}"
}
}
You can associate the public subnets with the default route table (which must have a route to the Internet Gateway). Personally, I prefer creating my own public route table, rather than updating the default.
Next, you'll need to create a NAT Gateway in each public subnet:
resource "aws_nat_gateway" "example" {
depends_on = [aws_internet_gateway.example]
for_each = local.subnets
allocation_id = aws_eip.nat_ip[each.key].id
subnet_id = aws_subnet.public[each.key].id
tags = {
Name = "nat-${each.key}"
}
}
And now the important part: each private subnet needs its own route table, which references the NAT Gateway specific to its availability zone.
resource "aws_route_table" "private" {
for_each = local.subnets
vpc_id = aws_vpc.example.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.example[each.key].id
}
tags = {
Name = "private-${each.key}"
}
}
resource "aws_route_table_association" "private" {
for_each = local.subnets
subnet_id = aws_subnet.private[each.key].id
route_table_id = aws_route_table.private[each.key].id
}

Retrieve Subnet ids for map variables

I have created subnet as map variable for availability zone and CIDR block and
variable "public_subnets_list" {
type = map(any)
description = "Public Subnets"
default = {
"ap-south-1a" = "10.0.1.0/24"
"ap-south-1b" = "10.0.2.0/24"
}
}
This works fine for creating subnets under my custom VPC with below code
resource "aws_subnet" "public_subnet" {
depends_on = [
aws_vpc.terraform_vpc
]
for_each = tomap(var.public_subnets_list)
availability_zone = each.key
cidr_block = each.value
vpc_id = aws_vpc.terraform_vpc.id
tags = {
Name = "Public_Subnet_${each.key}"
}
}
How do I retrieve and display the subnet id created for respective AZs from output which i get from aws_subnet.public_subnet[*]
From the Terraform documentation, splat expressions do not work with resources that use the for_each argument.
To retrieve a list of subnet IDs using your Terraform configuration, you can do the following:
output "subnet_ids" {
value = [for subnet in aws_subnet.public_subnet : subnet.id]
}

Terraform - Multi Loops or Maps

I am creating few public subnets, private subnets, igw, nat, route_tables, and route_table entry in AWS using terraform. Below is the number of resources I am creating.
terraform.tfvars
vpc_cidr = "10.0.0.0/16"
public_subnet_count = 6
public_subnets_cidr = ["10.0.1.0/24","10.0.2.0/24","10.0.3.0/24","10.0.4.0/24","10.0.5.0/24", "10.0.6.0/24"]
availability_zones = ["us-east-2a", "us-east-2b","us-east-2c","us-east-2d"]
resources.tf
resource "aws_vpc" "vpc" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.environment}-vpc"
Environment = var.environment
}
}
resource "aws_subnet" "public_subnet" {
vpc_id = "${aws_vpc.vpc.id}"
count = var.public_subnet_count
cidr_block = "${element(var.public_subnets_cidr, count.index)}"
availability_zone = length(var.availability_zones) > 1 ? var.availability_zones[count.index % length(var.availability_zones)] : var.availability_zones[0]
map_public_ip_on_launch = false
tags = {
Name = "${var.environment}-${element(var.availability_zones, count.index)}-public-subnet"
Environment = "${var.environment}"
}
}
resource "aws_internet_gateway" "ig" {
count = 1
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.environment}-igw"
Environment = "${var.environment}"
}
}
resource "aws_route_table" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.vpc.id
tags = {
Name = "${var.environment}-public-route-table"
Environment = "${var.environment}"
}
}
resource "aws_route" "public_internet_gateway" {
count = length(aws_route_table.public.*.id)
route_table_id = element(aws_route_table.public.*.id, count.index)
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.ig[0].id
}
resource "aws_route_table_association" "public" {
count = length(var.availability_zones)
subnet_id = element(aws_subnet.public_subnet.*.id, count.index)
route_table_id = element(aws_route_table.public.*.id, count.index)
}
Query in aws_route_table_association.public section.
How do I attach private subnet of a particular az to a route table id. For example if i use 6 private subnets in tfvars, it creates atleast 2 private subnets in one az (ex: us-east-2a). How do i loop and attach 2 subnet from us-east-2a to one route table created for us-east-2a. Kind of map between multiple subnets in one az to route table in that az.
This is to attach each az nat gateway to that az subnet for routing.
You don't need to create a route table per AZ, the route tables are linked to subnets. So, you can just create a single route table and then link all your public subnets to it:
resource "aws_route_table" "public" {
vpc_id = aws_vpc.vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.ig.id
}
}
resource "aws_route_table_association" "public" {
count = var.public_subnet_count
subnet_id = aws_subnet.public_subnet[count.index].id
route_table_id = aws_route_table.public.id
}
I've included the single route into the route_table to reduce the amount of code, so you can delete the aws_route resource if you use this.
[Edit]
Sorry I just noticed your mention of private subnets, but attaching route tables follows the same principle, and you don't need a NAT gateway per AZ either (that's expensive!) so you could route all private subnets through a single NAT gateway with one route table.
If you really want 4 NAT gateways, then you could do something like:
resource "aws_route_table" "private" {
count = length(var.availability_zones)
vpc_id = aws_vpc.vpc.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.nat[count.index].id
}
}
resource "aws_route_table_association" "private" {
count = var.private_subnet_count
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private[
count.index % length(var.availability_zones)
].id
}
Since you already use the modulus (%) operator to distribute the subnets, I think you'll get the same result doing it this way.

Terraform: How to create multiple aws subnets from one resource block?

I'm trying to create multiple subnets from one resource block and I get the following error
Error: aws_subnet.private: cidr_block must be a single value, not a list
main.tf
resource "aws_subnet" "private" {
vpc_id = "${aws_vpc.vpcname.id}"
cidr_block = "${var.private_subnet}"
availability_zone = "${data.aws_availability_zones.available.names[count.index]}"
map_public_ip_on_launch = false
tags {
Name = "${var.private}"
Environment = "${terraform.workspace}"
}
}
variable.tf
variable "private_subnet" {
type = "list"
default = []
}
dev.tfvars
private_subnet = ["10.0.2.0/24", "10.0.3.0/24"]
You have to create multiple aws_subnet resources by utilitizing the count argument to create one resource for each entry in your var.private_subnet list:
resource "aws_subnet" "private" {
count = "${length(var.private_subnet)}"
vpc_id = "${aws_vpc.vpcname.id}"
cidr_block = "${var.private_subnet[count.index]}"
availability_zone = "${data.aws_availability_zones.available.names[count.index]}"
map_public_ip_on_launch = false
}
This expands the single aws_subnet resource into two, each with slightly different values based on the enumeration of count when each resource block is evaluated by terraform.
private_subnet is a list, so you should pick a single element, e.g.
cidr_block = "${element(var.private_subnet,count.index)}"
also add data module to get the availability zones for a region
data "aws_availability_zones" "available" {}
e.g.
availability_zone = "${data.aws_availability_zones.available.names[count.index]}"