TT Lab
开始
学习 学习路径 课程

Terraform 实战

三个服务乘三个环境,手写展开成了二十个块

在 TT Lab 中继续学习

目标

接收嵌套对象变量并转换为映射,用 flatten 和 setproduct 两种方式展开双重循环并得到相同的结果,用 dynamic 块创建可选块并通过错误确认它的局限,最后把展开的矩阵渲染成文件和报告。

为什么重要

基础设施代码变长的真正原因,不是资源多,而是同样的形状在重复。如果手工展开这些重复,需要修改的地方就有二十处,评审的人还得用眼睛核对这二十个块是否真的相同。HCL 用两条路来解决这个问题——构造值的一方和构造块的一方。值的一方(for 表达式、flatten、setproduct、对象类型)可以随意折叠和展开,块的一方(dynamic)则只能在 provider schema 允许的范围内创建。不了解这个差别,就会想用 dynamic 来写所有东西,最后得到没法阅读的配置。在实际工作中,dynamic 最有价值的用法其实不是循环,而是“要不要放入这个块”这样的选择,循环通常由 for_each 和值表达式来承担。

步骤

  1. 在 /root/tfa-dyn/variables.tf 中声明类型为 list(object({ name = string, port = number, envs = list(string) })) 的 services 变量,并添加检查名称是否重复的 validation。在 /root/tfa-dyn/terraform.tfvars 中写入 api(8080,dev、prod)、web(8081,dev)、batch(9090,dev、stage、prod)三项。在 /root/tfa-dyn/main.tf 中放入 provider 声明,以及把 jsonencode(var.services) 写入 out/services.json 的 local_file.services,然后执行 init 和 apply。
  2. 在 /root/tfa-dyn/svc.tf 中创建 local.by_name(以名称为键的映射),并用它运行 for_each,声明为每个服务写入 /root/tfa-dyn/out/svc/<이름>.conf(占位符为服务名称)的 local_file.svc。文件内容是 name=<이름> 和 port=<번호> 两行(占位符依次为服务名称与端口号)。apply 之后,查看状态中的实例键是什么。
  3. 在 /root/tfa-dyn/flat.tf 中,用 flatten 构造展开了“服务 × 该服务的所有环境”的 local.pairs(每一项是带有 env、name、port 的对象),再把以 "<env>/<name>" 为键的 local.by_key 映射(占位符依次为环境与服务名称)用 jsonencode 写入 /root/tfa-dyn/out/flat.json。必须得到六项。
  4. 在 /root/tfa-dyn/product.tf 中创建 local.all_envs(所有环境的去重列表),在 setproduct(local.all_envs, var.services) 的所有组合中,只保留该服务实际使用的环境,构造 local.combo_by_key,并写入 /root/tfa-dyn/out/product.json。它必须与第 3 步的结果完全相同。
  5. 在 /root/tfa-dyn/cert.tf 中放入 var.with_subject(bool,默认 true)、ED25519 私钥,以及用 dynamic "subject" 开关主体块的 tls_self_signed_cert.c。主体为 common_name = "api.internal"、organization = "LabHub"。证书写入 /root/tfa-dyn/out/cert.pem。先用 -var with_subject=false 应用,把 openssl x509 -noout -subject 的结果保存到 /root/tfa-dyn/out/no-subject.txt,然后用默认值重新应用,把同一命令的结果保存到 /root/tfa-dyn/out/subject.txt。
  6. 暂时修改 /root/tfa-dyn/cert.tf 中的 dynamic "subject",让它创建两个块,然后 apply,并把失败的输出保存到 /root/tfa-dyn/out/limit-error.txt。接着恢复原样重新应用,使计划处于干净的状态。
  7. 在 /root/tfa-dyn/rendered.tf 中,用第 3 步的 local.by_key 运行 for_each,创建六个 /root/tfa-dyn/rendered/<env>-<name>.conf(占位符依次为环境与服务名称)。内容是 env=、name=、port= 三行。
  8. 在 /root/tfa-dyn/summary.tf 中创建 /root/tfa-dyn/out/summary.json,其中包含各环境的项目数(by_env)、按名称排序的列表(services)、服务端口之和(total_ports)三项。这三个值都用 HCL 表达式计算。

参考

用变量类型接收嵌套对象

在 /root/tfa-dyn/variables.tf 中声明类型为 list(object({ name = string, port = number, envs = list(string) })) 的 services 变量,并添加检查名称是否重复的 validation。在 /root/tfa-dyn/terraform.tfvars 中写入 api(8080,dev、prod)、web(8081,dev)、batch(9090,dev、stage、prod)三项。在 /root/tfa-dyn/main.tf 中放入 provider 声明,以及把 jsonencode(var.services) 写入 out/services.json 的 local_file.services,然后执行 init 和 apply。

