Terraform - Multi Loops or Maps - amazon-web-services

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.

Related

How to have AWS VPC subnets associate with one Route Table in Terraform?

I have a Terraform file that is supposed to create an AWS VPC remotely. In doing so, it would create 3 Public Subnets and 3 Private Subnets. There would then be one Route Table for each group of public and private subnets. My current configuration has it so that a route table is created for EACH subnet. I'm also having a similar issue with NAT Gateways and Public Subnets; I only want one NAT Gateway for all Public Subnets.
These are the pertinent blocks from my main.tf file:
//Creating Public Subnets
resource "aws_subnet" "publicsubnets" {
provider = aws.east
count = "${length(var.public_subnets)}"
vpc_id = aws_vpc.Main.id
cidr_block = "${var.public_subnets[count.index]}" //CIDR block of public subnets
availability_zone = "${var.availability_zones[count.index]}"
tags = {
Name = "publicsubnets"
}
}
//Creating Private Subnets
resource "aws_subnet" "privatesubnets" {
provider = aws.east
count = "${length(var.private_subnets)}"
vpc_id = aws_vpc.Main.id
cidr_block = "${var.private_subnets[count.index]}" //CIDR block of private subnets
availability_zone = "${var.availability_zones[count.index]}"
tags = {
Name = "privatesubnets"
}
}
//Creating Route Table for Public Subnets
resource "aws_route_table" "PublicRT" {
provider = aws.east
count = "${length(var.public_subnets)}"
vpc_id = aws_vpc.Main.id
route {
cidr_block = "IP_Addresses" //Traffic from Public Subnet reaches Internet via Internet Gateway
gateway_id = aws_internet_gateway.IGW.id
}
tags = {
Name = "publicRT"
}
}
//Creating Route Table for Private Subnet
resource "aws_route_table" "PrivateRT" {
provider = aws.east
count = "${length(var.private_subnets)}"
vpc_id = aws_vpc.Main.id
route {
cidr_block = "IP_Addresses" //Traffic from Private Subnet reaches Internet via NAT Gateway
nat_gateway_id = aws_nat_gateway.NATgw[count.index].id
}
tags = {
Name = "privateRT"
}
}
// Associate Public Subnets with Route Table
resource "aws_route_table_association" "PublicRTassociation" {
provider = aws.east
count = "${length(var.public_subnets)}"
subnet_id = aws_subnet.publicsubnets[count.index].id
route_table_id = aws_route_table.PublicRT[count.index].id
}
//Associate Private Subnets with Route Table
resource "aws_route_table_association" "PrivateRTassociation" {
provider = aws.east
count = "${length(var.private_subnets)}"
subnet_id = aws_subnet.privatesubnets[count.index].id
route_table_id = aws_route_table.PrivateRT[count.index].id
}
//Associate Elastic IP's to Public Subnets
resource "aws_eip" "nateIP" {
provider = aws.east
count = "${length(var.public_subnets)}"
vpc = true
tags = {
Name = "nateIP"
}
}
//Associate Public Subnets to NAT Gateways
resource "aws_nat_gateway" "NATgw" {
provider = aws.east
count = "${length(var.public_subnets)}"
allocation_id = aws_eip.nateIP[count.index].id
subnet_id = aws_subnet.publicsubnets[count.index].id
tags = {
Name = "NATgw"
}
}
I have provider and variables declared in separate files.
Note: I have changed some names for security/privacy reasons.
What should I do?
You are creating NATs in a loop. If you want to use single NAT gateway, you need to create only one NAT gateway resource (without a loop).
Something like below:
resource "aws_eip" "nateIP" {
provider = aws.east
vpc = true
tags = {
Name = "nateIP"
}
}
resource "aws_nat_gateway" "NATgw" {
provider = aws.east
allocation_id = aws_eip.nateIP.id
subnet_id = aws_subnet.publicsubnets[0].id
tags = {
Name = "NATgw"
}
}
Then, in your route table, you'd set NAT like below:
resource "aws_route_table" "PrivateRT" {
provider = aws.east
count = "${length(var.private_subnets)}"
vpc_id = aws_vpc.Main.id
route {
cidr_block = "IP_Addresses" //Traffic from Private Subnet reaches Internet via NAT Gateway
nat_gateway_id = aws_nat_gateway.NATgw.id
}
tags = {
Name = "privateRT"
}
}
However, having a single NAT has its downside. Your infrastructure would be reylying only on one availablity zone for NAT. In case there are issues with that Availability zone, your application may have issues.

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
}

Terraform project to create VPC/IGW, VPN, and private subnet with NATGW - Connectivity Analyzer says no route from NATGW to IGW

