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

基础设施即代码

挪走状态文件后,同样的服务器变成了两台

在 TT Lab 中继续学习

目标

打开状态文件查看其结构,并用 OpenTofu 逐一引发并确认:丢失状态时的重复创建、lineage 安全机制、import 恢复、并发应用时的锁、状态拆分以及提交规则。

为什么重要

状态是连接代码与真实基础设施的唯一记录,所以事故通常不是出在代码上,而是出在状态上。状态消失后,工具不知道已经存在的东西,会再创建一遍;用错误的状态去覆盖,就会抹掉别人的记录。恢复只有用 import 逐个找回这一条路,所以启用了版本控制的后端是基本要求。有了锁,两次应用才不会重叠;不把生命周期不同的层放在同一个状态中,每天都在变的一侧才不会让几乎不变的一侧陷入危险。

步骤

  1. 在 /root/iac-state/main.tf 中放置 random_id.server(byte_length 4),以及向 out/server-<그 hex>.txt(占位符为该 hex 值)写入一行 server <hex> 的 local_file.inventory,然后执行 init·apply。接着把状态文件复制到 /root/iac-state/backup/terraform.tfstate,并用从该副本中读到的值,在 /root/iac-state/anatomy.txt 中写入 lineage=<값>、serial=<값>、resources=<리소스 블록 수>(占位符依次为 lineage 值、serial 值、resources 块的数量)三行。
  2. 创建 /root/iac-state/lost/,并把 terraform.tfstate 和 terraform.tfstate.backup(如果有)移到那里。把 tofu plan 的输出保存到 /root/iac-state/lost-plan.txt,然后 apply。评分器会检查被移走的状态与新状态是否为不同的 lineage,以及 out/ 中是否生成了两个服务器文件。
  3. 尝试用 tofu state push lost/terraform.tfstate 把旧状态放回去。把输出(包括错误)保存到 /root/iac-state/push.txt。不要使用 -force。评分器还会检查当前状态的 lineage 是否仍与旧状态不同。
  4. 把同样的 main.tf 复制到 /root/iac-state/rebuild/,并用 import 块从旧状态(lost/terraform.tfstate)中导入 random_id.server。import 标识符要从旧状态中该资源的属性里找。在 rebuild 中执行 init·apply 之后,计划必须是干净的。评分器会检查 rebuild 状态中的 hex 是否与旧服务器的 hex 相同。
  5. 在 /root/iac-state/lockdemo/main.tf 中放置通过 local-exec 睡眠 15 秒的 terraform_data.slow,并执行 init。在后台启动 apply,在它结束之前,在同一目录中运行 tofu plan,把输出保存到 /root/iac-state/lock.txt。接着运行 tofu plan -lock-timeout=60s,等到锁被释放之后,把输出保存到 /root/iac-state/lockwait.txt。评分器也会在临时目录中亲自制造同样的情形来检查。
  6. 在 /root/iac-state/network/ 中放置 random_id.vpc(byte_length 3)和输出 vpc_id = "vpc-<hex>";在 /root/iac-state/app/ 中放置 terraform_remote_state 数据源(local 后端,../network/terraform.tfstate),读取该输出,并向 out/app.conf 写入一行 vpc=<vpc_id> 的 local_file.app。先对 network、后对 app 执行 init·apply。两个目录的计划都必须是干净的。
  7. 创建 /root/iac-state/.gitignore。以 git check-ignore 为准,terraform.tfstate、terraform.tfstate.backup、lost/terraform.tfstate、.terraform/(其中的文件)、change.tfplan 这类计划文件必须被忽略,而 .terraform.lock.hcl、main.tf、app/main.tf 不得被忽略。评分器会把这个 .gitignore 放进临时 git 仓库来检查。

参考

打开状态文件看一看

在 /root/iac-state/main.tf 中放置 random_id.server(byte_length 4),以及向 out/server-<그 hex>.txt(占位符为该 hex 值)写入一行 server <hex> 的 local_file.inventory,然后执行 init·apply。接着把状态文件复制到 /root/iac-state/backup/terraform.tfstate,并用从该副本中读到的值,在 /root/iac-state/anatomy.txt 中写入 lineage=<값>、serial=<값>、resources=<리소스 블록 수>(占位符依次为 lineage 值、serial 值、resources 块的数量)三行。

