从 count 到 for_each,不销毁地迁过去
目标
通过状态地址确认 count 与 for_each 的区别,并掌握如何在不销毁的前提下,把已经应用的资源迁移为 for_each。
为什么重要
count 和 for_each 看起来像是语法选择,实际上是在决定以什么作为每个实例的标识符。count 用顺序作标识符,for_each 用名称作标识符。从列表中删掉中间的一项,差异就会显现——count 会让后面的实例依次前移一位,连本应保留的资源也被重新创建,而 for_each 只会让对应的键消失。在真实的云环境中,这个差异会表现为“只删了一行配置,两台服务器却被重新创建了”。需要修改已应用资源的地址时,请使用 moved 块。moved 只迁移状态,不动实际资源,而且重构记录会留在代码中,其他人只要运行 plan 就会自动跟进。判断迁移是否正确只有一个标准——计划中不能有任何一项销毁。
步骤
- 在
/root/tf/foreach/main.tf中用count = 3声明local_file资源node,执行tofu init后应用。然后在迁移地址之前,把tofu state list的结果保存为/root/tf/foreach/out/count-state.txt。这个文件中必须有local_file.node[0]、local_file.node[1]、local_file.node[2]三行(不能出现字符串键的形式),它是第 7 步迁移前的快照。 - 把
node的文件名设为/root/tf/foreach/out/node-${count.index}.txt,内容设为node-${count.index}。不要手写三遍名称,必须用循环索引生成。out下的node-*.txt必须恰好有 3 个,每个文件的第一行依次为node-0、node-1、node-2。 - 用
for_each = toset(["kr", "us", "eu"])创建local_file资源zone,使其生成/root/tf/foreach/out/zone-<키>.txt(占位符为键)共三个文件。状态中的键必须是eu、kr、us。 - 遍历映射来创建
local_file资源service。键为api、web、worker,每个值中包含端口(web的端口必须是8080)。/root/tf/foreach/out/service-web.txt中必须分别用一行写入name=web和port=8080。 - 用
for表达式定义输出service_ports,以服务名称为键、端口为值,并把tofu output -json的结果保存为/root/tf/foreach/out/outputs.json。条目必须为 3 个,web必须是8080。 - 创建
variable "enable_debug"(bool,默认值 false),并在local_file资源debug中使用count = var.enable_debug ? 1 : 0。文件路径为/root/tf/foreach/out/debug.txt,内容任意(开关最多只有一个实例,不会出现索引错位,这就是仍然可以使用count的原因)。把打开开关的计划(tofu plan -var enable_debug=true)的输出保存为/root/tf/foreach/out/toggle.txt(其中必须能看到1 to add),但不要应用。最终状态中debug实例必须为 0 个,/root/tf/foreach/out/debug.txt也不能存在。 - 把
node改为for_each = { n0 = 0, n1 = 1, n2 = 2 }。映射的键用于新地址,值是旧索引,因此文件名和内容必须用each.value生成,而不是each.key,使node-0.txt、node-1.txt、node-2.txt及其内容与第 1–2 步完全一致(改了名称就不是迁移而是重新创建,计划中会出现销毁)。然后用 3 个moved块把旧地址与新地址连接起来,例如把local_file.node[0]迁移到local_file.node["n0"]。应用之后,把tofu plan的输出保存为/root/tf/foreach/out/after-plan.txt,其内容必须是No changes,且不能有任何一项销毁。 - 创建
/root/tf/foreach/out/addresses.json。在addresses键中以数组形式放入tofu state list的地址(node 3 个 + zone 3 个 + service 3 个 = 至少 9 个),带数字索引的地址一个都不能有。在numeric_index_count键中写入带数字索引的地址个数,迁移完成的话是0。
参考
- 本实验的状态文件是
/root/tf/foreach/terraform.tfstate,用 count 创建的产物是/root/tf/foreach/out/node-0.txt、/root/tf/foreach/out/node-1.txt、/root/tf/foreach/out/node-2.txt。迁移之后,这些文件名也必须保持不变。 tofu state list每行输出一个地址。用jq -R -s整体读入并折叠成数组,第 8 步会更方便。- 第 1 步的
count-state.txt与第 8 步的addresses.json是同一个命令在迁移前后的快照。在真实的迁移工作中,先留下迁移前的地址列表和状态副本,也是第一步。没有记录,就无法证明“没有遗漏资源”。 moved块在应用一次之后,按惯例也要保留在代码中。因为其他人的状态中也需要发生同样的迁移。- 第 6 步的
toggle.txt要等其他变更全部应用之后再导出,计划中才会只出现一个开关的变化。 - 常见错误 1:在第 7 步把文件名改成
node-${each.key}.txt。名称一变,就不是迁移而是重新创建,计划中会出现销毁。 - 常见错误 2:把列表直接传给
for_each。必须用toset()转换为集合。 - 常见错误 3:完成迁移之后才导出
count-state.txt。这个文件必须是迁移之前的记录才有意义。
用 count 创建三个,并留下迁移前的地址
在 /root/tf/foreach/main.tf 中用 count = 3 声明 local_file 资源 node,执行 tofu init 后应用。然后在迁移地址之前,把 tofu state list 的结果保存为 /root/tf/foreach/out/count-state.txt。这个文件中必须有 local_file.node[0]、local_file.node[1]、local_file.node[2] 三行(不能出现字符串键的形式),它是第 7 步迁移前的快照。
在资源块中写入 count = 3,就会产生三个实例。稍后要迁移地址,所以请现在就把地址列表保存为文件——没有迁移前的记录,就无法证明什么被迁移成了什么。
用 count.index 区分实例
把 node 的文件名设为 /root/tf/foreach/out/node-${count.index}.txt,内容设为 node-${count.index}。不要手写三遍名称,必须用循环索引生成。out 下的 node-*.txt 必须恰好有 3 个,每个文件的第一行依次为 node-0、node-1、node-2。
每个实例的文件名必须不同,所以把索引嵌入名称。内容也用同样的方式区分。
使用遍历集合的 for_each
用 for_each = toset(["kr", "us", "eu"]) 创建 local_file 资源 zone,使其生成 /root/tf/foreach/out/zone-<키>.txt(占位符为键)共三个文件。状态中的键必须是 eu、kr、us。
列表不能直接传给 for_each。必须转换为集合,此时键就是元素值本身。
遍历映射的 for_each 与 each.value
遍历映射来创建 local_file 资源 service。键为 api、web、worker,每个值中包含端口(web 的端口必须是 8080)。/root/tf/foreach/out/service-web.txt 中必须分别用一行写入 name=web 和 port=8080。
传入映射时,each.key 是映射的键,each.value 是对应的值。值是对象时,可以继续取它的字段。
用 for 表达式构造输出映射
用 for 表达式定义输出 service_ports,以服务名称为键、端口为值,并把 tofu output -json 的结果保存为 /root/tf/foreach/out/outputs.json。条目必须为 3 个,web 必须是 8080。
for_each 是用来增加资源的元参数,for 是把值转换为另一种形状的表达式。请使用 { for k, v in ... : k => ... } 的形式。
用条件 count 做功能开关
创建 variable "enable_debug"(bool,默认值 false),并在 local_file 资源 debug 中使用 count = var.enable_debug ? 1 : 0。文件路径为 /root/tf/foreach/out/debug.txt,内容任意(开关最多只有一个实例,不会出现索引错位,这就是仍然可以使用 count 的原因)。把打开开关的计划(tofu plan -var enable_debug=true)的输出保存为 /root/tf/foreach/out/toggle.txt(其中必须能看到 1 to add),但不要应用。最终状态中 debug 实例必须为 0 个,/root/tf/foreach/out/debug.txt 也不能存在。
있으면 1, 없으면 0(韩文,意为“有则为 1,无则为 0”)是开关的惯用写法。开启后的状态只通过计划来确认,不要真正应用。
用 moved 块把 count 迁移为 for_each
把 node 改为 for_each = { n0 = 0, n1 = 1, n2 = 2 }。映射的键用于新地址,值是旧索引,因此文件名和内容必须用 each.value 生成,而不是 each.key,使 node-0.txt、node-1.txt、node-2.txt 及其内容与第 1–2 步完全一致(改了名称就不是迁移而是重新创建,计划中会出现销毁)。然后用 3 个 moved 块把旧地址与新地址连接起来,例如把 local_file.node[0] 迁移到 local_file.node["n0"]。应用之后,把 tofu plan 的输出保存为 /root/tf/foreach/out/after-plan.txt,其内容必须是 No changes,且不能有任何一项销毁。
地址一变,工具就会把它读成销毁加创建。用 moved 连接旧地址和新地址,并保持文件名和内容不变,才不会重新创建。
制作完整的地址列表和迁移完成报告
创建 /root/tf/foreach/out/addresses.json。在 addresses 键中以数组形式放入 tofu state list 的地址(node 3 个 + zone 3 个 + service 3 个 = 至少 9 个),带数字索引的地址一个都不能有。在 numeric_index_count 键中写入带数字索引的地址个数,迁移完成的话是 0。
把状态列表转换成 JSON 数组放进去。请自己数一数,确认没有任何数字索引地址残留。