Access the index of a map in for_each - amazon-web-services

I have a map that looks like this
variable "mysubnets" {
type = map(string)
default = {
"subnet1" = "10.1.0.0/24"
"subnet2" = "10.1.1.0/24"
}
}
In my module I'm trying to place subnets in different availability zones in the same vpc
data "aws_availability_zones" "azs" {
state = "available"
}
resource "aws_subnet" "test-subnets" {
for_each = var.mysubnets
cidr_block = "${each.value}"
vpc_id = aws_vpc.myvpc.id
availability_zone = data.aws_availability_zones.azs.names[index("${each.value}")]
tags = {
Name = "${each.key}"
}
}
I can get the key and value from the map no problem, but when trying to pick an availability zone I can't find how to change the value. Is there a way to get the index of a map, or create a counter for a number that increments?

Your data source is called azs, not available. So it should be:
availability_zone = data.aws_availability_zones.azs.names[index("${each.value}")]
Update:
To use index with your var.mysubnets you can do as follows:
resource "aws_subnet" "test-subnets" {
for_each = {for idx, subnet in keys(var.mysubnets):
idx => {
name = subnet
cidr = var.mysubnets[subnet]
}
}
cidr_block = each.value.cidr
vpc_id = aws_vpc.myvpc.id
availability_zone = element(data.aws_availability_zones.azs.names, each.key)
tags = {
Name = each.value.name
}
}

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
}

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.

Attach multiple private subnet to route table for each terraform

I have public and private subnets established in a VPC created with for each. I am now trying to create route tables for the subnets and nat gateways specifically for access for private instances. My subnets, route tables, and public subnet associations are working properly. I am having trouble getting my private subnets to attach to the route table connecting it to the NAT gateway. I believe my logic correct. My NAT gateways are sitting in my public subnets. The only issue is private subnets being attached to the route table that connects to the NAT gateway. Below is my code, any advice is appreciated.
resource "aws_route_table" "public" {
for_each = var.pub_subnet
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = var.rt_tags
}
}
resource "aws_route_table_association" "public" {
for_each = aws_subnet.public
route_table_id = aws_route_table.public[each.key].id
subnet_id = each.value.id
}
resource "aws_route_table_association" "nat" {
for_each = aws_subnet.private
route_table_id = aws_route_table.nat[each.key].id
subnet_id = each.value.id
}
resource "aws_route_table" "nat" {
for_each = var.pub_subnet
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_nat_gateway.main[each.key].id
}
tags = {
Name = var.rt_tags_private
}
}
resource "aws_subnet" "public" {
for_each = var.pub_subnet
vpc_id = aws_vpc.main.id
cidr_block = each.value.cidr_block
availability_zone = each.value.availability_zone
map_public_ip_on_launch = true
tags = {
Name = each.key
}
}
resource "aws_subnet" "private" {
for_each = var.priv_subnet
vpc_id = aws_vpc.main.id
cidr_block = each.value.cidr_block
availability_zone = each.value.availability_zone
map_public_ip_on_launch = false
tags = {
Name = each.key
}
}
Variables
variable "pub_subnet" {
type = map(object({
cidr_block = string
availability_zone = string
}))
default = {
"PubSub1" = {
cidr_block = "10.0.1.0/24"
availability_zone = "us-west-1a"
}
}
}
variable "priv_subnet" {
type = map(object({
cidr_block = string
availability_zone = string
}))
default = {
"PrivSub1" = {
cidr_block = "10.0.2.0/24"
availability_zone = "us-west-1c"
}
}
}
Error
Error: Invalid index
on vpc.tf line 61, in resource "aws_route_table_association" "nat":
61: route_table_id = aws_route_table.nat[each.key].id
|----------------
| aws_route_table.nat is object with 1 attribute "PubSub1"
| each.key is "PrivSub1"
The given key does not identify an element in this collection value.
NAT Gateway
resource "aws_nat_gateway" "main" {
for_each = aws_subnet.public
subnet_id = each.value.id
allocation_id = aws_eip.main[each.key].id
}
EIP
resource "aws_eip" "main" {
for_each = aws_subnet.public
vpc = true
lifecycle {
create_before_destroy = true
}
}
You are defining your route table for nat using var.pub_subnet which has the form of:
"PubSub1" = {
cidr_block = "10.0.1.0/24"
availability_zone = "us-west-1a"
}
Thus to refer to aws_route_table you have to use PubSub1 key.
However, in your aws_route_table_association you are iterating over aws_subnet.private which has key of PrivSub1.
update
The issue can be overcome by creating a local mapping for private=>public subnets names, e.g.:
locals {
private_public_mapping = zipmap(keys(var.priv_subnet), keys(var.pub_subnet))
}
resource "aws_route_table_association" "nat" {
for_each = aws_subnet.private
route_table_id = aws_route_table.nat[local.private_public_mapping[each.key]].id
subnet_id = each.value.id
}

