Moshe
Moshe

Reputation: 5139

Terraform get list index on for_each

Terraform newbie here. I'd like to iterate a list using for_each, but it seems like the key and value are the same:

provider "aws" {
  profile = "default"
  region  = "us-east-1"
}

variable "vpc_cidrs" {
  default = ["10.0.0.0/16", "10.1.0.0/16"]
}

resource "aws_vpc" "vpc" {
  for_each             = toset(var.vpc_cidrs)
  cidr_block           = each.value
  enable_dns_hostnames = true
  tags                 = { Name = "Company0${each.key}" }
}

I'd like the tag Name to be "Name" = "Company01" and "Name" = "Company02" but according to terraform apply, I get: "Name" = "Company010.0.0.0/16" and "Name" = "Company010.1.0.0/16" What am I missing?

Upvotes: 37

Views: 115955

Answers (6)

Skyborg
Skyborg

Reputation: 874

In foreach you can use something like this:

provider "aws" {
 profile = "default"
 region  = "us-east-1"
}

variable "vpc_cidrs" {
 default = ["10.0.0.0/16", "10.1.0.0/16"]
}

 resource "aws_vpc" "vpc" {
  for_each             = toset(var.vpc_cidrs)
  cidr_block           = each.value
  enable_dns_hostnames = true
  tags                 = { Name = "Company0${index(keys(var.vpc_cidrs), each.key)}" }
}

You get the keys with:

keys(var.vpc_cidrs) 

and after that you find the key in the keys with:

index(keys(var.vpc_cidrs), each.key)

I am using this for index and priority as well e.g.:

 resource "azurerm_network_security_rule" "rules" {
 for_each = var.nw_rules
 name                        = each.value["name"]
 priority                    = 1100 + index(keys(var.nw_rules), each.key)
 direction                   = "Inbound"
 access                      = "Allow"
 protocol                    = each.value["protocol"]
 source_port_range           = each.value["source_port_range"]
 destination_port_range      = each.value["destination_port_range"]
 ...

}

Upvotes: 0

Rotem jackoby
Rotem jackoby

Reputation: 22088

One of the comments was asking about how to achieve this use an index when using for_each inside a dynamic block.

Short answer

This can be done by refering to <name-of-dynamic-block>.key (key is the index).


An example

Use case

I'll take a use case where I have one S3 bucket that have multiple folders inside it, and each folder has a dedicated cloudfront distribution pointing to it.

What I want in the scope of AWS

Let's say I have 2 folders inside the s3 bucket: name1 and name2.

So I'll have to create an aws_s3_bucket_policy that looks like this:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "AWS": "<arn-of-cloudfront-distribution-1>"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::some-bucket/name1/*"
        },
        {
            "Sid": "",
            "Effect": "Allow",
            "Principal": {
                "AWS": "<arn-of-cloudfront-distribution-2>"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::some-bucket/name2/*"
        }
    ]
}

(*) The creation of the cloudfront distributions was removed for brevity.

How to achieve this with Terraform

So I have a list of folders that needs to be created inside my s3 bucket:

variable "all_s3_folders" {
  type        = list(string)
  default     = []
  description = "All folders inside the bucket"
}

For each folder I created a cloudfront distribution (removed for brevity) and an cloudfront origin access identity (see oai) resources.

We will create those resources based on a count on the number of folders:

resource "aws_cloudfront_origin_access_identity" "oai" {
  count = length(var.all_s3_folders)
  comment = "some-comment"
}

Now we need to add each folder (+ its relevant cloudfront distrobution and oai) as an entry in the bucket policy.

I want to use dynamic block with for_each inside it but without relying on count.index.

But I'll still have to iterate over the aws_cloudfront_origin_access_identity resources with an index.

So,

In order to use an index when using for_each inside a dynamic block you just refer to <name-of-dynamic-block>.key - here it's statement.key

data "aws_iam_policy_document" "my-doc" {
  dynamic "statement" {
    for_each = var.folders_array
    content {
      effect = "Allow"

      actions = [
        "s3:GetObject"
      ]

      resources = [
        "${aws_s3_bucket.my_s3_bucket.arn}/${statement.value}/*" # <--Using statement value
      ]

      principals {
        type = "AWS"
        identifiers = [some_cloudfront_resource[statement.key].id] # <--Using statement key
      }
    }
  }
}

Upvotes: -1

Peter Arboleda
Peter Arboleda

Reputation: 521

You could use the count function and use the index to do this:

provider "aws" {
  profile = "default"
  region  = "us-east-1"
}

variable "vpc_cidrs" {
  type = list(string)
  default = ["10.0.0.0/16", "10.1.0.0/16"]
}

resource "aws_vpc" "vpc" {
  count = length(var.vpc_cidrs)
  cidr_block           = var.vpc_cidrs[count.index]
  enable_dns_hostnames = true
  tags                 = { Name = "Company0${count.index}" }
}

Upvotes: 5

mariux
mariux

Reputation: 3117

There is also another way of achieving the wanted result without using index():

change the following three lines:

for_each   = { for idx, cidr_block in var.vpc_cidrs: cidr_block => idx}
cidr_block = each.key
tags       = { Name = format("Company%02d", each.value + 1) }
  • the for_each will use a map of cidr_block to index mapping as returned by for
  • the cidr_block can then just use the each.key value
  • and in the tags also use format() on each.value to have a two digit with leading zeros of the index

Full example will be:

provider "aws" {
  profile = "default"
  region  = "us-east-1"
}

variable "vpc_cidrs" {
  default = ["10.0.0.0/16", "10.1.0.0/16"]
}

resource "aws_vpc" "vpc" {
  for_each             = { for idx, cidr_block in var.vpc_cidrs: cidr_block => idx}
  cidr_block           = each.key
  enable_dns_hostnames = true
  tags                 = { Name = format("Company%02d", each.value + 1) }
}

Upvotes: 39

Moshe
Moshe

Reputation: 5139

Found an easy solution using the index function:

tags = { Name = "Company0${index(var.vpc_cidrs, each.value) + 1}" }

Upvotes: 53

Max Smolens
Max Smolens

Reputation: 3811

When for_each is used with a set, each.key and each.value are the same.

To generate strings like "Company01", "Company02", etc., you need the index of each CIDR block in the list. One way to do this is to create a local map using a for expression like:

locals {
  vpc_cidrs = {for s in var.vpc_cidrs: index(var.vpc_cidrs, s) => s}
}

resource "aws_vpc" "vpc" {
  for_each             = local.vpc_cidrs
  cidr_block           = each.value
  enable_dns_hostnames = true
  tags                 = { Name = "Company0${each.key}" }
}

As a bonus, you can use the format function to build the zero-padded name string:

resource "aws_vpc" "vpc" {
  ...
  tags                 = { Name = format("Company%02d", each.key) }
}

Upvotes: 15

Related Questions