count parameter

Loop over resources and modules.

resource "aws_iam_user" "example" {
  count = 3
  name  = "neo.${count.index}"
}

After you’ve used count on a resource, it becomes an array of resources rather than just one resource. That means to read an attribute from a resource:

# instead of standard syntax
<PROVIDER>_<TYPE>.<NAME>.<ATTRIBUTE>
# array lookup syntax needs to be used
<PROVIDER>_<TYPE>.<NAME>[INDEX].ATTRIBUTE

Example:

output "first_arn" {
  value       = aws_iam_user.example[0].arn
  description = "The ARN for the first user"
}
 
# will output:
# first_arn = "arn:aws:iam::123456789012:user/neo.0"

or use splat expression (”*”) to access all items in the array:

output "all_arns" {
  value       = aws_iam_user.example[*].arn
  description = "The ARNs for all users"
}
 
# will output:
# all_arns = [
#  "arn:aws:iam::123456789012:user/neo.0",
#  "arn:aws:iam::123456789012:user/neo.1",
#  "arn:aws:iam::123456789012:user/neo.2",
#]

Limitations:

  • Can’t be used for inline blocks.
  • Can be tricky when values are removed from the array. For example:
variable "user_names" {
  description = "Create IAM users with these names"
  type        = list(string)
  default     = ["neo", "trinity", "morpheus"]
}

If you try to remove the middle item, instead of removing resource that deployed with it, all resource array will be shifted:

$ terraform plan
 
(...)
 
Terraform will perform the following actions:
 
  # aws_iam_user.example[1] will be updated in-place
  ~ resource "aws_iam_user" "example" {
        id            = "trinity"
      ~ name          = "trinity" -> "morpheus"
    }
 
  # aws_iam_user.example[2] will be destroyed
  - resource "aws_iam_user" "example" {
      - id            = "morpheus" -> null
      - name          = "morpheus" -> null
    }
 
Plan: 0 to add, 1 to change, 1 to destroy.

for_each is better suited for these situations, as it will remove exact, instead of shifting everything to the left.

for_each expression

Loop over resources, inline blocks within a resource, and modules.

resource "<PROVIDER>_<TYPE>" "<NAME>" {
  for_each = <COLLECTION>
 
  [CONFIG ...]
}

where COLLECTION is a set or map to loop over (lists are not supported when using for_each on a resource).

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

When for_each is used on the resource, it becomes a map of resources (instead of a single resource or an array of resources as with count):

output "all_users" {
  value = aws_iam_user.example
}
 
# will output:
# all_users = {
#   "morpheus" = {
#     "arn" = "arn:aws:iam::123456789012:user/morpheus"
#     "force_destroy" = false
#     "id" = "morpheus"
#     "name" = "morpheus"
#     "path" = "/"
#     "tags" = {}
#   }
#   "neo" = {
#     "arn" = "arn:aws:iam::123456789012:user/neo"
#     "force_destroy" = false
#     "id" = "neo"
#     "name" = "neo"
#     "path" = "/"
#     "tags" = {}
#   }
#   "trinity" = {
#     "arn" = "arn:aws:iam::123456789012:user/trinity"
#     "force_destroy" = false
#     "id" = "trinity"
#     "name" = "trinity"
#     "path" = "/"
#     "tags" = {}
#   }
# }

To extract specific attribute for all resources in the map:

output "all_arns" {
  value = values(aws_iam_user.example)[*].arn
}
 
# will output
# all_arns = [
#  "arn:aws:iam::123456789012:user/morpheus",
#  "arn:aws:iam::123456789012:user/neo",
#  "arn:aws:iam::123456789012:user/trinity",
# ]

Inline blocks

To dynamically generate inline blocks:

dynamic "<VAR_NAME>" {
  for_each = <COLLECTION>
 
  content {
    [CONFIG...]
  }
}

where VAR_NAME is the name to use for the variable that will store the value of each “iteration,” COLLECTION is a list or map to iterate over, and the content block is what to generate from each iteration.

Use <VAR_NAME>.key and <VAR_NAME>.value within the content block to access the key and value, respectively, of the current item in the COLLECTION.

When you’re using for_each with a list, the key will be the index, and the value will be the item in the list at that index, and when using for_each with a map, the key and value will be one of the key-value pairs in the map.

Example:

resource "aws_autoscaling_group" "example" {
  launch_configuration = aws_launch_configuration.example.name
  vpc_zone_identifier  = data.aws_subnets.default.ids
  target_group_arns    = [aws_lb_target_group.asg.arn]
  health_check_type    = "ELB"
 
  min_size = var.min_size
  max_size = var.max_size
 
  tag {
    key                 = "Name"
    value               = var.cluster_name
    propagate_at_launch = true
  }
 
  dynamic "tag" {
    for_each = var.custom_tags
 
    content {
      key                 = tag.key
      value               = tag.value
      propagate_at_launch = true
    }
  }
}

for expression

Loop over lists and maps.

Lists

Syntax:

# list output
[for <ITEM> in <LIST> : <OUTPUT>]
 
# map output
{for <ITEM> in <LIST> : <OUTPUT_KEY> => <OUTPUT_VALUE>}

Examples:

output "upper_names" {
  value = [for name in var.names : upper(name)]
}
 
output "short_upper_names" {
  value = [for name in var.names : upper(name) if length(name) < 5]
}
 
output "upper_roles" {
  value = {for name, role in var.hero_thousand_faces : upper(name) => upper(role)}
}

Maps

Syntax:

# list output
[for <KEY>, <VALUE> in <MAP> : <OUTPUT>]
 
# map output
{for <KEY>, <VALUE> in <MAP> : <OUTPUT_KEY> => <OUTPUT_VALUE>}

Examples:

variable "hero_thousand_faces" {
  description = "map"
  type        = map(string)
  default     = {
    neo      = "hero"
    trinity  = "love interest"
    morpheus = "mentor"
  }
}
 
output "bios" {
  value = [for name, role in var.hero_thousand_faces : "${name} is the ${role}"]
}
 
# will output:
# bios = [
#   "morpheus is the mentor",
#   "neo is the hero",
#   "trinity is the love interest",
# ]
 
output "upper_roles" {
  value = {for name, role in var.hero_thousand_faces : upper(name) => upper(role)}
}

for string directive

Loop over lists and maps withing a string.

String directives allow you to use control statements (e.g., for-loops and if-statements) within strings using a syntax similar to string interpolations, but instead of a dollar sign and curly braces (${…}), you use a percent sign and curly braces (%{…}).

Syntax:

%{ for <ITEM> in <COLLECTION> }<BODY>%{ endfor }
 
# with index
%{ for <INDEX>, <ITEM> in <COLLECTION> }<BODY>%{ endfor }

Example:

variable "names" {
  description = "Names to render"
  type        = list(string)
  default     = ["neo", "trinity", "morpheus"]
}
 
output "for_directive" {
  value = "%{ for name in var.names }${name}, %{ endfor }"
}
 
# for_directive = "neo, trinity, morpheus, "
 
output "for_directive_index" {
  value = "%{ for i, name in var.names }(${i}) ${name}, %{ endfor }"
}
 
# for_directive_index = "(0) neo, (1) trinity, (2) morpheus, "

To get rid of the comma and space and the end, use [[Terraform conditionals#if-string-directive|if string directive]]:

output "for_directive_index_if_else_strip" {
  value = <<EOF
%{~ for i, name in var.names ~}
${name}%{ if i < length(var.names) - 1 }, %{ else }.%{ endif }
%{~ endfor ~}
EOF
}
 
# for_directive_index_if_else_strip = "neo, trinity, morpheus."