https://github.com/phillhocking/aws-network-vpn/tree/1000
I have been trying to figure this out for quite some time, and I really am struggling at understanding why these components can't talk to each other. The VPC has an IGW which is necessary for an EIP for the NATGW, however, nothing on this subnet can get out to the public internet. Everything works fine over the VPN link, but the Connectivity Analyzer indicates there's no connectivity between the NATGW and the IGW as there is not a route - how would I route just the NATGW traffic to the IGW without something like a 0.0.0.0/0 route which already is assigned to the NATGW for this subnet?
Connectivity Analyzer - Route Table has no route to IGW
I know I am just missing something bonehead simple. Look at the repo for how the whole thing goes together, but here's the VPC module:
data "aws_availability_zones" "available" {
state = "available"
}
resource "aws_vpc" "main" {
cidr_block = var.cidr_block
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = var.vpc_name
}
}
resource "aws_subnet" "dev" {
count = var.subnet_count
# This line is necessary to ensure that we pick availabiltiy zones that can launch any size ec2 instance
availability_zone = data.aws_availability_zones.available.names[0]
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.cidr_block, 6, count.index * 2 + 1)
tags = {
Name = "dev-subnet-${count.index}"
}
}
resource "aws_network_acl" "dev" {
vpc_id = aws_vpc.main.id
subnet_ids = aws_subnet.dev[*].id
ingress {
protocol = -1
rule_no = 1000
action = "allow"
#cidr_block = var.prem_network_address_space
cidr_block = "0.0.0.0/0"
from_port = 0
to_port = 0
}
egress {
protocol = -1
rule_no = 100
action = "allow"
cidr_block = "0.0.0.0/0"
from_port = 0
to_port = 0
}
tags = {
Name = "dev-acl"
}
}
# Gateways
resource "aws_internet_gateway" "gw" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.vpc_name}-internet-gateway"
}
}
resource "aws_eip" "nat-gw" {
vpc = true
tags = {
Name = "nat-elastic-ip"
}
depends_on = [aws_internet_gateway.gw]
}
resource "aws_nat_gateway" "gw" {
allocation_id = aws_eip.nat-gw.id
subnet_id = aws_subnet.dev[0].id
tags = {
Name = "${var.vpc_name}-nat-gateway-dev"
}
depends_on = [aws_internet_gateway.gw]
}
# VPC Route Table
resource "aws_default_route_table" "default" {
default_route_table_id = aws_vpc.main.main_route_table_id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.gw.id
}
tags = {
Name = "${var.vpc_name}-public"
}
depends_on = [aws_internet_gateway.gw]
}
# dev Subnet Route Table
resource "aws_route_table" "dev" {
vpc_id = aws_vpc.main.id
tags = {
Name = "dev-route-table"
}
}
resource "aws_route_table_association" "dev_routes" {
subnet_id = aws_subnet.dev[0].id
route_table_id = aws_route_table.dev.id
depends_on = [aws_nat_gateway.gw]
}
resource "aws_route" "dev_nat" {
route_table_id = aws_route_table.dev.id
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.gw.id
depends_on = [aws_nat_gateway.gw]
You are placing NAT in aws_subnet.dev[0], then you create aws_route_table.dev that you attach to the subnet. What's more the aws_route_table.dev has a route aws_route.dev_nat which points to the NAT.
So basically you are doing some circular routing - all traffic in aws_subnet.dev[0] is direct to NAT in the same subnet, which in turn is directed again to the same NAT.
As you pointed in comment, NAT should be in public subnet, while subnet directing traffic to NAT should be private subnets.

Terraform AWS - route table association - add multiple subnet

I have created 4 subnets in my vpc, 2 of them public and 2 of them private.
I need to associate 2 public subnets to a one route table and 2 private subnets to another route table.
Looking at the docs, aws_route_table_association seems like accepts only one subnet_id.
How do I add multiple subnets as show in this pic?
Associate route table to subnets
resource "aws_route_table_association" "public-test" {
subnet_id = -> I need to add 2 public subnets here
route_table_id = aws_route_table.public-test.id
}
resource "aws_route_table_association" "private-test" {
subnet_id = -> I need to add 2 private subnets here
route_table_id = aws_route_table.private-test.id
}
Here are the subnets and routes:
Create Subnet
resource "aws_subnet" "public-test-a" {
vpc_id = aws_vpc.vpc-test-02.id
cidr_block = "10.0.0.0/28"
map_public_ip_on_launch = true
availability_zone = var.AZ[0]
tags = {
Name = "public-test-a"
}
}
resource "aws_subnet" "public-test-b" {
vpc_id = aws_vpc.vpc-test-02.id
cidr_block = "10.0.0.16/28"
map_public_ip_on_launch = true
availability_zone = var.AZ[1]
tags = {
Name = "public-test-b"
}
}
resource "aws_subnet" "private-test-a" {
vpc_id = aws_vpc.vpc-test-02.id
cidr_block = "10.0.0.32/28"
availability_zone = var.AZ[0]
tags = {
Name = "private-test-a"
}
}
resource "aws_subnet" "private-test-b" {
vpc_id = aws_vpc.vpc-test-02.id
cidr_block = "10.0.0.48/28"
availability_zone = var.AZ[1]
tags = {
Name = "private-test-b"
}
}
Create route table
resource "aws_route_table" "public-test" {
vpc_id = aws_vpc.vpc-test-02.id
route {
cidr_block = "10.0.0.0/26"
}
route {
cidr_block = "0.0.0.0/0"
gateway_id =aws_internet_gateway.myIG-test-02.id
}
tags = {
Name = "public-test"
}
}
resource "aws_route_table" "private-test" {
vpc_id = aws_vpc.vpc-test-02.id
route {
cidr_block = "10.0.0.0/26"
}
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_nat_gateway.myNat-test-02.id
}
}
You can simple declare two route table association resources.
resource "aws_subnet" "public_test_a" {
vpc_id = aws_vpc.vpc-test-02.id
cidr_block = "10.0.0.0/28"
map_public_ip_on_launch = true
availability_zone = var.AZ[0]
tags = {
Name = "public-test-a"
}
}
resource "aws_subnet" "public-test-b" {
vpc_id = aws_vpc.vpc-test-02.id
cidr_block = "10.0.0.16/28"
map_public_ip_on_launch = true
availability_zone = var.AZ[1]
tags = {
Name = "public-test-b"
}
}
resource "aws_route_table_association" "public-test-a" {
subnet_id = aws_subnet.public-test-a.id # first subnet
route_table_id = aws_route_table.public-test.id
}
resource "aws_route_table_association" "public-test-b" {
subnet_id = aws_subnet.public-test-b.id # second subnet
route_table_id = aws_route_table.public-test.id
}
resource "aws_route_table" "public-test" {
vpc_id = aws_vpc.vpc-test-02.id
route {
cidr_block = "10.0.0.0/26"
}
route {
cidr_block = "0.0.0.0/0"
gateway_id =aws_internet_gateway.myIG-test-02.id
}
tags = {
Name = "public-test"
}
}
Also it is considered good practice to follow naming conventions. Quoting the docs
Use _ (underscore) instead of - (dash) in all: resource names, data source names, variable names, outputs.
Beware that actual cloud resources have many hidden restrictions in their naming conventions. Some cannot contain dashes, some must be camel cased. These conventions refer to Terraform names themselves.
Only use lowercase letters and numbers.

