Terraform Loops

Terraform loops, implemented using count or for_each meta-arguments, allow the creation of multiple instances of a resource or module based on an integer count or items in a map or set, simplifying repetitive resource configurations.

In Terraform, there are two primary ways to implement loops: count and for_each. These are meta-arguments that can be used with the resource and module blocks to create multiple instances of a resource or module. Here’s a brief explanation of both:

Loop TypeBrief Explanation
countCreates multiple instances of a resource or module based on an integer count. The count.index can be used to distinguish between instances. This is suitable for creating resources that have a similar configuration but don’t require unique keys.
for_eachCreates multiple instances of a resource or module for each item in a map or set. This is more powerful and flexible than count, as it allows for better tracking of resource instances using keys, which can be useful when managing complex resources.

count

The Terraform code defines an AWS IAM user resource using the count meta-argument to create multiple user instances based on the length of the user_names variable, which is a list of strings with default values “david”, “brian”, and “marc”. The names for each user instance are set using the count.index value to reference the corresponding user name in the user_names list.

This code creates and array which is a list of values with each value address using zero-based indexing, so [0]:[1]:[2].

Keep in mind that using count has a significant drawback. If you want to delete user [1] (brian) it would result in user [2] (marc) being recreated. This is because count is simply an array based on zero indexing and it cannot have gaps between indexes. Meaning with a count of 3 you have an array indexed as [0]:[1]:[2]. If you remove 1 then the array be becomes [0]:[2] not [0]:[1] so index [2] is deleted and recreated as [1] to replace the original. This could cause issue in real life as you are re-creating already deployed resources without intending to.


provider "aws" {
  region = "us-east-2"
}

resource "aws_iam_user" "example" {
  count = length(var.user_names)
  name = var.user_names[count.index]
}

variable "user_names" {
  description = "Usernames of the users"
  type = list(string)
  default = ["david", "brian", "marc"]
}

Terraform will perform the following actions:

  # aws_iam_user.example[0] will be created
  + resource 
aws_iam_user example {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = david
      + path          = /
      + tags_all      = (known after apply)
      + unique_id     = (known after apply)
    }

  # aws_iam_user.example[1] will be created
  + resource aws_iam_user example {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = brian
      + path          = /
      + tags_all      = (known after apply)
      + unique_id     = (known after apply)
    }

  # aws_iam_user.example[2] will be created
  + resource aws_iam_user example {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = marc
      + path          = /
      + tags_all      = (known after apply)
      + unique_id     = (known after apply)
    }

Plan: 3 to add, 0 to change, 0 to destroy

for_each

You can loop over list, set, and maps with a for_each loop.

This example loops over each username in the list variable of usernames and creates each IAM user. The toset converts the variable list into a set as for_each only supports set or maps when used with a resource block.

The result of this code is a map with a key:value pair of result instead of array. This would allow you to target a resource, such as removing a user, without affecting the other resources as each map value is independant of each other.


provider "aws" {
  region = "us-east-2"
}

resource "aws_iam_user" "example" {
  for_each = toset(var.user_names)
  name = each.value
}

variable "user_names" {
  description = "Usernames of the users"
  type = list(string)
  default = ["david", "brian", "marc"]
}

output "all_arns" {
  value = values(aws_iam_user.example)[*].arn
}

Terraform will perform the following actions:

  # aws_iam_user.example["brian"] will be created
  + resource "aws_iam_user" "example" {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = "brian"
      + path          = "/"
      + tags_all      = (known after apply)
      + unique_id     = (known after apply)
    }

  # aws_iam_user.example["david"] will be created
  + resource "aws_iam_user" "example" {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = "david"
      + path          = "/"
      + tags_all      = (known after apply)
      + unique_id     = (known after apply)
    }

  # aws_iam_user.example["marc"] will be created
  + resource "aws_iam_user" "example" {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = "marc"
      + path          = "/"
      + tags_all      = (known after apply)
      + unique_id     = (known after apply)
    }

