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

基础设施即代码

只改了一个属性,文件却被重新创建

在 TT Lab 中继续学习

目标

阅读真正的声明式工具(OpenTofu)生成的计划,用退出码、已保存的计划和计划 JSON 构建自动判定,并亲自确认 prevent_destroy 能拦住什么、拦不住什么。

为什么重要

上一项实验中用 shell 模拟的 plan 符号,在真正的工具里遵循的是 provider 的规则。只改了文件内容的一行,资源却被替换,这种事很常见;如果不能从计划中读出来,就只能在应用之后才发现。自动化必须代替人来阅读计划,所以要按约定处理退出码和 JSON;而“评审过的计划与实际应用的计划是同一份”这一保证,来自已保存的计划。还要了解删除保护只有写在配置里才有效,这一局限决定了在评审中要看什么。

步骤

  1. 在 /root/iac-plan/main.tf 中声明需要 local·random provider,并声明三个资源。local_file.motd 向 ${path.module}/out/motd.txt 写入一行 hello v1(末尾换行);terraform_data.release 的 input 为 v1;random_pet.suffix 的 keepers 为 team = "blue"。用 tofu init 和 tofu apply -auto-approve 应用。
  2. 创建 /root/iac-plan/gate.sh <작업디렉터리>(占位符为工作目录)。在该目录中运行 tofu plan -detailed-exitcode,没有变更时输出 clean 并以 0 结束,有变更时输出 changes 并以 2 结束,出错时输出 error 并以 1 结束。输出只有那个单词的一行。评分器会创建无变更、有变更、配置错误三种临时目录来运行这个脚本。
  3. 在 main.tf 中修改三个值:motd 的内容改为 hello v2,release 的 input 改为 v2,suffix 的 keepers team 改为 green。不要应用,先用 tofu plan -out=/root/iac-plan/change.tfplan 保存计划,然后在 /root/iac-plan/actions.txt 中为每个发生变化的资源各写一行 <주소> <create|update|replace|delete>(占位符为资源地址)。哪些是原地修改、哪些是替换,根据计划输出中的符号以及 tofu show -json 的 actions 来判断。
  4. 先用 tofu apply change.tfplan 原样应用已保存的计划。然后把 motd 的内容改为 hello v3,并用 tofu plan -out=/root/iac-plan/late.tfplan 保存。在应用这份计划之前,制造出别人插手的情形:先运行 tofu apply -auto-approve -replace=random_pet.suffix -target=random_pet.suffix(只替换 suffix,不动 motd)。现在运行 tofu apply late.tfplan,把标准输出和错误一起保存到 /root/iac-plan/stale.txt。
  5. 创建 /root/iac-plan/summary.sh <계획JSON>(占位符为计划 JSON 文件)。它接收 tofu show -json 的结果文件,输出 create=<수> update=<수> replace=<수> delete=<수>(占位符均为数量)一行。替换(同一个资源中同时存在删除和创建)只算作一次 replace。然后把当前工作目录的计划保存为 /root/iac-plan/now.json(先 tofu plan -out,再 tofu show -json),并把它的汇总保存到 /root/iac-plan/now-summary.txt。
  6. 给 local_file.motd 加上 lifecycle { prevent_destroy = true }。在内容为 hello v3 的状态下运行 tofu plan,把输出(包括错误)保存到 /root/iac-plan/guard.txt。然后把 motd 的内容改回 hello v2,让计划重新变得干净(工作目录的计划必须是无变更)。评分器会在副本中制作一份修改了 motd 内容的计划,确认保护确实起了拦截作用。
  7. 把 /root/iac-plan 连同状态文件整个复制到 /root/iac-plan-bypass(cp -a)。在副本的 main.tf 中,把 local_file.motd 资源块连同 lifecycle 整个删除,并在副本中用 tofu plan -out=/root/iac-plan-bypass/bypass.tfplan 保存计划。不要应用。原目录保持不变。
  8. 创建 /root/iac-plan/review.sh <작업디렉터리>(占位符为工作目录)。把该目录的计划保存为临时文件并用 summary.sh 统计:没有变更时输出 CLEAN 并以 0 结束;只有新增·修改时输出以 AUTO 开头的一行并以 2 结束;只要有一个替换或删除,就输出以 REVIEW 开头的一行并以 3 结束;计划失败时输出 ERROR 并以 1 结束。不要在工作目录中留下计划文件。评分器会用原目录(CLEAN)、绕过副本(REVIEW)以及它自己创建的临时目录来检查。

参考

声明三个资源并应用一次

在 /root/iac-plan/main.tf 中声明需要 local·random provider,并声明三个资源。local_file.motd 向 ${path.module}/out/motd.txt 写入一行 hello v1(末尾换行);terraform_data.release 的 input 为 v1;random_pet.suffix 的 keepers 为 team = "blue"。用 tofu init 和 tofu apply -auto-approve 应用。

local·random 可以从 Pod 内的镜像源获取,所以没有互联网也能完成 init。terraform_data 是无需 provider 的内置资源。应用之后,用 tofu state list 确认状态所知道的地址。