把类型写成 object,在 tfvars 中拼写有误时,会在 apply 之前被拦下。validation 的 condition 为真时通过——只需检查名称个数与去重后的个数是否相同。jsonencode 会把 HCL 值原样转换为 JSON。

把列表转换为映射,传给 for_each

在 /root/tfa-dyn/svc.tf 中创建 local.by_name(以名称为键的映射),并用它运行 for_each,声明为每个服务写入 /root/tfa-dyn/out/svc/<이름>.conf(占位符为服务名称)的 local_file.svc。文件内容是 name=<이름> 和 port=<번호> 两行(占位符依次为服务名称与端口号)。apply 之后,查看状态中的实例键是什么。

for_each 不接受列表——必须是集合或映射。对列表使用 count 时,删除中间一项会使后面的全部错位并被重新创建,而用映射的键,只会删除那一项。请确认状态地址是以方括号内的键来表示的。

用 flatten 把双重循环展开为一层

在 /root/tfa-dyn/flat.tf 中,用 flatten 构造展开了“服务 × 该服务的所有环境”的 local.pairs(每一项是带有 env、name、port 的对象),再把以 "<env>/<name>" 为键的 local.by_key 映射(占位符依次为环境与服务名称)用 jsonencode 写入 /root/tfa-dyn/out/flat.json。必须得到六项。

由于内层 for 返回的是列表,所以外层 for 的结果是列表的列表。flatten 会剥掉这一层。转换成映射后,下一步就能直接传给 for_each,键本身就是人能读懂的名称。

用 setproduct 重新构造同一个矩阵

在 /root/tfa-dyn/product.tf 中创建 local.all_envs(所有环境的去重列表),在 setproduct(local.all_envs, var.services) 的所有组合中,只保留该服务实际使用的环境,构造 local.combo_by_key,并写入 /root/tfa-dyn/out/product.json。它必须与第 3 步的结果完全相同。

setproduct 不会替你过滤——它先构造所有组合,再用 for 表达式的 if 挑出要保留的。“先构造所有组合再丢弃”的方式,与“一开始就只展开需要的”方式,哪个更容易阅读,请把两个文件并排放在一起判断。

用 dynamic 让块有或没有

在 /root/tfa-dyn/cert.tf 中放入 var.with_subject(bool,默认 true)、ED25519 私钥,以及用 dynamic "subject" 开关主体块的 tls_self_signed_cert.c。主体为 common_name = "api.internal"、organization = "LabHub"。证书写入 /root/tfa-dyn/out/cert.pem。先用 -var with_subject=false 应用,把 openssl x509 -noout -subject 的结果保存到 /root/tfa-dyn/out/no-subject.txt,然后用默认值重新应用,把同一命令的结果保存到 /root/tfa-dyn/out/subject.txt。

给 dynamic 的 for_each 传入空集合时,这个块根本不会被创建。这是实际工作中使用 dynamic 最常见的原因——不是循环,而是可选块。证书的主体是否真的为空,请打开生成的证书确认,而不是看配置。

dynamic 只能创建 schema 允许的数量

暂时修改 /root/tfa-dyn/cert.tf 中的 dynamic "subject",让它创建两个块,然后 apply,并把失败的输出保存到 /root/tfa-dyn/out/limit-error.txt。接着恢复原样重新应用,使计划处于干净的状态。

dynamic 不是能创建任意数量块的机制,而是“用配置值创建块”的机制。最多允许几个,由 provider 的 schema 决定。请看错误消息是否同时说明了允许范围和实际数量。

把展开的矩阵渲染成文件

在 /root/tfa-dyn/rendered.tf 中,用第 3 步的 local.by_key 运行 for_each,创建六个 /root/tfa-dyn/rendered/<env>-<name>.conf(占位符依次为环境与服务名称)。内容是 env=、name=、port= 三行。

键中已经包含环境和名称,所以每个文件名从 each.value 中取出来拼装即可。之后如果某个服务多用了一个环境,请确认只改 tfvars 的一行,就会多生成一个文件——这正是使用这种结构的原因。

把展开的值重新折叠成报告

在 /root/tfa-dyn/summary.tf 中创建 /root/tfa-dyn/out/summary.json,其中包含各环境的项目数(by_env)、按名称排序的列表(services)、服务端口之和(total_ports)三项。这三个值都用 HCL 表达式计算。

by_env 只需对每个环境过滤 pairs 并统计长度,端口之和用 sum 函数求出。这里之所以要排序,不是为了便于人看,而是因为不排序的话,同样的输入每次运行会以不同的顺序出现,导致无法比较。