用生命周期管住,并把漂移撤回来
目标
用 lifecycle 的四个参数控制替换、删除和忽略,检测在代码之外发生的变更并朝两个方向分别应对,然后把这个判定用脚本自动化。
为什么重要
基础设施代码出现偏差的方式总是相同——遇到紧急情况,有人直接在控制台上修改,而这个变更没有回到代码中。几天之后,一次因完全不相关的原因执行的 apply,就会悄悄撤销凌晨的应急处理。因此,应对漂移真正困难的部分不是命令,而是决定方向的判断。如果应急处理是错的,就以代码为准恢复;如果判断是正确的,就应该把代码改成与实际资源一致。没有这个判断就运行自动修复,是最危险的。lifecycle 块就是提前介入这个流程的机制。尤其是 ignore_changes,它把像自动伸缩器这样有意在代码之外变化的值排除在检测对象之外,以减少误报;而 prevent_destroy 则给不能被删除的资源装上刹车。最后,判定要靠 -detailed-exitcode 而不是人眼,这样才能接入 cron 和 CI。
步骤
- 在
/root/tf/lifecycle/main.tf中声明local_file资源endpoint。文件路径为/root/tf/lifecycle/out/endpoint.txt,内容为不带换行的一行字符串。在该资源的lifecycle块中写入create_before_destroy = true,执行tofu init后应用。 - 添加
local_file资源statefile_guard(文件为/root/tf/lifecycle/out/guard.txt),在lifecycle中写入prevent_destroy = true并应用。然后只针对该资源尝试删除(tofu destroy -target=local_file.statefile_guard),并把输出连同标准错误一起保存为/root/tf/lifecycle/out/prevent.txt。其中必须留有拒绝消息,状态中的statefile_guard和guard.txt必须原样保留。 - 创建一个
local_file资源,文件为/root/tf/lifecycle/out/managed-note.txt,内容设为ignored-value,然后应用。接着在lifecycle中写入ignore_changes = [content],只把代码中的内容值改成另一个字符串,并把tofu plan的输出保存为/root/tf/lifecycle/out/ignore-plan.txt。计划必须是No changes,实际文件内容仍必须是ignored-value。 - 创建一个
null_resource资源,让它通过triggers持有一个版本值,并给local_file资源checksum(文件为/root/tf/lifecycle/out/checksum.txt)设置replace_triggered_by = [null_resource.<이름>](占位符为资源名称)。保持 checksum 自身的参数不变,只修改版本值,用tofu plan -out=<계획파일>(占位符为计划文件)保存计划,然后把tofu show -json <계획파일>的结果保存为/root/tf/lifecycle/out/trigger-plan.json。在 JSON 的resource_changes中,local_file.checksum的动作必须有两个(删除和创建)。确认之后再应用。 - 不经过代码,直接修改
/root/tf/lifecycle/out/endpoint.txt,使内容中包含hand-edited。然后执行tofu plan -refresh-only -detailed-exitcode -out=<계획파일>(占位符为计划文件),只把退出码写入/root/tf/lifecycle/out/drift-exit.txt(必须是2),并把tofu show -json <계획파일>的结果保存为/root/tf/lifecycle/out/drift.json。JSON 的resource_drift中必须包含 endpoint。不要应用这个计划。 - 用
tofu apply以代码为准恢复实际资源,并把输出保存为/root/tf/lifecycle/out/remediate.txt。其中必须留有Apply complete字样,endpoint.txt中的hand-edited必须消失,文件内容必须与状态中记录的content值完全相同。 - 先用内容
256应用local_file资源quota(文件为/root/tf/lifecycle/out/quota.txt)。这次手动把文件改为512,不要恢复,而是把main.tf中的值改成512,吸收进代码并应用。然后把tofu plan的输出保存为/root/tf/lifecycle/out/accept-plan.txt,其内容必须是No changes,且quota.txt与main.tf中都必须包含512。 - 创建
/root/tf/lifecycle/drift-report.sh并赋予执行权限。这个脚本要用带-detailed-exitcode的计划命令判定漂移,并把结果以exit_code、drift、checked_at、addresses四个字段保存到/root/tf/lifecycle/out/drift-report.json。完成实验时不应存在漂移,所以exit_code必须是0,drift必须是false。
参考
- 本实验的状态文件是
/root/tf/lifecycle/terraform.tfstate。漂移判断是通过比较这个状态与实际资源得出的。 -detailed-exitcode在无变更时为 0,出错时为 1,有变更时为 2。只有当该值为 2 时,drift字段才应为true。tofu show -json <계획파일>(占位符为计划文件)会以机器可读的形式输出计划。resource_changes是将要做的事,resource_drift是已经在代码之外发生的事。local_file的内容请写成不带换行的一行。第 6 步会逐字符比较文件内容与状态中记录的值。- 常见错误 1:在第 8 步的脚本中加入
set -e。退出码为 2 时脚本会直接终止,无法生成报告。 - 常见错误 2:在第 3 步先加入
ignore_changes再应用。要在需要忽略的值进入状态之后再修改代码,才能看到效果。 - 常见错误 3:在第 5 步修改了代码,却称之为漂移。漂移只指没有经过代码的变更。
设置 create_before_destroy
在 /root/tf/lifecycle/main.tf 中声明 local_file 资源 endpoint。文件路径为 /root/tf/lifecycle/out/endpoint.txt,内容为不带换行的一行字符串。在该资源的 lifecycle 块中写入 create_before_destroy = true,执行 tofu init 后应用。
lifecycle 块写在资源内部。请找出在需要替换时让新资源先创建的那个参数。
用 prevent_destroy 阻止删除
添加 local_file 资源 statefile_guard(文件为 /root/tf/lifecycle/out/guard.txt),在 lifecycle 中写入 prevent_destroy = true 并应用。然后只针对该资源尝试删除(tofu destroy -target=local_file.statefile_guard),并把输出连同标准错误一起保存为 /root/tf/lifecycle/out/prevent.txt。其中必须留有拒绝消息,状态中的 statefile_guard 和 guard.txt 必须原样保留。
这个参数会在计划阶段报错。请只指定一个目标来尝试删除,并把输出整个保存下来。
用 ignore_changes 忽略特定属性
创建一个 local_file 资源,文件为 /root/tf/lifecycle/out/managed-note.txt,内容设为 ignored-value,然后应用。接着在 lifecycle 中写入 ignore_changes = [content],只把代码中的内容值改成另一个字符串,并把 tofu plan 的输出保存为 /root/tf/lifecycle/out/ignore-plan.txt。计划必须是 No changes,实际文件内容仍必须是 ignored-value。
要先应用让值进入状态,再修改代码中的值,才能看到效果。实际文件的内容必须保持首次应用的值。
通过其他资源的变更触发重新创建
创建一个 null_resource 资源,让它通过 triggers 持有一个版本值,并给 local_file 资源 checksum(文件为 /root/tf/lifecycle/out/checksum.txt)设置 replace_triggered_by = [null_resource.<이름>](占位符为资源名称)。保持 checksum 自身的参数不变,只修改版本值,用 tofu plan -out=<계획파일>(占位符为计划文件)保存计划,然后把 tofu show -json <계획파일> 的结果保存为 /root/tf/lifecycle/out/trigger-plan.json。在 JSON 的 resource_changes 中,local_file.checksum 的动作必须有两个(删除和创建)。确认之后再应用。
关键在于:自身的参数没变,却因为别的资源的变更而被替换。请先把计划保存为文件,再转换成 JSON。
把代码之外的变更检测为漂移
不经过代码,直接修改 /root/tf/lifecycle/out/endpoint.txt,使内容中包含 hand-edited。然后执行 tofu plan -refresh-only -detailed-exitcode -out=<계획파일>(占位符为计划文件),只把退出码写入 /root/tf/lifecycle/out/drift-exit.txt(必须是 2),并把 tofu show -json <계획파일> 的结果保存为 /root/tf/lifecycle/out/drift.json。JSON 的 resource_drift 中必须包含 endpoint。不要应用这个计划。
只有不经过代码、直接修改实际资源,才算漂移。请同时使用仅刷新的计划和详细退出码。
以代码为准恢复实际资源
用 tofu apply 以代码为准恢复实际资源,并把输出保存为 /root/tf/lifecycle/out/remediate.txt。其中必须留有 Apply complete 字样,endpoint.txt 中的 hand-edited 必须消失,文件内容必须与状态中记录的 content 值完全相同。
这是应对方向中的第一种。恢复之后,状态中记录的值必须与实际内容一致。
把漂移吸收进代码
先用内容 256 应用 local_file 资源 quota(文件为 /root/tf/lifecycle/out/quota.txt)。这次手动把文件改为 512,不要恢复,而是把 main.tf 中的值改成 512,吸收进代码并应用。然后把 tofu plan 的输出保存为 /root/tf/lifecycle/out/accept-plan.txt,其内容必须是 No changes,且 quota.txt 与 main.tf 中都必须包含 512。
这次是相反的方向。如果判断手动修改的值是正确的,就把代码改成那个值。
制作漂移检测脚本和报告
创建 /root/tf/lifecycle/drift-report.sh 并赋予执行权限。这个脚本要用带 -detailed-exitcode 的计划命令判定漂移,并把结果以 exit_code、drift、checked_at、addresses 四个字段保存到 /root/tf/lifecycle/out/drift-report.json。完成实验时不应存在漂移,所以 exit_code 必须是 0,drift 必须是 false。
判定靠退出码而不是人眼。请注意别让 set -e 在退出码为 2 时把脚本杀掉。