for_each
概述
for_each 是 Terraform 中的一个强大元参数,它允许您基于映射(map)或字符串集合(set of strings)创建资源或模块(Module)的多个实例。与 count 元参数相比,for_each 提供了更灵活的方式来管理类似资源的集合,特别是当每个实例需要使用不同的配置值,并且这些值无法简单地从数字索引派生时。
for_each 的主要优势在于它为每个资源实例分配了一个稳定的标识符(基于映射键或集合成员),这使得 Terraform 能够精确跟踪单个资源实例,即使底层集合的顺序发生变化。这种标识方式避免了使用 count 时可能遇到的“资源更新风暴”问题,即当列表中添加或删除元素时,所有后续索引都会发生变化。
通过 each.key 和 each.value 两个特殊属性,您可以在资源配置中引用当前迭代的键和值,从而为每个实例提供不同的配置。这使得 for_each 特别适合于创建需要复杂和多样化配置的资源集合,如多种类型的安全组规则、不同配置的存储桶或具有不同标签和属性的计算实例。
本文档将详细介绍 for_each 元参数的语法、用法、与 count 的比较,并提供在阿里云环境中的各种实际应用示例,帮助您充分利用这一强大特性来简化和优化您的基础设施管理。
注意:一个资源或Module块不能同时使用 count 和 for_each。
基本语法
for_each 是由 Terraform 语言定义的元参数。它可以与模块和所有资源类型一起使用。
for_each 元参数接受一个映射或字符串集合,并为该映射或集合中的每个项创建一个实例。每个实例都有一个与之关联的独特基础设施对象,并且在执行 apply 命令时,每个对象都会被单独创建、更新或销毁。
映射示例:
resource "alicloud_resource_manager_resource_group" "rg" {
for_each = {
group1 = "华东1(杭州)"
group2 = "华北2(北京)"
}
display_name = each.key
resource_group_name = "${each.key}-${each.value}"
}
字符串集合示例:
resource "alicloud_ram_user" "users" {
for_each = toset(["alice", "bob", "charlie", "dave"])
name = each.key
display_name = "User ${each.key}"
}
子模块示例:
# my_buckets.tf
module "bucket" {
for_each = toset(["assets", "media"])
source = "./oss_bucket"
name = "${each.key}_bucket"
}
# oss_bucket/bucket.tf
variable "name" {} # 这是模块的输入参数
resource "alicloud_oss_bucket" "example" {
# 因为 var.name 包含调用Module块中的 each.key,
# 其值对于此模块的每个实例都将不同。
bucket = var.name
# ...
}
resource "alicloud_ram_user" "deploy_user" {
name = "${var.name}_deployer"
# ...
}
each 对象
在设置了 for_each 的块中,表达式中可以使用额外的 each 对象,因此您可以修改每个实例的代码配置。此对象有两个属性:
each.key — 对应于此实例的映射键(或集合成员)。
each.value — 对应于此实例的映射值。(如果提供的是集合,则这与 each.key 相同。)
使用限制
for_each 映射的键(或在字符串集合的情况下的所有值)必须是已知值,否则您将收到错误消息,提示 for_each 具有无法在 apply 命令前确定的依赖关系,可能需要使用 -target。
for_each 键不能是动态函数(包括 uuid、bcrypt 或 timestamp)的结果(或依赖于这些函数的结果),因为它们的预览结果在整个 plan 命令执行期间被推迟。
敏感值(标记为 sensitive 的值,如敏感输入变量、敏感输出或敏感资源属性)不能用作 for_each 的参数。在 for_each 中使用的值用于标识资源实例,并且将始终在 UI 输出中显示,这就是为什么不允许敏感值,尝试使用敏感值作为 for_each 参数将导致错误。如果您将包含敏感数据的值转换为要在 for_each 中使用的参数,请注意,Terraform 中的大多数函数在给定包含任何敏感内容的参数时都会返回敏感结果。在许多情况下,您可以通过使用 for 表达式实现与用于此目的的函数类似的结果。例如,如果您想调用 keys(local.map),其中 local.map 是一个具有敏感值(但非敏感键)的对象,您可以使用 toset([for k, v in local.map : k]) 创建一个传递给 for_each 的值。
在 for_each 中使用表达式
for_each 元参数接受映射或集合表达式。然而,与大多数参数不同,for_each 值必须在 Terraform 执行任何远程资源操作之前已知。这意味着 for_each 不能引用任何在配置应用之前未知的资源属性(例如,创建对象时由远程 API 生成的唯一 ID)。
for_each 值必须是每个所需资源实例有一个元素的映射或集合。要使用序列作为 for_each 值,您必须使用显式返回集合值的表达式,如 toset 函数。为了防止转换过程中出现不必要的意外,for_each 参数不会隐式地将列表或数组转换为集合。如果您需要根据嵌套数据结构或多个数据结构元素的组合声明资源实例,可以使用 Terraform 表达式和函数来构造合适的值。例如:
使用带有 flatten 函数的嵌套 for 表达式,将多级嵌套结构转换为扁平列表。
使用 for 表达式内的 setproduct 函数,生成两个或更多集合元素的详尽组合列表。
资源之间引用 for_each
由于使用 for_each 的资源在其他地方的表达式中使用时显示为对象映射,因此在有一对一关联关系的情况下,您可以直接使用一个资源作为另一个资源的 for_each。
例如,在阿里云中,一个 VPC 对象通常与许多其他依赖该 VPC 资源相关联,例如交换机,安全组等。如果您使用 for_each 声明多个 VPC 实例,那么您可以将该 for_each 链接到另一个资源中,为每个 VPC 声明一个交换机:
variable "vpcs" {
type = map(object({
cidr_block = string
zone_id = string
}))
}
resource "alicloud_vpc" "example" {
# 为 var.vpcs 中的每个元素创建一个 VPC
for_each = var.vpcs
# each.value 是 var.vpcs 中的一个值
vpc_name = each.key
cidr_block = each.value.cidr_block
}
resource "alicloud_vswitch" "example" {
# 每个 VPC 一个交换机
for_each = alicloud_vpc.example
# each.value 是一个完整的 alicloud_vpc 对象
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
}
}
这种引用模式明确而简洁地声明了交换机实例和 VPC 实例之间的关系,这告诉 Terraform 期望两者的实例键总是一起变化,并且通常也使代码更容易被理解和维护。
引用实例
当设置了 for_each 时,Terraform 区分块本身和与之关联的多个资源或模块实例。实例由映射键(或集合成员)标识,该键来自提供给 for_each 的值。
<TYPE>.<NAME>
或module.<NAME>
(例如,alicloud_resource_manager_resource_group.rg
)引用块。<TYPE>.<NAME>[<KEY>]
或module.<NAME>[<KEY>]
(例如,alicloud_resource_manager_resource_group.rg["group1"]
,alicloud_resource_manager_resource_group.rg["group2"]
等)引用单个实例。
这与没有 count 或 for_each 的资源和模块不同,后者可以在没有索引或键的情况下被引用。
同样,来自具有多个实例的子模块的资源在 plan 输出和 UI 的其他地方显示时,会以 module.<NAME>[<KEY>]
为前缀。对于没有 count 或 for_each 的模块,地址不会包含模块索引,因为模块的名称足以引用模块。
注意:在嵌套的 provisioner 或 connection 块中,特殊的 self 对象引用当前资源实例,而不是整个资源块。
使用集合
Terraform 语言没有集合值的字面语法,但您可以使用 toset 函数显式地将字符串列表转换为集合:
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 # 注意:对于集合,each.key 和 each.value 是相同的
security_groups = ["sg-xyz123456"]
tags = {
Name = "Server ${each.key}"
}
}
从列表到集合的转换会丢弃列表中项目的顺序并删除任何重复元素。toset(["b", "a", "b"]) 将生成一个仅包含 "a" 和 "b" 的集合,没有特定顺序;第二个 "b" 被丢弃。
如果您正在编写一个模块,其输入变量将用作 for_each 的字符串集合,您可以将其类型设置为 set(string),以避免需要显式类型转换:
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"]
# ... (其他参数与上文相同)
}
使用示例
创建多个 RAM 用户和访问密钥
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"
}
# 为用户分配不同的策略
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
}
使用嵌套数据结构创建多区域资源
locals {
# 包含区域和每个区域的资源定义的嵌套结构
regions = {
hangzhou = {
display_name = "杭州"
zone_id = "cn-hangzhou-i"
instances = ["small", "medium"]
}
beijing = {
display_name = "北京"
zone_id = "cn-beijing-h"
instances = ["medium", "large"]
}
}
# 使用 for 表达式和 flatten 创建实例配置的扁平列表
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}"
}
]
])
# 转换为 for_each 可以使用的映射
instance_map = {
for config in local.instance_configs :
config.instance_name => config
}
}
# 使用转换后的映射创建多个实例
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"
# 下面假设您已经为每个区域创建了 VPC 和交换机
vswitch_id = "${each.value.region_key}_vsw_id" # 实际上您会使用真实的交换机 ID
security_groups = ["sg-1231454254"] # 实际上您会使用真实的安全组 ID
tags = {
Name = each.value.instance_name
Region = each.value.region_name
Type = each.value.instance_type
}
}
使用 setproduct 创建多维资源组合
locals {
environments = ["dev", "staging", "prod"]
instance_types = ["web", "app", "db"]
# 使用 setproduct 创建所有可能的组合
combinations = [
for pair in setproduct(local.environments, local.instance_types) : {
env = pair[0]
type = pair[1]
name = "${pair[0]}-${pair[1]}"
}
]
# 转换为映射以供 for_each 使用
combination_map = {
for combo in local.combinations :
combo.name => combo
}
}
# 为每种组合创建安全组
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" # 假设使用现有的 VPC ID
}
# 创建适合每种环境和类型的安全组规则
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"
# 根据实例类型配置不同的端口规则
port_range = each.value.type == "web" ? "80/80" : (
each.value.type == "app" ? "8080/8080" : "3306/3306")
# 生产环境只允许特定 IP 访问
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}"
}
通过使用 for_each,您可以更灵活地管理多个类似的资源实例,并确保资源标识保持稳定,即使配置中的元素顺序发生变化。这使得 Terraform 能够更精确地计划更改,并减少不必要的资源删除重建。
注:本文参考自 Terraform 官方文档 for_each,可前往了解更多细节。
- 本页导读 (1)
- 概述
- 基本语法
- 映射示例:
- 字符串集合示例:
- 子模块示例:
- each 对象
- 使用限制
- 在 for_each 中使用表达式
- 资源之间引用 for_each
- 引用实例
- 使用集合
- 使用示例
- 创建多个 RAM 用户和访问密钥
- 使用嵌套数据结构创建多区域资源
- 使用 setproduct 创建多维资源组合