从代码里删掉,差点毁了正在运行的资源
目标
使用不销毁资源、只把它从状态中移除的 removed 块,亲自询问工具这个版本的 removed 接收什么并记录下来,用 moved 只修改名称,用 -replace 和 taint 只重新创建一个,最后把整个模块放手。
为什么重要
基础设施代码运行得久了,总会有一天需要调整代码与实际资源之间的所有权关系。团队分家、迁移到其他工具、把接手的东西还回去。然而,从配置中删除资源块,在工具看来就是“销毁”——并不是放弃所有权的意思。所以需要一条单独表达这层意思的声明,而之所以用声明而不是命令,是为了评审和记录。谁在什么时候把什么从状态中移除了,会留在提交里。这里还有一个陷阱——文档总是描述最新版本,而你使用的版本可能比它低。了解 removed 块接收的参数因版本而异,并养成不看文档、而是向手边的工具询问来确认的习惯,是本模块要学的第二件事。
步骤
- 在
/root/tfa-removed/keep.tf中放入random_pet.keep(length 2)和把它的名称写入out/keep.txt的local_file.keep,在/root/tfa-removed/legacy.tf中放入向out/legacy.txt写入一行legacy service的local_file.legacy,然后执行 init 和 apply。接着只把状态文件复制一次到/root/tfa-removed/snapshots/before.tfstate。 - 删除
/root/tfa-removed/legacy.tf,并在/root/tfa-removed/removed.tf中用removed块声明要把local_file.legacy从状态中移除。在 apply 之前把计划输出保存到/root/tfa-removed/removed-plan.txt,然后 apply。out/legacy.txt必须原样保留。 - 暂时在
/root/tfa-removed/removed.tf的 removed 块中放入lifecycle块(destroy = false),运行tofu plan并把结果保存到/root/tfa-removed/lifecycle-probe.txt,然后恢复原样。 接着在/root/tfa-removed/removed-facts.tsv中写入三行,每行用制表符分隔两列——tofu_version是这个 Pod 的核心版本,removed_lifecycle是supported或unsupported,removed_effect是forget或destroy。 - 把
/root/tfa-removed/keep.tf中的random_pet.keep改为random_pet.app(引用也一并修改),在/root/tfa-removed/moved.tf中用moved块声明从旧地址迁移到新地址,然后 apply。接着在/root/tfa-removed/moved.tsv中写入before(第 1 步副本中记录的random_pet.keep的 id)和after(当前状态中random_pet.app的 id)两行,每行用制表符分隔两列。两个值必须相同。 - 暂时恢复
/root/tfa-removed/legacy.tf(内容与第 2 步相同),运行tofu plan,把输出保存到/root/tfa-removed/conflict.txt,然后再次删除该文件。removed 块保持不动。最后计划必须是干净的。 - 把状态文件复制到
/root/tfa-removed/snapshots/before-replace.tfstate,然后运行tofu apply -replace=random_pet.app -auto-approve。接着在/root/tfa-removed/replace.tsv中写入before(副本中的 id)和after(当前的 id)两行,每行用制表符分隔两列。两个值必须不同。 - 用
tofu taint random_pet.app标记之后,把状态中记录的该实例的状态值用一行保存到/root/tfa-removed/taint-status.txt,并把tofu plan的输出保存到/root/tfa-removed/taint-plan.txt。然后用tofu untaint撤回,使计划处于干净的状态。 - 在
/root/tfa-removed/modules/archive/main.tf中创建一个写入out/archive-1.txt和out/archive-2.txt两个文件的模块,在/root/tfa-removed/archive.tf中用module "archive"调用,然后 apply。接着删除/root/tfa-removed/archive.tf,并在/root/tfa-removed/archive-removed.tf中用removed块把整个module.archive从状态中移除。计划输出保存到/root/tfa-removed/module-plan.txt,两个文件必须保留在磁盘上。
参考
- Pod 中有 OpenTofu 1.9.0 以及 local、random provider 的 mirror,无需联网即可运行。
- 用 state mv 和 state rm 进行的状态手术,在 iac-basics 的状态实验中讲解。本实验是用声明来完成同样的事。
- 常见错误:加入了 removed 块,却没有删除资源块。这样会在计划阶段被阻止(在第 5 步亲眼看到)。
- 常见错误:工作结束后没有清理 removed 块。将来如果那个地址的资源又被需要,到时就会被阻止。
- Removing Resources · moved · Command: taint · Command: untaint · Command: apply
把移交过来的东西也放进同一个状态
在 /root/tfa-removed/keep.tf 中放入 random_pet.keep(length 2)和把它的名称写入 out/keep.txt 的 local_file.keep,在 /root/tfa-removed/legacy.tf 中放入向 out/legacy.txt 写入一行 legacy service 的 local_file.legacy,然后执行 init 和 apply。接着只把状态文件复制一次到 /root/tfa-removed/snapshots/before.tfstate。
要在后面的步骤中判断“这个资源是被重新创建了,还是保持原样”,现在就必须把当前的标识符留在某处。副本只在最初复制一次——以后覆盖的话,用来比较的原件就没有了。
只从状态中移除,保留实际资源
删除 /root/tfa-removed/legacy.tf,并在 /root/tfa-removed/removed.tf 中用 removed 块声明要把 local_file.legacy 从状态中移除。在 apply 之前把计划输出保存到 /root/tfa-removed/removed-plan.txt,然后 apply。out/legacy.txt 必须原样保留。
如果只从配置中删除资源块,工具会把它读成要销毁的意思。removed 块是另外表达“在状态中忘记这个地址”的声明。请确认计划输出中指示这个动作的词不是 destroy。
向工具询问这个版本的 removed 接收什么
暂时在 /root/tfa-removed/removed.tf 的 removed 块中放入 lifecycle 块(destroy = false),运行 tofu plan 并把结果保存到 /root/tfa-removed/lifecycle-probe.txt,然后恢复原样。
接着在 /root/tfa-removed/removed-facts.tsv 中写入三行,每行用制表符分隔两列——tofu_version 是这个 Pod 的核心版本,removed_lifecycle 是 supported 或 unsupported,removed_effect 是 forget 或 destroy。
文档总是描述最新版本。你手边的这个版本接收什么,只能向工具询问才能知道,把答案记录下来,能节省团队的时间。效果在第 2 步的计划输出中已经说明了。
只改名称——不重新创建同一个东西
把 /root/tfa-removed/keep.tf 中的 random_pet.keep 改为 random_pet.app(引用也一并修改),在 /root/tfa-removed/moved.tf 中用 moved 块声明从旧地址迁移到新地址,然后 apply。接着在 /root/tfa-removed/moved.tsv 中写入 before(第 1 步副本中记录的 random_pet.keep 的 id)和 after(当前状态中 random_pet.app 的 id)两行,每行用制表符分隔两列。两个值必须相同。
没有 moved 块的话,工具会把它读成“旧名称消失、新名称出现”,从而销毁后重新创建。moved 是告诉工具同一个对象只是地址变了的声明,所以标识符保持不变。是否真的如此,请与第 1 步的副本比较确认。
说了要遗忘,却又重新声明
暂时恢复 /root/tfa-removed/legacy.tf(内容与第 2 步相同),运行 tofu plan,把输出保存到 /root/tfa-removed/conflict.txt,然后再次删除该文件。removed 块保持不动。最后计划必须是干净的。
如果“遗忘声明”与“创建声明”同时存在于同一个地址,工具就不知道该做什么。这个错误会通过 tofu validate,只在 plan 中出现——请想一想为什么(地址解析是计划阶段的工作,而不是配置验证的工作)。
只重新创建一个
把状态文件复制到 /root/tfa-removed/snapshots/before-replace.tfstate,然后运行 tofu apply -replace=random_pet.app -auto-approve。接着在 /root/tfa-removed/replace.tsv 中写入 before(副本中的 id)和 after(当前的 id)两行,每行用制表符分隔两列。两个值必须不同。
-replace 不修改配置,而是把“只销毁这个并重新创建”放进计划。在 provider 出现异常时,或需要重置配置中看不到的启动状态时使用。也请在计划中看看依赖这个资源的内容是否也被一并重新创建。
手动标记后再撤回
用 tofu taint random_pet.app 标记之后,把状态中记录的该实例的状态值用一行保存到 /root/tfa-removed/taint-status.txt,并把 tofu plan 的输出保存到 /root/tfa-removed/taint-plan.txt。然后用 tofu untaint 撤回,使计划处于干净的状态。
标记不会触碰基础设施,只会写进状态文件——下一次计划会读取它,并加入替换。请用 jq 查看该实例的 status。如今同样的事可以用 -replace 更安全地完成(因为不会预先修改状态),但必须知道这个命令做了什么,这样遇到别人标记过的内容时才不会慌张。
把整个模块放手
在 /root/tfa-removed/modules/archive/main.tf 中创建一个写入 out/archive-1.txt 和 out/archive-2.txt 两个文件的模块,在 /root/tfa-removed/archive.tf 中用 module "archive" 调用,然后 apply。接着删除 /root/tfa-removed/archive.tf,并在 /root/tfa-removed/archive-removed.tf 中用 removed 块把整个 module.archive 从状态中移除。计划输出保存到 /root/tfa-removed/module-plan.txt,两个文件必须保留在磁盘上。
removed 的 from 中不仅可以放资源地址,也可以放模块地址,这样该模块内的所有内容会一次性从状态中移除。划分组织时,把一个团队的份额整个移交出去的工作就是这个样子。在模块内设置文件路径时,使用以根模块为基准的路径更不容易混淆。