assign multiple subnets to route table aws

I think multiple people have asked the same question but my condition is different. I am taking input from the user for the vpc region, cidr value even the public subnet segment too. I have to attach all my public subnet to the default route table and private subnets to the diff route table . can you help me in how to attach them .
provider "aws" {
region = var.region
}
resource "aws_vpc" "app_vpc" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = var.vpc_name
}
}
# create igw
resource "aws_internet_gateway" "app_igw" {
vpc_id = aws_vpc.app_vpc.id
}
data "aws_availability_zones" "available" {
state = "available"
}
#provision public subnet
resource "aws_subnet" "public_subnet_01" {
vpc_id = aws_vpc.app_vpc.id
cidr_block = var.public_subnet_01
availability_zone = data.aws_availability_zones.available.names[0]
tags = {
Name = "public_subnet_01"
}
depends_on = [aws_vpc_dhcp_options_association.dns_resolver]
}
resource "aws_subnet" "public_subnet_02" {
vpc_id = aws_vpc.app_vpc.id
cidr_block = var.public_subnet_02
availability_zone = data.aws_availability_zones.available.names[1]
tags = {
Name = "public_subnet_02"
}
depends_on = [aws_vpc_dhcp_options_association.dns_resolver]
}
resource "aws_subnet" "public_subnet_03" {
vpc_id = aws_vpc.app_vpc.id
cidr_block = var.public_subnet_03
availability_zone = data.aws_availability_zones.available.names[2]
tags = {
Name = "public_subnet_03"
}
depends_on = [aws_vpc_dhcp_options_association.dns_resolver]
}
#default route table
resource "aws_default_route_table" "default" {
default_route_table_id = aws_vpc.app_vpc.default_route_table_id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.app_igw.id
}
}
resource "aws_route_table_association" "default_association_01" {
subnet_id = [aws_subnet.public_subnet_01.id, aws_subnet.public_subnet_02.id, aws_subnet.public_subnet_03.id]
route_table_id = aws_vpc.app_vpc.default_route_table_id
}
I am getting error in adding multiple subnet so can u please help here :)
aws_route_table_association takes only one subnet as an input, not a list of subnets.
If you want to create the associations using your list, you can use for_each:
resource "aws_route_table_association" "default_association_01" {
for_each = toset([aws_subnet.public_subnet_01.id, aws_subnet.public_subnet_02.id, aws_subnet.public_subnet_03.id])
subnet_id = each.key
route_table_id = aws_vpc.app_vpc.default_route_table_id
}
The above assumes that everything else is correct. There could be still some errors in your code which aren't apparent yet.