Assigning multiple public subnet to route table using for each

I have multiple subnets created with for each that I am trying to get associated with a route table. Below is the code I have, error, and what I have tried.
locals {
az_names = data.aws_availability_zones.azs.names
pub_sub_ids = aws_subnet.public.*.id
}
resource "aws_route_table_association" "main" {
for_each = var.public_sub_cidr
subnet_id = local.pub_sub_ids[each.key]
route_table_id = aws_route_table.main.id
}
resource "aws_subnet" "public" {
for_each = { for index, az_name in local.az_names : index => az_name }
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, each.key + 1)
availability_zone = local.az_names[each.key]
map_public_ip_on_launch = true
tags = {
Name = "${var.vpc_tags}-PubSubnet"
}
}
Error: Unsupported attribute
on vpc.tf line 3, in locals:
3: pub_sub_ids = aws_subnet.public.*.id
This object does not have an attribute named "id".
I believe this should be working. Any advice on this error and getting these public subnets to attach to the route table would be helpful.
UPDATE
I made some changes and removed the local variable 'pub_sub_ids' and also changed 'aws_route_table_association" "main" to
resource "aws_route_table_association" "main" {
for_each = var.public_sub_cidr
subnet_id = each.key
route_table_id = aws_route_table.main.id
}
Now I am getting an error
Error: Error creating route table association: InvalidSubnetID.NotFound: The `subnet ID '' does not exist`
It says the subnet does not exist even though I see it in the Console. Any advice would be appreciated in associating these public subnets to the route table.
subnet_id in aws_route_table_association should be subnet id, not subnet CIDR.
Since aws_route_table is not given, I made my own to verify the setup. Thus you could do the following:
resource "aws_route_table_association" "main" {
count = length(aws_subnet.public)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.main.id
}
And below is the full code I used for verification:
provider "aws" {
# your data
}
data "aws_availability_zones" "azs" {
state = "available"
}
locals {
az_names = data.aws_availability_zones.azs.names
}
variable "vpc_cidr" {
default = "10.0.0.0/16"
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
}
resource "aws_subnet" "public" {
for_each = {for index, az_name in local.az_names: index => az_name}
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, each.key + 1)
availability_zone = local.az_names[each.key]
map_public_ip_on_launch = true
}
resource "aws_internet_gateway" "gw" {
vpc_id = aws_vpc.main.id
tags = {
Name = "main"
}
}
resource "aws_route_table" "main" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.gw.id
}
}
resource "aws_route_table_association" "main" {
count = length(aws_subnet.public)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.main.id
}

Terraform - how to use for_each loop on a list of objects to create resources

