for_each
Overview
The for_each meta-argument creates multiple instances of a resource or module from a map or a set of strings. Compared with count, for_each is more flexible for managing similar resources whose configuration values differ and cannot be derived from a numeric index.
for_each assigns a stable identifier (based on map keys or set elements) to each resource instance, so Terraform can accurately track individual instances even when the underlying collection order changes. This avoids the "resource update storm" that occurs with count, where adding or removing a list element shifts all subsequent indexes.
Two special attributes, each.key and each.value, let you reference the key and value of the current iteration in the resource configuration, giving each instance a different configuration. This makes for_each well suited for creating resource collections with complex, varied configurations, such as multiple security group rule types, buckets with different settings, or compute instances with different tags and attributes.
This topic covers the syntax and usage of for_each, compares it with count, and provides Alibaba Cloud examples to help you simplify and optimize your infrastructure management.
Note: A resource or module block cannot use both count and for_each.
Syntax
for_each is a meta-argument defined by Terraform that can be used with modules and all resource types.
The for_each meta-argument accepts a map or a set of strings and creates one instance for each item. Each instance is associated with a unique infrastructure object that is created, updated, or destroyed individually when the apply command runs.
Map example:
resource "alicloud_resource_manager_resource_group" "rg" {
for_each = {
group1 = "China (Hangzhou)"
group2 = "China (Beijing)"
}
display_name = each.key
resource_group_name = "${each.key}-${each.value}"
}
String set example:
resource "alicloud_ram_user" "users" {
for_each = toset(["alice", "bob", "charlie", "dave"])
name = each.key
display_name = "User ${each.key}"
}
Example of child modules:
# my_buckets.tf
module "bucket" {
for_each = toset(["assets", "media"])
source = "./oss_bucket"
name = "${each.key}_bucket"
}
# oss_bucket/bucket.tf
variable "name" {}# This is the input argument of the module
resource "alicloud_oss_bucket" "example" {
# As var.name contains each.key in the called module block,
# its value is different for each instance of this module.
bucket = var.name
# ...
}
resource "alicloud_ram_user" "deploy_user" {
name = "${var.name}_deployer"
# ...
}
each object
In a block that contains the for_each argument, you can use an additional each object to customize each instance. This object has two attributes:
-
each.key - The map key or set element corresponding to this instance.
-
each.value - The map value corresponding to this instance. (If a set is provided, each.value is the same as each.key.)
Limits
-
The keys of a for_each map or all values of a string set must be known, or you will receive an error indicating that for_each has a dependency that cannot be determined by the apply command. You may need to use -target to resolve this.
-
The for_each key cannot be the result of or depend on a dynamic function such as uuid, bcrypt, or timestamp, because the preview results of these functions are deferred during plan execution.
-
Sensitive values (values marked as sensitive, such as sensitive input variables, sensitive outputs, or sensitive resource attributes) cannot be used as arguments of for_each. The values used in for_each identify resource instances and are always displayed in the UI output, which is why sensitive values are not allowed. Using sensitive values as for_each arguments causes an error. If you convert a value containing sensitive data into a for_each argument, note that most Terraform functions return sensitive results when given a sensitive argument. In many cases, you can achieve similar results with a for expression. For example, if you want to call keys(local.map), where local.map is an object with a sensitive value (but not a sensitive key), you can use toset([for k, v in local.map : k]) to create a value that is passed to for_each.
Use expressions in for_each
The for_each meta-argument accepts a map or set expression. However, unlike most arguments, the value of for_each must be known before Terraform performs any remote resource operations. Therefore, for_each cannot reference unknown resource attributes that are generated during object creation, such as a unique ID returned by a remote API.
A for_each value must be a map or set with one element per desired resource instance. To use a sequence as a for_each value, you must use an expression that explicitly returns a set value, such as the toset function. The for_each argument does not implicitly convert a list or array to a set, which prevents unexpected behavior during conversion. To declare resource instances based on a nested data structure or a combination of elements from multiple data structures, use Terraform expressions and functions to construct the appropriate value. Examples:
-
Use a nested for expression with the flatten function to convert a multi-level nested structure to a flat list.
-
Use the setproduct function within a for expression to generate an exhaustive list of two or more elements.
Reference for_each between resources
Resources that use for_each appear as object mappings in expressions, so you can directly use one resource as the for_each value of another resource when a one-to-one association exists.
For example, in Alibaba Cloud, a VPC is typically associated with many dependent resources, such as vSwitches and security groups. If you use for_each to declare multiple VPCs, you can reference the VPC resource as the for_each value of another resource to declare a vSwitch for each VPC:
variable "vpcs" {
type = map(object({
cidr_block = string
zone_id = string
}))
}
resource "alicloud_vpc" "example" {
# Create a VPC for each element in var.vpcs
for_each = var.vpcs
# each.value is a value in var.vpcs
vpc_name = each.key
cidr_block = each.value.cidr_block
}
resource "alicloud_vswitch" "example" {
# One vSwitch for each VPC
for_each = alicloud_vpc.example
# each.value is a complete alicloud_vpc object
vpc_id = each.value.id
cidr_block = cidrsubnet(each.value.cidr_block, 8, 0)
zone_id = var.vpcs[each.key].zone_id
vswitch_name = "${each.key}-default-vswitch"
}
output "vpc_ids" {
value = {
for k, v in alicloud_vpc.example : k => v.id
}
}
This reference pattern clearly expresses the relationship between vSwitches and VPCs, ensuring that their instance keys change together. It also makes the code easier to understand and maintain.
Reference instances
After you configure the for_each argument, Terraform distinguishes between the block itself and the individual resource or module instances associated with it. Each instance is identified by a map key or set element from the value provided to for_each.
-
<TYPE>.<NAME>ormodule.<NAME>. For example,alicloud_resource_manager_resource_group.rgreferences a block. -
<TYPE>.<NAME>[<KEY>]ormodule.<NAME>[<KEY>]. For example,alicloud_resource_manager_resource_group.rg["group1"]oralicloud_resource_manager_resource_group.rg["group2"]references a single instance.
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 outputs and elsewhere in the UI. If a module does not contain the count or for_each argument, the address does not contain the module index because the module can be referenced by name.
Note: In a nested provisioner or connection block, the special self object references the current resource instance, not the entire resource block.
Use a set
Terraform does not have a literal syntax for set values. You can use the toset function to explicitly convert a list of strings to a set:
locals {
vswitch_ids = toset([
"vsw-abc123456",
"vsw-def789012",
])
}
resource "alicloud_instance" "server" {
for_each = local.vswitch_ids
instance_name = "server-${each.key}"
instance_type = "ecs.s6-c1m2.small"
image_id = "aliyun_2_1903_x64_20G_alibase_20230523.vhd"
vswitch_id=each.key# Note: For sets, each.key and each.value are the same.
security_groups = ["sg-xyz123456"]
tags = {
Name = "Server ${each.key}"
}
}
The list-to-set conversion ignores element order and removes duplicates. toset(["b", "a", "b"]) produces a set containing only "a" and "b" in no particular order, and the second "b" is discarded.
If you are writing a module whose input variable will be used as a set of strings for for_each, you can set its type to set(string) to avoid explicit type conversion:
variable "vswitch_ids" {
type = set(string)
}
resource "alicloud_instance" "server" {
for_each = var.vswitch_ids
instance_name = "server-${each.key}"
instance_type = "ecs.s6-c1m2.small"
image_id = "aliyun_2_1903_x64_20G_alibase_20230523.vhd"
vswitch_id = each.key
security_groups = ["sg-xyz123456"]
# ... (Other arguments are the same as above)
}
Examples
Create multiple RAM users and AccessKey pairs
variable "users" {
description = "Map of RAM users to create"
type = map(object({
email = string
role = string
}))
default = {
"dev-user1" = {
email = "dev1@example.com"
role = "developer"
},
"dev-user2" = {
email = "dev2@example.com"
role = "developer"
},
"admin-user" = {
email = "admin@example.com"
role = "administrator"
}
}
}
resource "alicloud_ram_user" "users" {
for_each = var.users
name = each.key
display_name = each.key
email = each.value.email
}
resource "alicloud_ram_access_key" "keys" {
for_each = alicloud_ram_user.users
user_name = each.value.name
status = "Active"
secret_file = "./credentials/ak-${each.value.name}.txt"
}
# Assign different policies to users
resource "alicloud_ram_user_policy_attachment" "policy" {
for_each = var.users
policy_name = each.value.role == "administrator" ? "AdministratorAccess" : "ReadOnlyAccess"
policy_type = "System"
user_name = alicloud_ram_user.users[each.key].name
}
Use a nested data structure to create a multi-region resource
locals {
# A nested structure that contains regions and resource definitions for each region
regions = {
hangzhou = {
display_name = "Hangzhou"
zone_id = "cn-hangzhou-i"
instances = ["small", "medium"]
}
beijing = {
display_name = "Beijing"
zone_id = "cn-beijing-h"
instances = ["medium", "large"]
}
}
# Use the for expression and flatten to create a flat list of instance configurations
instance_configs = flatten([
for region_key, region in local.regions : [
for instance_type in region.instances : {
region_key = region_key
region_name = region.display_name
zone_id = region.zone_id
instance_type = instance_type
instance_name = "${region_key}-${instance_type}"
}
]
])
# Convert to a map that can be used by for_each
instance_map = {
for config in local.instance_configs :
config.instance_name => config
}
}
# Create multiple instances using the converted map
resource "alicloud_instance" "multi_region_servers" {
for_each = local.instance_map
instance_name = each.value.instance_name
instance_type = each.value.instance_type == "small" ? "ecs.s6-c1m2.small" : (
each.value.instance_type == "medium" ? "ecs.s6-c1m4.xlarge" : "ecs.s6-c1m8.2xlarge")
image_id = "aliyun_2_1903_x64_20G_alibase_20230523.vhd"
# Assume that you have created a VPC and a vSwitch for each region.
vswitch_id = "${vsw_id}_each.value.region_key" # The actual vSwitch ID
security_groups = ["sg-1231454254"] # The actual security group ID
tags = {
Name = each.value.instance_name
Region = each.value.region_name
Type = each.value.instance_type
}
}
Use setproduct to create a multi-dimensional resource combination
locals {
environments = ["dev", "staging", "prod"]
instance_types = ["web", "app", "db"]
# Use setproduct to create all possible combinations
combinations = [
for pair in setproduct(local.environments, local.instance_types) : {
env = pair[0]
type = pair[1]
name = "${pair[0]}-${pair[1]}"
}
]
# Convert to a map that can be used by for_each
combination_map = {
for combo in local.combinations :
combo.name => combo
}
}
# Create a security group for each combination
resource "alicloud_security_group" "by_env_and_type" {
for_each = local.combination_map
security_group_name = "sg-${each.key}"
description = "Security group for ${each.value.type} in ${each.value.env} environment"
vpc_id = "vpc-abc123456" # Use the existing VPC ID.
}
# Create security group rules that are suitable for each environment and type.
resource "alicloud_security_group_rule" "rules" {
for_each = local.combination_map
security_group_id = alicloud_security_group.by_env_and_type[each.key].id
type = "ingress"
ip_protocol = "tcp"
# Configure different port rules based on the instance type.
port_range = each.value.type == "web" ? "80/80" : (
each.value.type == "app" ? "8080/8080" : "3306/3306")
# Only allow access from specific IP addresses in the production environment
cidr_ip = each.value.env == "prod" ? "10.0.0.0/8" : "0.0.0.0/0"
description = "Default rule for ${each.value.type} in ${each.value.env}"
}
By using for_each, you can manage multiple similar resource instances more flexibly and keep resource identities stable even when the configuration order changes. This enables Terraform to plan changes more accurately and reduces unnecessary resource deletion and recreation.
Note: This topic is based on the official Terraform topic for_each. For more information, see the official documentation.