挪走状态文件后,同样的服务器变成了两台
目标
打开状态文件查看其结构,并用 OpenTofu 逐一引发并确认:丢失状态时的重复创建、lineage 安全机制、import 恢复、并发应用时的锁、状态拆分以及提交规则。
为什么重要
状态是连接代码与真实基础设施的唯一记录,所以事故通常不是出在代码上,而是出在状态上。状态消失后,工具不知道已经存在的东西,会再创建一遍;用错误的状态去覆盖,就会抹掉别人的记录。恢复只有用 import 逐个找回这一条路,所以启用了版本控制的后端是基本要求。有了锁,两次应用才不会重叠;不把生命周期不同的层放在同一个状态中,每天都在变的一侧才不会让几乎不变的一侧陷入危险。
步骤
- 在
/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 块的数量)三行。 - 创建
/root/iac-state/lost/,并把terraform.tfstate和terraform.tfstate.backup(如果有)移到那里。把tofu plan的输出保存到/root/iac-state/lost-plan.txt,然后 apply。评分器会检查被移走的状态与新状态是否为不同的 lineage,以及 out/ 中是否生成了两个服务器文件。 - 尝试用
tofu state push lost/terraform.tfstate把旧状态放回去。把输出(包括错误)保存到/root/iac-state/push.txt。不要使用-force。评分器还会检查当前状态的 lineage 是否仍与旧状态不同。 - 把同样的
main.tf复制到/root/iac-state/rebuild/,并用import块从旧状态(lost/terraform.tfstate)中导入random_id.server。import 标识符要从旧状态中该资源的属性里找。在 rebuild 中执行 init·apply 之后,计划必须是干净的。评分器会检查 rebuild 状态中的 hex 是否与旧服务器的 hex 相同。 - 在
/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。评分器也会在临时目录中亲自制造同样的情形来检查。 - 在
/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。两个目录的计划都必须是干净的。 - 创建
/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 仓库来检查。
参考
- Pod 中有 OpenTofu 1.9.0 和 local·random provider 镜像源,无需互联网即可运行。本实验的后端全部是 local。
- local 后端的锁是操作系统的文件锁,所以正在应用的进程一旦终止,锁也随之释放。因此需要强制解锁(force-unlock)的“残留的锁”只会出现在远程后端,本 Pod 中不会重现。
- 常见错误:在第 2 步中保留 terraform.tfstate.backup——虽然有备份,工具也不会自动恢复,但日后会让人分不清哪个才是真的。
- 常见错误:用 import 找回时把 hex 填进标识符。每种资源接收的标识符都不同,所以要查看 provider 文档。
- State · State Locking · tofu state push · Import · random_id (Import) · terraform_remote_state · Backend: local
打开状态文件看一看
在 /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.* 这样的模式到底能匹配到哪些文件。