I have an object containing the list of subnets I want to create.
variable "subnet-map" {
default = {
ec2 = [
{
cidr_block = "10.0.1.0/24"
availability_zone = "eu-west-1a"
}
],
lambda = [
{
cidr_block = "10.0.5.0/24"
availability_zone = "eu-west-1a"
},
{
cidr_block = "10.0.6.0/24"
availability_zone = "eu-west-1b"
},
{
cidr_block = "10.0.7.0/24"
availability_zone = "eu-west-1c"
}
],
secrets = [
{
cidr_block = "10.0.8.0/24"
availability_zone = "eu-west-1a"
},
{
cidr_block = "10.0.9.0/24"
availability_zone = "eu-west-1b"
},
{
cidr_block = "10.0.10.0/24"
availability_zone = "eu-west-1c"
}
],
rds = [
{
cidr_block = "10.0.11.0/24"
availability_zone = "eu-west-1a"
},
{
cidr_block = "10.0.12.0/24"
availability_zone = "eu-west-1b"
},
{
cidr_block = "10.0.13.0/24"
availability_zone = "eu-west-1c"
}
]
}
}
Earlier I was using the count loop construct. So I used to flatten the above structure into a list of objects
locals {
subnets = flatten([
for resource in keys(var.subnet-map) : [
for subnet in var.subnet-map[resource] : {
resource = resource
cidr_block = subnet.cidr_block
availability_zone = subnet.availability_zone
}
]
])
}
And then I would create the resources by doing
resource "aws_subnet" "aws-subnets" {
count = length(local.subnets)
vpc_id = aws_vpc.aws-vpc.id
cidr_block = local.subnets[count.index].cidr_block
availability_zone = local.subnets[count.index].availability_zone
tags = {
Name = "subnet-${local.subnets[count.index].resource}-${local.subnets[count.index].availability_zone}"
}
}
Now I want to use the for_each loop. But I cannot figure out how to do it. This is what I've done so far.
resource "aws_subnet" "subnets-dev" {
for_each = var.subnet-map
vpc_id = aws_vpc.vpc-dev.id
cidr_block = each.value.cidr_block
availability_zone = each.value.availability_zone
tags = {
Name = "subnet-dev-${each.value.resource}-${each.value.availability_zone}"
environment = "dev"
}
}
But it keeps giving an error saying
Error: Unsupported attribute
on vpc/main.tf line 93, in resource "aws_subnet" "subnets-dev":
93: Name = "subnet-dev-${each.value.resource}-${each.value.availability_zone}"
|----------------
| each.value is tuple with 3 elements
This value does not have any attributes.
How could I fix this?
I'm not sure I fully follow all of what you tried here because your initial snippet of var.subnet-map shows it being a map of maps of lists of objects, but later on when you used for_each = var.subnet-map it seems to have treated it as a map of lists instead. Did you remove that extra level of maps (the "default" key) before trying for_each here?
Working with your original definition of variable "subnet-map", your first step with for_each will be similar to what you did with count: you need to flatten the structure, this time into a map of objects rather than a list of objects. The easiest way to get there is to derive a map from your existing flattened list:
locals {
subnets = flatten([
for resource in keys(var.subnet-map) : [
for subnet in var.subnet-map[resource] : {
resource = resource
cidr_block = subnet.cidr_block
availability_zone = subnet.availability_zone
}
]
])
subnets_map = {
for s in local.subnets: "${s.resource}:${s.availability_zone}" => s
}
}
Here I assumed that your "resource" string and your availability zone together are a suitable unique identifier for a subnet. If not, you can adjust the "${s.resource}:${s.availability_zone}" part to whatever unique key you want to use for these.
Now you can use the flattened map as the for_each map:
resource "aws_subnet" "subnets-dev" {
for_each = local.subnets_map
vpc_id = aws_vpc.vpc-dev.id
cidr_block = each.value.cidr_block
availability_zone = each.value.availability_zone
tags = {
Name = "subnet-dev-${each.value.resource}-${each.value.availability_zone}"
environment = "dev"
}
}
This will give you instances with addresses like aws_subnet.subnets-dev["ec2:eu-west-1a"].
Note that if you are migrating from count with existing subnets that you wish to retain, you'll need to also do a one-time migration step to tell Terraform which indexes from the existing state correspond to which keys in the new configuration.
For example, if (and only if) index 0 was previously the one for ec2 in eu-west-1a, the migration command for that one would be:
terraform state mv 'aws_subnet.subnets-dev[0]' 'aws_subnet.subnets-dev["ec2:eu-west-1a"]'
If you're not sure how they correlate, you can run terraform plan after adding for_each and look at the instances that Terraform is planning to destroy. If you work through each one of those in turn, taking the address Terraform currently knows along with the resource and availability zone names shown in the Name tag, you can migrate each of them to its new address so that Terraform will no longer think you're asking for it to destroy the numbered instances and replace them with named ones.