从名单删掉的人,文件只在脚本那边留了下来
目标
把同一份名单分别交给命令式脚本和 OpenTofu,比较名单缩减时两种方式各做了什么,并亲手确认所有权范围、地址体系(count 与 for_each)以及 moved 迁移。
为什么重要
命令式脚本只知道“现在要做什么”,所以要清理名单中被去掉的项目,就得另外记住过去创建过什么。声明式工具把这份记忆保存在状态文件里,因此会把代码中消失的东西计算为删除。但这并不意味着它无所不知——工具只认识自己创建的东西,同一个目录里别人手工放进去的文件不会出现在计划中。而且,如果用位置编号作为地址,列表前面的一行变动就会导致后面所有资源被替换,酿成事故;所以应该用键作为地址,迁移时用 moved 告知状态。
步骤
- 在
/root/iac-conv/users.txt中每行写一个:alice、bob、carol。/root/iac-conv/imperative.sh <명단파일> <출력디렉터리>(占位符依次为名单文件、输出目录)会为名单中的每个名称向<출력>/<이름>.txt(占位符依次为输出目录、名称)写入user <이름>(占位符为名称),并向<출력>/registry.log(占位符为输出目录)追加一行名称。用/root/iac-conv/users.txt和/root/iac-conv/imp运行这个脚本两次。 - 在
/root/iac-conv/main.tf中读取同一个users.txt(排除空行),用 for_each 为每个名称声明local_file.user["<이름>"](占位符为名称)。文件为${path.module}/decl/<이름>.txt(占位符为名称),内容为user <이름>(占位符为名称)一行。执行 init 和 apply。评分器会检查状态的键是否与 users.txt 中的名称完全一致,以及计划是否干净。 - 从
users.txt中删除carol。用相同的参数把命令式脚本再运行一次;声明一侧用tofu plan -out=/root/iac-conv/remove.tfplan保存计划,然后应用该计划。评分器会检查已保存的计划是否只包含 carol 这一项删除,以及decl/carol.txt是否消失而imp/carol.txt是否保留。 - 有人手工创建了
/root/iac-conv/decl/mallory.txt(自己创建,内容随意)。确认tofu plan -detailed-exitcode仍然是 0。然后创建/root/iac-conv/orphans.sh <관리디렉터리> <작업디렉터리>(占位符依次为管理目录、工作目录),让它按每行一个输出存在于管理目录中但状态并不认识的文件名,并把针对decl的结果保存到/root/iac-conv/orphans.txt。 - 在
/root/iac-conv/count-demo/main.tf中放一个names变量(默认值为["a", "b", "c"])和用 count 创建的local_file.n。文件为out/<인덱스>.txt(占位符为索引),内容是该名称一行。应用之后,把默认值中的"a"删掉,用tofu plan -out=/root/iac-conv/count-demo/shift.tfplan只保存而不应用。在/root/iac-conv/count-demo/shift.txt中为每个发生变化的资源写入<주소> <update|replace|delete|create>(占位符为资源地址),最后把默认值改回["a", "b", "c"]。 - 在
/root/iac-conv/foreach-demo/main.tf中放同样的names变量,以及用for_each = toset(var.names)创建的local_file.n。文件为out/<이름>.txt(占位符为名称),内容是名称一行。应用之后把默认值中的"a"删掉,用tofu plan -out=/root/iac-conv/foreach-demo/drop.tfplan只保存而不应用。按与第 5 步相同的格式写入/root/iac-conv/foreach-demo/drop.txt,并把默认值再改回["a", "b", "c"]。 - 把
/root/iac-conv/count-demo中的local_file.n改成以名称为键的 for_each,文件名(out/0.txt·out/1.txt·out/2.txt)和内容保持不变,并用三个moved块告知n[0]→n["a"]、n[1]→n["b"]、n[2]→n["c"]。确认计划中没有任何创建·删除·替换之后再应用。评分器会检查状态地址是否已改为名称键,以及计划是否干净。 - 创建
/root/iac-conv/converge.sh <작업디렉터리> <관리디렉터리>(占位符依次为工作目录、管理目录)。apply 之后,如果第二次plan -detailed-exitcode不是 0,就输出以not-converged开头的一行并以 2 结束;如果 orphans.sh 找到了陌生文件,就输出以stray开头的一行(包含文件名)并以 3 结束;两者都没问题则输出converged并以 0 结束;apply·plan 失败则输出以error开头的一行并以 1 结束。评分器会用/root/iac-conv的副本(其中有 mallory.txt,所以应为 3)和它自己创建的临时目录来检查。不必在原目录中运行。
参考
- Pod 中有 OpenTofu 1.9.0 和 local provider 镜像源,没有互联网也能完成 init。
- 从计划中读取操作:
tofu show -json <계획파일> | jq '.resource_changes[] | {address, actions: .change.actions}'(占位符为计划文件) - 常见错误:用 moved 迁移时,连文件名的表达式也改了,导致迁移之后又多出一次替换。
- 常见错误:在只要求生成计划的步骤中连 apply 也做了,使下一步的起点发生变化。
- for_each · Refactoring(moved) · Resource Behavior · tofu show
把命令式脚本运行两次
在 /root/iac-conv/users.txt 中每行写一个:alice、bob、carol。/root/iac-conv/imperative.sh <명단파일> <출력디렉터리>(占位符依次为名单文件、输出目录)会为名单中的每个名称向 <출력>/<이름>.txt(占位符依次为输出目录、名称)写入 user <이름>(占位符为名称),并向 <출력>/registry.log(占位符为输出目录)追加一行名称。用 /root/iac-conv/users.txt 和 /root/iac-conv/imp 运行这个脚本两次。
> 是覆盖,>> 是追加。文件内容运行两次也相同,但 registry.log 会按运行次数增长。这一差别就是不具备幂等性的命令式脚本的样子。
用声明的方式写出同一份名单
在 /root/iac-conv/main.tf 中读取同一个 users.txt(排除空行),用 for_each 为每个名称声明 local_file.user["<이름>"](占位符为名称)。文件为 ${path.module}/decl/<이름>.txt(占位符为名称),内容为 user <이름>(占位符为名称)一行。执行 init 和 apply。评分器会检查状态的键是否与 users.txt 中的名称完全一致,以及计划是否干净。
用 file() 读取,再用 split·compact·toset 构造集合。for_each 只接受集合或映射。用 tofu state list 确认资源地址方括号中的不是索引数字,而是名称。
从名单中删掉一行——只有一边被清理
从 users.txt 中删除 carol。用相同的参数把命令式脚本再运行一次;声明一侧用 tofu plan -out=/root/iac-conv/remove.tfplan 保存计划,然后应用该计划。评分器会检查已保存的计划是否只包含 carol 这一项删除,以及 decl/carol.txt 是否消失而 imp/carol.txt 是否保留。
声明式工具会计算状态中记录的内容与代码之间的差异,所以代码中消失的键会表现为删除。命令式脚本只看现在的名单,不知道过去创建过什么。
所有权范围之外的文件,声明式工具也不认识
有人手工创建了 /root/iac-conv/decl/mallory.txt(自己创建,内容随意)。确认 tofu plan -detailed-exitcode 仍然是 0。然后创建 /root/iac-conv/orphans.sh <관리디렉터리> <작업디렉터리>(占位符依次为管理目录、工作目录),让它按每行一个输出存在于管理目录中但状态并不认识的文件名,并把针对 decl 的结果保存到 /root/iac-conv/orphans.txt。
tofu show -json 的 values 中有状态所知道的资源和属性。比较 local_file 的 filename 与目录中的文件列表。声明式工具能使其收敛的范围,只有所声明的资源。
删掉 count 列表最前面的一项,后面全都变了
在 /root/iac-conv/count-demo/main.tf 中放一个 names 变量(默认值为 ["a", "b", "c"])和用 count 创建的 local_file.n。文件为 out/<인덱스>.txt(占位符为索引),内容是该名称一行。应用之后,把默认值中的 "a" 删掉,用 tofu plan -out=/root/iac-conv/count-demo/shift.tfplan 只保存而不应用。在 /root/iac-conv/count-demo/shift.txt 中为每个发生变化的资源写入 <주소> <update|replace|delete|create>(占位符为资源地址),最后把默认值改回 ["a", "b", "c"]。
count 的地址是位置编号。去掉最前面的一项后,后面的元素会各向前移一位,同一个编号里会放入不同的值。local_file 一旦内容改变就是替换。
在 for_each 中,只有被去掉的才会被去掉
在 /root/iac-conv/foreach-demo/main.tf 中放同样的 names 变量,以及用 for_each = toset(var.names) 创建的 local_file.n。文件为 out/<이름>.txt(占位符为名称),内容是名称一行。应用之后把默认值中的 "a" 删掉,用 tofu plan -out=/root/iac-conv/foreach-demo/drop.tfplan 只保存而不应用。按与第 5 步相同的格式写入 /root/iac-conv/foreach-demo/drop.txt,并把默认值再改回 ["a", "b", "c"]。
for_each 的地址是键。键 "b" 和 "c" 与列表中的位置无关,保持原样,所以不会出现在计划中。
把 count 迁移为 for_each,同时不删除任何东西
把 /root/iac-conv/count-demo 中的 local_file.n 改成以名称为键的 for_each,文件名(out/0.txt·out/1.txt·out/2.txt)和内容保持不变,并用三个 moved 块告知 n[0]→n["a"]、n[1]→n["b"]、n[2]→n["c"]。确认计划中没有任何创建·删除·替换之后再应用。评分器会检查状态地址是否已改为名称键,以及计划是否干净。
moved 块是只修改状态中地址的约定。新地址的配置必须与旧对象连属性也相同,才会只发生迁移——如果文件名变了,迁移之后就会接着发生替换。要从名称得到位置编号,用 for 表达式来构造映射。
应用之后,连同第二次计划和陌生文件一起确认的收敛检查
创建 /root/iac-conv/converge.sh <작업디렉터리> <관리디렉터리>(占位符依次为工作目录、管理目录)。apply 之后,如果第二次 plan -detailed-exitcode 不是 0,就输出以 not-converged 开头的一行并以 2 结束;如果 orphans.sh 找到了陌生文件,就输出以 stray 开头的一行(包含文件名)并以 3 结束;两者都没问题则输出 converged 并以 0 结束;apply·plan 失败则输出以 error 开头的一行并以 1 结束。评分器会用 /root/iac-conv 的副本(其中有 mallory.txt,所以应为 3)和它自己创建的临时目录来检查。不必在原目录中运行。
幂等性(第二次计划为空)和收敛(我的领域里没有我不知道的东西)是不同的问题。直接调用前面步骤中的 orphans.sh。第二次计划不为空的配置,通常来自 timestamp() 这类每次都会变化的值。