For_each and count in same terraform AWS resource - amazon-web-services

I'm trying to create "aws_route" in terraform iterating over a list of route_tables with vpc_peering of another service. The other service vpc destination_cidr_block is a list.
variable "route_tables" {
type = set(string)
description = "Set of route table entries eg : rt-1, rt-2 , rt-3"
}
variable "ext_service_destination_cidr_blocks"{
type = list(string)
description = "list of destination cidr blocks of external service, eg:[\"10.10.1.1/20\", \"10.2.10.1/10\"]"
}
resource "aws_route" "ext_service_route" {
// iterating over route tables [ rt-1, rt-2 , rt-3 ]
for_each = var.route_tables
route_table_id = each.key
// Iterating over cidr list
count = var.ext_service_destination_cidr_blocks
destination_cidr_block = var.ext_service_destination_cidr_blocks[count.index]
vpc_peering_connection_id = var.ext_service_peering_connection_id
}
Here, I would like to iterate over list of destination_cidr_block.
Simply put, I need a nested loop, count inside for_each.
I can't have both count and for_each in the same block, is there any workaround for this?
Or is there any way I can split this into two modules?

We can use setproduct to compute the Cartesian product of those two collections and create a map based on that. This map can be used to do for_each on it:
resource "aws_route" "ext_service_route" {
for_each = { for i, pair in tolist(setproduct(var.route_tables, var.ext_service_destination_cidr_blocks)) : "route-${i}" => { "name" : pair[0], "cidr" : pair[1] } }
route_table_id = each.value.name
destination_cidr_block = each.value.cidr
vpc_peering_connection_id = var.ext_service_peering_connection_id
}

You can also use mod operation to iterate over the two lists simultaneously (for example, when you are using older TF version that does not support for_each yet):
resource "aws_route" "ext_service_route" {
count = length(var.ext_service_destination_cidr_blocks) * length(var.route_tables)
route_table_id = element(var.route_tables, count.index + 1 % length(var.route_tables))
destination_cidr_block = element(var.ext_service_destination_cidr_blocks, count.index + 1 % length(var.ext_service_destination_cidr_blocks))
vpc_peering_connection_id = var.ext_service_peering_connection_id
}

Related

Terraform tuple to id for subnet

I got following output:
output "private_subnets" {
description = "List of IDs of private subnets"
value = module.vpc.private_subnets
}
which returns subnet ids like: [subnet-1***, subnet-2***, subnet-3***]
How can I use this in nlb resource?
dynamic "subnet_mapping" {
#for_each = { for k,v in module.vpc.private_subnets : k => v}
for_each = [for s in module.vpc.private_subnets : s]
content {
subnet_id = s
}
}
I have tried s.id, s.value, nothing works. I get following errors:
A reference to a resource type must be followed by at least one attribute access, specifying the resource name.
It should be:
dynamic "subnet_mapping" {
for_each = toset(module.vpc.private_subnets)
content {
subnet_id = subnet_mapping.value
}
}

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]
}

Access the index of a map in for_each

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
}
}

Sorting subnets by name in Terraform v0.11.14

I'm trying to grab a list of subnets by name, filter out the ones which end in "d" (for the "d" Availability Zone), and then grab the IDs of the remaining subnets in Terraform v0.11.14. This is how I create the subnets:
resource "aws_subnet" "private" {
vpc_id = "${var.vpc_id}"
cidr_block = "${element(split(",", var.cidrs), count.index)}"
availability_zone = "${element(split(",", var.azs), count.index)}"
count = "${length(split(",", var.cidrs))}"
tags { Name = "${var.name}.${element(split(",", var.azs), count.index)}" }
lifecycle { create_before_destroy = true }
}
I know how to achieve my result in Terraform v1.0.0:
output "subnet_ids" { value = "${join(",", list(for subnet in aws_subnet.private.*: subnet.id if substr(subnet.name, -1, 1) != "d" ))}" }
I'm trying to do the same thing in Terraform v0.11.14 by just cutting off the last subnet in hopes that the subnets are by default sorted by name, but they aren't:
output "subnet_ids" { value = "${join(",", slice(aws_subnet.private.*.id, 0, length(aws_subnet.private.*.id) - 1))}" }
Anyone know how to achieve what I'm trying to do with Terraform v0.11.14?
You can check the following. As I understand, your filtering is based on the last letter of AZ name. Thus maybe you can sort by AZ, and remove last one, as you tried:
locals {
sorted_az = "${sort(aws_subnet.private.*.availability_zone)}"
length_az_minus_1 = "${length(local.sorted_az)-1}"
azs_without_last = "${slice(local.sorted_az, 0, local.length_az_minus_1)}"
}
output "subnet_ids" {
value = "${ matchkeys(aws_subnet.private.*.id, aws_subnet.private.*.availability_zone, local.azs_without_last)}"
}

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.