状态文件是 JSON。lineage 是这个状态在首次创建时附加的唯一编号,serial 则在每次写入时递增。用 jq 查看 .lineage、.serial、.resources | length。

丢失状态后,同一台服务器又多出了一台

创建 /root/iac-state/lost/,并把 terraform.tfstate 和 terraform.tfstate.backup(如果有)移到那里。把 tofu plan 的输出保存到 /root/iac-state/lost-plan.txt,然后 apply。评分器会检查被移走的状态与新状态是否为不同的 lineage,以及 out/ 中是否生成了两个服务器文件。

工具把不在状态中的资源视为“尚未创建”。由于这种资源的名称是随机生成的,所以不会发生冲突,而是悄悄地多出一个。阅读计划输出中的 Plan 那一行。

想覆盖旧状态,却被拒绝了

尝试用 tofu state push lost/terraform.tfstate 把旧状态放回去。把输出(包括错误)保存到 /root/iac-state/push.txt。不要使用 -force。评分器还会检查当前状态的 lineage 是否仍与旧状态不同。

lineage 不同的两个状态,并不是“同一套基础设施在不同时间点的样子”,而是“两份互不相干的记录”。想一想工具为什么要阻止这种做法。-force 是关闭这道安全机制的选项。

用 import 找回丢失的服务器

把同样的 main.tf 复制到 /root/iac-state/rebuild/,并用 import 块从旧状态(lost/terraform.tfstate)中导入 random_id.server。import 标识符要从旧状态中该资源的属性里找。在 rebuild 中执行 init·apply 之后,计划必须是干净的。评分器会检查 rebuild 状态中的 hex 是否与旧服务器的 hex 相同。

random_id 支持 import,并接收 b64_url 值作为标识符(见 provider 文档)。import 块只需要写 to 和 id 两项,计划中必须能看到“will be imported”。如果有新创建的 random_id,就说明做错了。

应用期间,其他计划会被锁拦住

在 /root/iac-state/lockdemo/main.tf 中放置通过 local-exec 睡眠 15 秒的 terraform_data.slow,并执行 init。在后台启动 apply,在它结束之前,在同一目录中运行 tofu plan,把输出保存到 /root/iac-state/lock.txt。接着运行 tofu plan -lock-timeout=60s,等到锁被释放之后,把输出保存到 /root/iac-state/lockwait.txt。评分器也会在临时目录中亲自制造同样的情形来检查。

锁由所有可以写入状态的命令自动获取。默认情况下不会等待,而是立即失败;指定 -lock-timeout 后,则会等待相应的时间。后台任务用 & 启动,用 wait 等待其结束。

生命周期不同的层要拆分状态,只通过输出相连

在 /root/iac-state/network/ 中放置 random_id.vpc(byte_length 3)和输出 vpc_id = "vpc-<hex>";在 /root/iac-state/app/ 中放置 terraform_remote_state 数据源(local 后端,../network/terraform.tfstate),读取该输出,并向 out/app.conf 写入一行 vpc=<vpc_id> 的 local_file.app。先对 network、后对 app 执行 init·apply。两个目录的计划都必须是干净的。

app 的状态中不会包含网络资源,只包含数据源。所以即使每天都修改 app,网络也不会被锁住,也不会出现在计划里。作为代价,app 会依赖 network 的输出名称。

状态和计划不提交,锁文件要提交

创建 /root/iac-state/.gitignore。以 git check-ignore 为准,terraform.tfstate、terraform.tfstate.backup、lost/terraform.tfstate、.terraform/(其中的文件)、change.tfplan 这类计划文件必须被忽略,而 .terraform.lock.hcl、main.tf、app/main.tf 不得被忽略。评分器会把这个 .gitignore 放进临时 git 仓库来检查。

状态和已保存的计划中,值是以明文保存的(下一项实验里会亲自看到)。锁文件会固定 provider 的版本和哈希,所以应与代码一起评审。确认 *.tfstate.* 这样的模式到底能匹配到哪些文件。