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

FDE综合实战:仓库收到了三次相同订单

把镜像带进离线集群的三条路

在 TT Lab 中继续学习

一句话总结

隔离网络集群中的节点自己无法获取镜像。必须有人把镜像放进运行时存储,而且放进之后,Pod 的拉取策略也不能再去向注册表询问。k3s 为此提供三条路径:私有注册表、按节点的镜像归档、内置注册表镜像(mirror)。

为什么需要它

由于客户公司的安全规定,生产集群连不上互联网。交付前一天应用清单后,Pod 经过 ErrImagePull 停在了 ImagePullBackOff。这是在联网的开发环境里从来没见过的状态,因为在那里 kubelet 会自己把镜像拉下来。在隔离网络里,没有这个“自己”。FDE 要做的有两件事:在联网的地方决定要带走什么并搬运过去,在接收一侧证明它与搬运之前是同一个东西,然后再放进运行时。

工作原理

Pod 看的是节点的运行时存储,而不是注册表。kubelet 启动容器时遵循拉取策略。根据 Kubernetes 文档,IfNotPresent 只在本地没有时才拉取,Never 不会去拉取,Always 则在每次启动容器时,由运行时联系注册表,把标签解析成摘要,只拉取缺少的层。不写策略的话,标签是 :latest 或没有标签时为 Always,其他标签则为 IfNotPresent。拉取失败时,kubelet 以指数增长的间隔重试,间隔上限为 300 秒。所以导入之后,Pod 也可能一动不动地停上几分钟。

k3s 的三条路径。k3s 的 air-gap 安装文档把引入镜像的方法分为三种。

사설 레지스트리       /etc/rancher/k3s/registries.yaml 로 미러·인증을 설정. 바꾸면 노드마다 k3s 재시작
노드에 직접 배포      /var/lib/rancher/k3s/agent/images/ 에 이미지 tar 를 둔다
내장 레지스트리 미러  한 노드의 containerd 저장소에 있는 이미지를 다른 노드가 받아 간다

私有注册表文档里有一句容易漏掉的话。containerd 对所有注册表都有一个隐含的默认端点,即使在 registries.yaml 里写了其他端点,这个默认端点也总是作为最后的手段被尝试。另外,没有写明注册表的镜像名称,由于历史原因被视为 docker.io。在隔离网络里,像 nginx:1.25 这样写得很短的清单会去向哪里询问,就取决于这一点。

镜像目录什么时候被读取。air-gap 文档写道,k3s 每次启动都会导入该目录中的归档。这是为了即使有镜像被删除或清理,也总能重新补齐;代价是在处理完所有归档之前 kubelet 不会启动,启动会变慢。所以自 2025 年 5 月的版本(v1.33.1+k3s1、v1.32.5+k3s1 等)起,只要在目录里放 .cache.json,就可以使用跳过大小和修改时间相同的归档的条件导入。这种情况下,被删除的镜像要用 ctr image import 手工重新导入,或者 touch 一下归档。另一方面,镜像导入文档写道,导入运行中放入的 tar 这一功能是从 2025 年 1 月的版本(v1.32.0+k3s1、v1.31.5+k3s1、v1.30.9+k3s1、v1.29.13+k3s1)开始的,在此之前只在开机时导入。这就是要先确认客户集群的 k3s 版本的原因。同一个目录里如果放一个每行写一个镜像名称的文本文件,反过来会成为在线预先拉取的用途,这一点也不要弄混。

导入以哈希收尾。搬运之前记录归档的 sha256,在接收一侧用同样的命令核对之后再放进去。放进去之后,再看运行时报告的镜像 ID(配置哈希)是否与归档里的配置哈希相同。这两者连起来,才能说“客户集群上运行的就是我们验证过的那个”。

在现场相遇的样子

在实验 VM 上这样测过(实测,v1.33.3+k3s1,containerd v2.0.5-k3s2)。用 REJECT 阻断出站后,对 https://registry.k8s.io/v2/ 的 curl 状态码从 401 变成了 000,新 Pod 在创建后 2 秒内经过 ErrImagePull 变为 ImagePullBackOff。事件中的原因行以 failed to resolve reference 开头,写着 Head "https://registry.k8s.io/v2/e2e-test-images/busybox/manifests/1.36.1-1" 请求以 connection refused 结束。也就是说,在拉取层之前,在为了把标签解析成摘要而向注册表发出的第一个请求时,就已经被挡住了。用 k3s ctr -n k8s.io images import 放入 busybox 归档(4.5MB)后,输出里打印出了 saved 和清单摘要。不过这个输出混有进度显示,保存成文件的镜像名称留下了缺少连字符和标签的样子。导入记录用摘要来核对,比用名称更安全。把 nginx 归档(17MB)复制到运行中的 k3s 的镜像目录后,k3s 日志里打印出 Importing images from 和 Imported images ... in 1.094242885s,不用重新启动,它就出现在了运行时存储中。用已导入的 busybox 创建 imagePullPolicy: Always 的 Pod,尽管镜像就在存储里,也因为同样的 HEAD 请求失败而出现 ErrImagePull。

第二常见的事故发生在导入结束之后。有人在清单里写了 imagePullPolicy: Always,或者把标签设成了 latest。镜像就在存储里,但 kubelet 还是会去向注册表询问,在被阻断的网络里失败。这时,Pod 容器的 imagePullPolicy 创建之后不能再改,所以必须重新创建 Pod。暂时解除阻断来蒙混过关,在联网环境里行得通,但客户的隔离网络没有这个选项。

模拟隔离网络时也有陷阱。本实验的 VM 是通过从外部进来的评分连接(8899)来操作的,所以阻断出站的同时,如果不保留已建立的连接、回环和集群内部网段,评分和 API 会一起断掉。规则要集中在一条链里,无论运行多少次都保持相同的样子,接手的人再运行才安全。

实际工作中真正重要的事

下一项实验要做什么

在联网状态的 k3s VM 上,把两个镜像下载为归档并记录哈希,用 iptables 阻断出站,造出隔离网络。在被阻断的状态下,把新镜像 Pod 拉取失败的情况留作证据,再通过 k3s ctr images import 和镜像目录两条路径导入并启动 Pod。确认用已导入的镜像、Always 的 Pod 会失败,修正策略之后,把导入记录与运行时核对并留下。

参考文档:K3s Air-Gap Install、K3s Import Images、K3s Private Registry Configuration、Kubernetes Images(imagePullPolicy)