做出 desired / actual / plan 三分
本实验在真正的 VM 中进行
这个环境不是 Pod,而是由 KubeVirt 启动的虚拟机。Linux 内核独立运行,
systemd 会实际管理服务,docker 也不是模拟程序,而是真正的 Docker 引擎。
通过 docker run 启动的容器会成为实际进程,docker exec 和 docker logs
也都可以照常运行。
过去,本实验在 Pod 内运行。由于环境撤销了全部内核权限,启动容器的步骤 无法执行,因此只能学习手动解开镜像归档的变通办法。现在不再需要变通。
有两点需要了解。
- 首次启动大约需要 1 分钟。 这是因为 VM 需要启动并安装 Docker。 它比 Pod 实验(通常 40 秒)更慢。
- 不提供浏览器预览。 进入 VM 的连接只开放一个评分端口。
如果启动了 Web 服务器,请在 VM 内使用
curl进行确认。
目标
亲手构建由期望状态(desired)、当前状态(actual)以及两者差异(plan)组成的三段式结构,并确认如何检测漂移及使状态收敛。
为什么重要
IaC 工具显示的 + create、- destroy、~ update 并不是魔法,而是比较两种状态所得的结果。亲手构建一次这一结构后,阅读 plan 的视角会有所不同。尤其是在自动化中,必须通过退出码接收结果,因此原样遵循 0 表示无变更、1 表示错误、2 表示存在变更这一惯例。用三轴模型来说,代码是 Desired,状态文件是 Last Known,实际基础设施是 Actual,只要其中任意两个不一致,就是漂移。GitOps 的自愈归根结底也是反复执行这种比较。
步骤
- 在
/root/iac1/desired.yaml中声明期望状态。在顶层写一行containers:,并在其下放置 2 个条目。名称分别为iac-web和iac-cache,每个条目各写一行image:(整个文件中image:必须恰好有 2 行)。镜像只能从 Pod 中预置的alpine:3.20、busybox:1.36、python:3.12-alpine、nginx:1.27-alpine中选择。 - 创建
/root/iac1/parse.sh,将该 YAML 转换为/root/iac1/desired.json。JSON 形状为{"containers":[{"name":"...","image":"..."}, ...]},其中有 2 个元素,且image值不得为空。 - 创建
/root/iac1/actual.sh。它将当前实际运行的受管容器以相同形状的 JSON 输出到标准输出。每个元素都必须同时包含name和image。受管对象仅限名称以iac-开头的容器;镜像字符串应去掉localhost/或docker.io/library/前缀,使其表示方式与 desired 一致。 - 创建
/root/iac1/plan.sh <desired.json> <actual.json>。仅存在于 desired 时逐行输出+ create <이름>,仅存在于 actual 时输出- destroy <이름>,两边都存在但 image 不同时输出~ update <이름>。完全没有差异时输出no changes并返回退出码 0。存在差异时返回退出码 2(0=无变更,1=错误,2=有变更)。 - 创建
/root/iac1/apply.sh,使实际状态与声明一致。执行完毕后,desired 中写明的两个容器都必须处于running状态。 - 在不做任何改动的情况下再次运行 plan,并保存到
/root/iac1/plan2.txt。该文件中必须有no changes,且不得有任何以+、-、~开头的行。 - 从代码外部扰动状态。使用
docker rm -f iac-cache删除一个容器,然后运行 plan 并保存到/root/iac1/drift.txt。变更行必须恰好只有一行,且该行必须是+ create iac-cache。 - 再次运行
apply.sh使状态收敛,然后将此后的 plan 保存到/root/iac1/plan3.txt。必须输出no changes,且两个容器都必须处于running状态。
参考
- 本实验的重点不是解析器质量,而是 desired/actual/plan 的三段式划分。用
grep/sed级别的方法解析 YAML 就足够了。 nginx:1.27-alpine保持原样也会持续运行,但alpine:3.20会立即退出。可以像docker run -d --name iac-cache alpine:3.20 tail -f /dev/null这样让它保持运行。- 第 6 项评分会将 desired.json 与 actual.sh 的输出都只保留
{name,image}后排序,再进行原样比较。镜像表示即使只差一个字符也会失败。 - 常见错误:actual.sh 把不以
iac-开头的其他实验容器也收集进来;plan 输出中漏掉符号与单词之间的空格;在第 07 步删除两个容器,因而出现两行变更。
声明期望状态
在 /root/iac1/desired.yaml 中声明期望状态。在顶层写一行 containers:,并在其下放置 2 个条目。名称分别为 iac-web 和 iac-cache,每个条目各写一行 image:(整个文件中 image: 必须恰好有 2 行)。镜像只能从 Pod 中预置的 alpine:3.20、busybox:1.36、python:3.12-alpine、nginx:1.27-alpine 中选择。
在 /root/iac1/desired.yaml 顶层写一行 containers:,并在其下放置 2 个条目。名称必须准确地是 iac-web 和 iac-cache,每个条目各有一行 image:(共 2 行)。由于环境离线,只能使用预置镜像。
将声明转换为机器可读格式
创建 /root/iac1/parse.sh,将该 YAML 转换为 /root/iac1/desired.json。JSON 形状为 {"containers":[{"name":"...","image":"..."}, ...]},其中有 2 个元素,且 image 值不得为空。
不需要 YAML 解析器。用 grep/sed/awk 提取 name 和 image 值,再用 jq -n 组装成 {containers:[...]}。/root/iac1/desired.json 中必须有 2 个元素,且 image 不得为空。
读取当前状态
创建 /root/iac1/actual.sh。它将当前实际运行的受管容器以相同形状的 JSON 输出到标准输出。每个元素都必须同时包含 name 和 image。受管对象仅限名称以 iac- 开头的容器;镜像字符串应去掉 localhost/ 或 docker.io/library/ 前缀,使其表示方式与 desired 一致。
actual.sh 将形状与 desired 相同的 JSON 输出到标准输出。受管对象仅限名称以 iac- 开头的容器。去掉 podman 添加的 localhost/、docker.io/library/ 前缀,使其表示方式与 desired 一致。
计算差异
创建 /root/iac1/plan.sh <desired.json> <actual.json>。仅存在于 desired 时输出 + create <이름>,仅存在于 actual 时输出 - destroy <이름>,两边都存在但 image 不同时逐行输出 ~ update <이름>。完全没有差异时输出 no changes 并返回退出码 0。存在差异时返回退出码 2(0=无变更,1=错误,2=有变更)。
plan.sh <desired.json> <actual.json> 对仅存在于 desired 的项输出 + create <이름>,仅存在于 actual 的项输出 - destroy <이름>,镜像不同时输出 ~ update <이름>。符号与单词之间要有空格。没有差异时输出 no changes 并返回退出码 0。
创建声明的状态
创建 /root/iac1/apply.sh,使实际状态与声明一致。执行完毕后,desired 中写明的两个容器都必须处于 running 状态。
apply.sh 只需执行 plan 指示的操作。对于 alpine:3.20 这类会立即退出的镜像,需要提供 tail -f /dev/null 之类的命令,才能使其持续处于 running 状态。
再次应用也没有变化
在不做任何改动的情况下再次运行 plan,并保存到 /root/iac1/plan2.txt。该文件中必须有 no changes,且不得有任何以 +、-、~ 开头的行。
在不做任何改动的状态下再次运行 plan,并保存到 /root/iac1/plan2.txt。其中只能有 no changes,不得有任何以 +、-、~ 开头的行。评分会重新比较 desired.json 与 actual.sh 的结果,因此镜像表示即使只差一个字符也会失败。
检测外部产生的变更
从代码外部扰动状态。使用 docker rm -f iac-cache 删除一个容器,然后运行 plan 并保存到 /root/iac1/drift.txt。变更行必须恰好只有一行,且该行必须是 + create iac-cache。
使用 docker rm -f iac-cache 从代码外部扰动状态,然后将 plan 保存到 /root/iac1/drift.txt。变更行必须恰好只有一行,即 + create iac-cache。只删除了一个容器却出现多行,说明 actual.sh 选错了受管对象。
自动收敛
再次运行 apply.sh 使状态收敛,然后将此后的 plan 保存到 /root/iac1/plan3.txt。必须输出 no changes,且两个容器都必须处于 running 状态。
再次 apply 后,将 plan 保存到 /root/iac1/plan3.txt。如果输出 no changes,并且两个容器都处于 running 状态,就相当于手动复现了 GitOps 控制器的自愈过程。