terraform/website/docs/configuration/meta-arguments/for_each.html.md

7.2 KiB

layout page_title
language The for_each Meta-Argument - Configuration Language

The for_each Meta-Argument

-> Version note: for_each was added in Terraform 0.12.6. Module support for for_each was added in Terraform 0.13, and previous versions can only use it with resources.

-> Note: A given resource or module block cannot use both count and for_each.

Hands-on: Try the Manage Similar Resources With For Each tutorial on HashiCorp Learn.

By default, a resource block configures one real infrastructure object (and similarly, a module block includes a child module's contents into the configuration one time). However, sometimes you want to manage several similar objects (like a fixed pool of compute instances) without writing a separate block for each one. Terraform has two ways to do this: count and for_each.

If a resource or module block includes a for_each argument whose value is a map or a set of strings, Terraform will create one instance for each member of that map or set.

Basic Syntax

for_each is a meta-argument defined by the Terraform language. It can be used with modules and with every resource type.

The for_each meta-argument accepts a map or a set of strings, and creates an instance for each item in that map or set. Each instance has a distinct infrastructure object associated with it, and each is separately created, updated, or destroyed when the configuration is applied.

-> Note: The keys of the map (or all the values in the case of a set of strings) must be known values, or you will get an error message that for_each has dependencies that cannot be determined before apply, and a -target may be needed. for_each keys cannot be the result (or rely on the result of) of impure functions, including uuid, bcrypt, or timestamp, as their evaluation is deferred during the main evaluation step.

Map:

resource "azurerm_resource_group" "rg" {
  for_each = {
    a_group = "eastus"
    another_group = "westus2"
  }
  name     = each.key
  location = each.value
}

Set of strings:

resource "aws_iam_user" "the-accounts" {
  for_each = toset( ["Todd", "James", "Alice", "Dottie"] )
  name     = each.key
}

Child module:

# my_buckets.tf
module "bucket" {
  for_each = toset(["assets", "media"])
  source   = "./publish_bucket"
  name     = "${each.key}_bucket"
}
# publish_bucket/bucket-and-cloudfront.tf
variable "name" {} # this is the input parameter of the module

resource "aws_s3_bucket" "example" {
  # Because var.name includes each.key in the calling
  # module block, its value will be different for
  # each instance of this module.
  bucket = var.name

  # ...
}

resource "aws_iam_user" "deploy_user" {
  # ...
}

The each Object

In blocks where for_each is set, an additional each object is available in expressions, so you can modify the configuration of each instance. This object has two attributes:

  • each.key — The map key (or set member) corresponding to this instance.
  • each.value — The map value corresponding to this instance. (If a set was provided, this is the same as each.key.)

Using Expressions in for_each

The for_each meta-argument accepts map or set expressions. However, unlike most arguments, the for_each value must be known before Terraform performs any remote resource actions. This means for_each can't refer to any resource attributes that aren't known until after a configuration is applied (such as a unique ID generated by the remote API when an object is created).

The for_each value must be a map or set with one element per desired resource instance. When providing a set, you must use an expression that explicitly returns a set value, like the toset function; to prevent unwanted surprises during conversion, the for_each argument does not implicitly convert lists or tuples to sets. If you need to declare resource instances based on a nested data structure or combinations of elements from multiple data structures you can use Terraform expressions and functions to derive a suitable value. For example:

Referring to Instances

When for_each is set, Terraform distinguishes between the block itself and the multiple resource or module instances associated with it. Instances are identified by a map key (or set member) from the value provided to for_each.

  • <TYPE>.<NAME> or module.<NAME> (for example, azurerm_resource_group.rg) refers to the block.
  • <TYPE>.<NAME>[<KEY>] or module.<NAME>[<KEY>] (for example, azurerm_resource_group.rg["a_group"], azurerm_resource_group.rg["another_group"], etc.) refers to individual instances.

This is different from resources and modules without count or for_each, which can be referenced without an index or key.

Similarly, resources from child modules with multiple instances are prefixed with module.<NAME>[<KEY>] when displayed in plan output and elsewhere in the UI. For a module without count or for_each, the address will not contain the module index as the module's name suffices to reference the module.

-> Note: Within nested provisioner or connection blocks, the special self object refers to the current resource instance, not the resource block as a whole.

Using Sets

The Terraform language doesn't have a literal syntax for set values, but you can use the toset function to explicitly convert a list of strings to a set:

locals {
  subnet_ids = toset([
    "subnet-abcdef",
    "subnet-012345",
  ])
}

resource "aws_instance" "server" {
  for_each = local.subnet_ids

  ami           = "ami-a1b2c3d4"
  instance_type = "t2.micro"
  subnet_id     = each.key # note: each.key and each.value are the same for a set

  tags = {
    Name = "Server ${each.key}"
  }
}

Conversion from list to set discards the ordering of the items in the list and removes any duplicate elements. toset(["b", "a", "b"]) will produce a set containing only "a" and "b" in no particular order; the second "b" is discarded.

If you are writing a module with an input variable that will be used as a set of strings for for_each, you can set its type to set(string) to avoid the need for an explicit type conversion:

variable "subnet_ids" {
  type = set(string)
}

resource "aws_instance" "server" {
  for_each = var.subnet_ids

  # (and the other arguments as above)
}