Plan: 3 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + all_arns = [
      + (known after apply),
      + (known after apply),
      + (known after apply),
    ]

## After Apply
Outputs:

all_arns = [
  "arn:aws:iam::232564287778:user/brian",
  "arn:aws:iam::232564287778:user/david",
  "arn:aws:iam::232564287778:user/marc",
]

for

This is not to be confused with for_each

In Terraform, the for expression is used to transform one collection (list, set, or map) into another collection by iterating over the elements of the input collection and applying a transformation to each element.

The general syntax for a for expression is:

lists or sets:

[for <element> in <input_collection>: <output_expression> if <condition>]

maps:

{for <key>, <value> in <input_map>: <key_expression> => <value_expression> if <condition>}

The example below takes the output of the IAM user creation and runs for over the results convering them to UPPERCASE.


provider "aws" {
  region = "us-east-2"
}

resource "aws_iam_user" "example" {
  for_each = toset(var.user_names)
  name = each.value
}

variable "user_names" {
  description = "Usernames of the users"
  type = list(string)
  default = ["david", "brian", "marc"]
}

output "upper_names" {
  value = [for name in var.user_names : upper(name)]
}

Terraform will perform the following actions:

  # aws_iam_user.example[
brian] will be created
  + resource aws_iam_user example {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = brian
      + path          = /
      + tags_all      = (known after apply)
      + unique_id     = (known after apply)
    }

  # aws_iam_user.example[david] will be created
  + resource aws_iam_user example {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = david
      + path          = /
      + tags_all      = (known after apply)
      + unique_id     = (known after apply)
    }

  # aws_iam_user.example[marc] will be created
  + resource aws_iam_user example {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = marc
      + path          = /
      + tags_all      = (known after apply)
      + unique_id     = (known after apply)
    }

Plan: 3 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + upper_names = [
      + DAVID,
      + BRIAN,
      + MARC,
    ]

Other examples include:

Filtering a list to include only elements that meet a condition:

locals {
  input_list = [1, 2, 3, 4, 5, 6]
  even_numbers = [for number in local.input_list: number if number % 2 == 0]
}

Transforming a map by appending a suffix to the keys:

locals {
  input_map = {
    "color1" = "red"
    "color2" = "blue"
    "color3" = "green"
  }
  output_map = {for key, value in local.input_map: "${key}_new" => value}
}

Filtering a map to include only key-value pairs that meet a condition:

locals {
  input_map = {
    "color1" = "red"
    "color2" = "blue"
    "color3" = "green"
  }
  red_only_map = {for key, value in local.input_map: key => value if value == "red"}
}

for loop and string directive

A string directive is a way to include or embed expressions, variables, or other values within a string by using a special syntax, allowing you to create dynamic and customized strings based on the combined or calculated values.


provider "aws" {
  region = "us-east-2"
}

resource "aws_iam_user" "example" {
  for_each = toset(var.user_names)
  name = each.value
}

variable "user_names" {
  description = "Usernames of the users"
  type = list(string)
  default = ["david", "brian", "marc"]
}

output "for_directive" {
    value = "%{ for name in var.user_names }${name}, %{ endfor}"
}

Terraform will perform the following actions:

  # aws_iam_user.example[
brian] will be created
  + resource aws_iam_user example {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = brian
      + path          = /
      + tags_all      = (known after apply)
      + unique_id     = (known after apply)
    }

  # aws_iam_user.example[david] will be created
  + resource aws_iam_user example {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = david
      + path          = /
      + tags_all      = (known after apply)
      + unique_id     = (known after apply)
    }

  # aws_iam_user.example[marc] will be created
  + resource aws_iam_user example {
      + arn           = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + name          = marc
      + path          = /
      + tags_all      = (known after apply)
      + unique_id     = (known after apply)
    }

Plan: 3 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + for_directive = "david, brian, marc" 

Last modified July 21, 2024: update (e2ae86c)