Terraform Loops
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.Categories:
8 minute read
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 Type | Brief Explanation |
---|---|
count | Creates 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_each | Creates 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.
WARNING
In most cases you would usefor_each
to create multipe resources rather than count
as if affords a map
result of values instead of an array
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"