用值折叠的重复,用块展开的重复
一句话总结
HCL 中的循环分为两类:构造值的一方(for、flatten、setproduct)和构造块的一方(dynamic)。前者可以随意折叠和展开,后者只能创建 provider schema 所允许的数量。
为什么需要它——把循环手工展开会发生什么
假设要把三个服务部署到三个环境。手工展开就是九个块。起初读起来很容易。问题出现在半年后,当“修改所有环境的健康检查路径”的需求到来时。要改九个地方,评审者还要用眼睛核对这九处是否真的改得完全一样。漏掉一处,计划也照样通过。
所以要把循环折叠成值。输入一份,规则一条,结果九个。这样需要修改的地方就只有一处,评审只需要看“规则对不对”这一件事。代价是需要训练阅读值表达式的能力——哪里值得承担这个取舍,正是本模块的主题。
工作原理
1. 先确定类型。嵌套对象的类型起着文档的作用。
variable "services" {
type = list(object({
name = string
port = number
envs = list(string)
}))
}
写明类型之后,tfvars 中如果拼写有误,在 apply 之前就会被拦下。在此基础上再加 validation,像“名称不能重复”这样的规则,也可以在值的阶段拦截。名称重复时,后面构造的 for_each 键会悄悄合并成一个,而这种事故在计划中只显示为“删除 1 项”,很难找到原因。
2. 把列表转换为映射再传给 for_each。for_each 只接受集合或映射。与 count 不同,实例地址是键,所以即使中间的一项被删掉,后面的也不会错位。
3. 把双重循环展开成一个。有两种方法。
# (가) 안에서 밖으로 — 필요한 것만 만든다
pairs = flatten([
for s in var.services : [
for e in s.envs : { env = e, name = s.name, port = s.port }
]
])
# (나) 다 만들고 거른다
combos = [
for pair in setproduct(local.all_envs, var.services) : {
env = pair[0], name = pair[1].name, port = pair[1].port
} if contains(pair[1].envs, pair[0])
]
两者可以得到相同的结果,但性质不同。flatten 一侧只创建输入中存在的内容,setproduct 一侧则先创建所有组合再丢弃一部分。组合数量大时,后者会做很多无用功。反过来,如果目的是“找出缺失的组合”,先构造所有组合的做法就更自然。
4. dynamic 构造块。它是从配置值构造出嵌套块而不是值的机制。
dynamic "subject" {
for_each = var.with_subject ? [1] : []
content {
common_name = "api.internal"
}
}
这里重要的是:能创建多少个,取决于 provider schema。如果给 schema 最多只允许一个的块创建了两个,计划能制定出来,但在值验证时会因为“最多允许几个,却来了几个”而被拦下。在实验的第 6 步,你会亲自遇到这个错误。
在现场相遇的样子
dynamic 在实际工作中最常用的地方,其实不是循环,而是可选块。利用“传入空集合时根本不会创建块”的性质,来表达“日志配置只在 prod 中启用”“只有打开了这个选项时才有加密块”。这比过去为了模拟条件块而把资源整个写成两份的做法好得多。
反过来,不该使用的地方也很明确。官方文档也写明,dynamic 会降低可读性,不要滥用。如果三个块彼此只有一点点不同,用动态方式构造会把“哪里不同”藏进数据里,在评审中看不出来。这种情况下,直接写三个块更好。标准很简单——数量随输入而变,就用动态方式;数量固定、只有值不同,就手写。
最后,把渲染结果导出为文件的习惯很有帮助。用 jsonencode 把展开的矩阵写入文件,人就能亲眼确认,也可以在 CI 中做 diff。仅凭计划输出,很难追查“这个组合为什么缺了”。
下一项实验要做什么
在 /root/tfa-dyn 中接收嵌套对象变量并转换成映射,用 flatten 和 setproduct 两种方式构造同一个矩阵,并通过文件核对结果是否相同。然后用 dynamic 打开和关闭证书主体块,打开实际的证书进行确认;接着收到因为想创建两个而被 schema 拦截的错误;最后把展开的矩阵渲染为六个文件和一份摘要报告。