LabHub
学习 学习路径 课程

基础设施即代码

做出 desired / actual / plan 三分

在 LabHub 中继续学习

本实验在真正的 VM 中进行

这个环境不是 Pod,而是由 KubeVirt 启动的虚拟机。Linux 内核独立运行, systemd 会实际管理服务,docker 也不是模拟程序,而是真正的 Docker 引擎。 通过 docker run 启动的容器会成为实际进程,docker execdocker logs 也都可以照常运行。

过去,本实验在 Pod 内运行。由于环境撤销了全部内核权限,启动容器的步骤 无法执行,因此只能学习手动解开镜像归档的变通办法。现在不再需要变通。

有两点需要了解。

目标

亲手构建由期望状态(desired)、当前状态(actual)以及两者差异(plan)组成的三段式结构,并确认如何检测漂移及使状态收敛。

为什么重要

IaC 工具显示的 + create- destroy~ update 并不是魔法,而是比较两种状态所得的结果。亲手构建一次这一结构后,阅读 plan 的视角会有所不同。尤其是在自动化中,必须通过退出码接收结果,因此原样遵循 0 表示无变更、1 表示错误、2 表示存在变更这一惯例。用三轴模型来说,代码是 Desired,状态文件是 Last Known,实际基础设施是 Actual,只要其中任意两个不一致,就是漂移。GitOps 的自愈归根结底也是反复执行这种比较。

步骤

  1. /root/iac1/desired.yaml 中声明期望状态。在顶层写一行 containers:,并在其下放置 2 个条目。名称分别为 iac-webiac-cache,每个条目各写一行 image:(整个文件中 image: 必须恰好有 2 行)。镜像只能从 Pod 中预置的 alpine:3.20busybox:1.36python:3.12-alpinenginx:1.27-alpine 中选择。
  2. 创建 /root/iac1/parse.sh,将该 YAML 转换为 /root/iac1/desired.json。JSON 形状为 {"containers":[{"name":"...","image":"..."}, ...]},其中有 2 个元素,且 image 值不得为空。
  3. 创建 /root/iac1/actual.sh。它将当前实际运行的受管容器以相同形状的 JSON 输出到标准输出。每个元素都必须同时包含 nameimage。受管对象仅限名称以 iac- 开头的容器;镜像字符串应去掉 localhost/docker.io/library/ 前缀,使其表示方式与 desired 一致。
  4. 创建 /root/iac1/plan.sh <desired.json> <actual.json>。仅存在于 desired 时逐行输出 + create <이름>,仅存在于 actual 时输出 - destroy <이름>,两边都存在但 image 不同时输出 ~ update <이름>。完全没有差异时输出 no changes 并返回退出码 0。存在差异时返回退出码 2(0=无变更,1=错误,2=有变更)。
  5. 创建 /root/iac1/apply.sh,使实际状态与声明一致。执行完毕后,desired 中写明的两个容器都必须处于 running 状态。
  6. 在不做任何改动的情况下再次运行 plan,并保存到 /root/iac1/plan2.txt。该文件中必须有 no changes,且不得有任何以 +-~ 开头的行。
  7. 从代码外部扰动状态。使用 docker rm -f iac-cache 删除一个容器,然后运行 plan 并保存到 /root/iac1/drift.txt。变更行必须恰好只有一行,且该行必须是 + create iac-cache
  8. 再次运行 apply.sh 使状态收敛,然后将此后的 plan 保存到 /root/iac1/plan3.txt。必须输出 no changes,且两个容器都必须处于 running 状态。

参考

声明期望状态

/root/iac1/desired.yaml 中声明期望状态。在顶层写一行 containers:,并在其下放置 2 个条目。名称分别为 iac-webiac-cache,每个条目各写一行 image:(整个文件中 image: 必须恰好有 2 行)。镜像只能从 Pod 中预置的 alpine:3.20busybox:1.36python:3.12-alpinenginx:1.27-alpine 中选择。

/root/iac1/desired.yaml 顶层写一行 containers:,并在其下放置 2 个条目。名称必须准确地是 iac-webiac-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 输出到标准输出。每个元素都必须同时包含 nameimage。受管对象仅限名称以 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 控制器的自愈过程。