把三种退出码转换成单词的关卡

创建 /root/iac-plan/gate.sh <작업디렉터리>(占位符为工作目录)。在该目录中运行 tofu plan -detailed-exitcode,没有变更时输出 clean 并以 0 结束,有变更时输出 changes 并以 2 结束,出错时输出 error 并以 1 结束。输出只有那个单词的一行。评分器会创建无变更、有变更、配置错误三种临时目录来运行这个脚本。

-detailed-exitcode 会给出 0·1·2 三个值。如果加上 set -e,脚本会在遇到 2 时先行结束。只生成计划的命令可以加上 -lock=false,避免妨碍别人的操作。

只改了一个属性,却要重新创建

在 main.tf 中修改三个值:motd 的内容改为 hello v2,release 的 input 改为 v2,suffix 的 keepers team 改为 green。不要应用,先用 tofu plan -out=/root/iac-plan/change.tfplan 保存计划,然后在 /root/iac-plan/actions.txt 中为每个发生变化的资源各写一行 <주소> <create|update|replace|delete>(占位符为资源地址)。哪些是原地修改、哪些是替换,根据计划输出中的符号以及 tofu show -json 的 actions 来判断。

~ 表示原地修改,-/+ 表示先删除再重新创建的替换。在 JSON 中,可以通过 actions 是 ["update"] 还是 ["delete","create"] 来区分。哪个属性会强制替换,由 provider 决定——在计划输出中寻找 # forces replacement 标记。

只应用评审过的计划,过期的计划会被拒绝

先用 tofu apply change.tfplan 原样应用已保存的计划。然后把 motd 的内容改为 hello v3,并用 tofu plan -out=/root/iac-plan/late.tfplan 保存。在应用这份计划之前,制造出别人插手的情形:先运行 tofu apply -auto-approve -replace=random_pet.suffix -target=random_pet.suffix(只替换 suffix,不动 motd)。现在运行 tofu apply late.tfplan,把标准输出和错误一起保存到 /root/iac-plan/stale.txt。

已保存的计划中同时包含生成计划时的状态。如果这期间状态变了,OpenTofu 就不会应用这份计划。-replace 是不修改配置、直接让该资源被替换的选项,-target 则把计划范围缩小到该资源(如果没有 -target,main.tf 中 v3 的变更也会一并应用)。加上 -no-color,保存下来的文件更容易阅读。

从计划 JSON 中统计操作

创建 /root/iac-plan/summary.sh <계획JSON>(占位符为计划 JSON 文件)。它接收 tofu show -json 的结果文件,输出 create=<수> update=<수> replace=<수> delete=<수>(占位符均为数量)一行。替换(同一个资源中同时存在删除和创建)只算作一次 replace。然后把当前工作目录的计划保存为 /root/iac-plan/now.json(先 tofu plan -out,再 tofu show -json),并把它的汇总保存到 /root/iac-plan/now-summary.txt。

resource_changes 的每个元素都有 change.actions 数组。没有变更的资源是 ["no-op"],不计入任何一栏。在 jq 中可以用 == 来比较数组。

加上保护之后,连替换也被拦住了

给 local_file.motd 加上 lifecycle { prevent_destroy = true }。在内容为 hello v3 的状态下运行 tofu plan,把输出(包括错误)保存到 /root/iac-plan/guard.txt。然后把 motd 的内容改回 hello v2,让计划重新变得干净(工作目录的计划必须是无变更)。评分器会在副本中制作一份修改了 motd 内容的计划,确认保护确实起了拦截作用。

local_file 的 content 一旦改变就是替换。替换中包含删除,所以在设置了 prevent_destroy 的资源上,计划本身就会以错误结束。从错误信息中读出资源地址和原因。

删除资源块,保护也随之消失

把 /root/iac-plan 连同状态文件整个复制到 /root/iac-plan-bypass(cp -a)。在副本的 main.tf 中,把 local_file.motd 资源块连同 lifecycle 整个删除,并在副本中用 tofu plan -out=/root/iac-plan-bypass/bypass.tfplan 保存计划。不要应用。原目录保持不变。

prevent_destroy 只有在配置里存在时才有效。删除资源块,这个配置也随之消失,所以计划会毫无错误地提出删除。这一差别告诉我们,在代码评审中应该看什么。

把含有破坏性操作的计划交给人

创建 /root/iac-plan/review.sh <작업디렉터리>(占位符为工作目录)。把该目录的计划保存为临时文件并用 summary.sh 统计:没有变更时输出 CLEAN 并以 0 结束;只有新增·修改时输出以 AUTO 开头的一行并以 2 结束;只要有一个替换或删除,就输出以 REVIEW 开头的一行并以 3 结束;计划失败时输出 ERROR 并以 1 结束。不要在工作目录中留下计划文件。评分器会用原目录(CLEAN)、绕过副本(REVIEW)以及它自己创建的临时目录来检查。

这道题是把前面步骤中的 gate.sh 和 summary.sh 连接起来。临时文件用 mktemp 创建,并用 trap 删除。没有计划文件就无法得到 JSON——先用 -out 保存,再执行 show。