离线集群里的 ImagePullBackOff
本实验在真正的 k3s 中运行
VM 里运行着一台 k3s(v1.33.3+k3s1),containerd 和 kubelet 会真实地拉取镜像。第一次启动需要几分钟。VM 起初能连上互联网(80/443)——第 1 步充当联网的导入准备机,第 2 步由你亲手阻断出站,把它变成客户隔离网络。评分由从外部进入 VM 的 8899 端口的代理执行,所以如果阻断了已建立的连接和回环,评分就会中断。
目标
在联网的地方把镜像下载为归档并记录哈希,在出站被阻断的 k3s 中把 ImagePullBackOff 留作证据,然后通过 k3s ctr images import 和 k3s 镜像目录两条路径导入,再确认导入之后仍会失败的拉取策略,并把导入记录与运行时核对后留下。
为什么重要
在隔离网络的客户公司,“Pod 起不来”有一半是镜像问题。连不上注册表的集群自己拿不到镜像,必须有人把它放进运行时存储。而且放进去也不算结束。把搬运途中被改动的文件放进去,就没人知道运行的是什么;如果清单是 imagePullPolicy: Always 或者标签是 latest,即使镜像已经放好,也会去向注册表询问并失败。导入不是“把文件复制了”,而是证明“运行时持有哈希一致的镜像,并且 Pod 在使用它”。
预计 60 分钟。会话结束后 VM 会消失,想保留的文件要在结束前另外保存。
步骤
- 把两个镜像
registry.k8s.io/e2e-test-images/busybox:1.36.1-1和registry.k8s.io/e2e-test-images/nginx:1.15-4下载为镜像归档,放在/root/airgap/bundle/busybox.tar和/root/airgap/bundle/nginx.tar,并在/root/airgap/manifest.txt中每行写一条sha256 파일이름 이미지(占位符依次为文件名和镜像)。归档内的镜像名称必须与上面的名称完全一致。 - 编写
/root/airgap/egress.sh,使重复运行时规则也只留下一份,并以此阻断出站。iptables 链名为AIRGAP-EGRESS,在 OUTPUT 和 FORWARD 中各转入一次。保留回环、已建立的连接、k3s Pod 网段(10.42.0.0/16)和服务网段(10.43.0.0/16),其余全部阻断。在阻断之前和之后,用curl -s -o /dev/null -w '%{http_code}' https://registry.k8s.io/v2/测得的值,用before=、after=写入/root/airgap/egress-proof.txt。 - 在命名空间
airgap中,用 busybox 镜像创建运行sleep 3600的 Podshop-api(清单/root/airgap/shop-api.yaml)。拉取失败后,把uid=<파드 UID>(占位符为 Pod UID)以及状态和事件保存到/root/airgap/pull-failure.txt。 - 核对哈希后,用
k3s ctr images import导入 busybox.tar,并把输出保存到/root/airgap/import-busybox.txt。让shop-api变为 Running。 - 把 nginx.tar 放进 k3s agent 镜像目录(
/var/lib/rancher/k3s/agent/images/)完成导入,并用该镜像让 Podshop-web(清单/root/airgap/shop-web.yaml)变为 Running。 - 用已导入的 busybox 镜像创建 imagePullPolicy 为
imagePullPolicy: Always的 Podshop-always(清单/root/airgap/shop-always.yaml),观察失败,并把uid=、policy=和状态保存到/root/airgap/always.txt。 - 在保持出站阻断的情况下,把
shop-always用imagePullPolicy: IfNotPresent重新创建,并让它变为 Running。 - 在
/root/airgap/import-record.json中留下导入记录。images数组的每一项写入ref、tar、tar_sha256、imported_via(ctr或images-dir)、runtime_image_id(k3s crictl inspecti报告的 id)、pods(当前使用该镜像且处于 Running 的 airgap Pod 名称列表),并在最顶层用 true/false 写入egress_blocked。
参考
- 下载为归档:
skopeo copy docker://<이미지> docker-archive:<파일>:<이미지>(占位符依次为镜像和文件) - 查看运行时存储:
k3s ctr -n k8s.io images ls -q,镜像详情:k3s crictl inspecti <이미지>(占位符为镜像) - Pod 状态原因:
kubectl -n airgap get pod <이름> -o jsonpath='{.status.containerStatuses[0].state.waiting.reason}'(占位符为 Pod 名称) - kubelet 的拉取记录在 k3s 日志中也有:
journalctl -u k3s | grep ErrImagePull - 常见错误 1:阻断时不保留已建立的连接(ESTABLISHED)。评分代理的响应会断掉,评分就会停住。
- 常见错误 2:导入之前不核对哈希。把搬运途中被改动的文件放进去,就没人知道运行的是什么。
- 常见错误 3:觉得导入似乎不起作用,就暂时解除阻断。客户的隔离网络没有这个选项。
在联网一侧制作导入物
把两个镜像下载为镜像归档,放在 /root/airgap/bundle/busybox.tar 和 /root/airgap/bundle/nginx.tar,并在 /root/airgap/manifest.txt 中每行写一条 sha256 파일이름 이미지(占位符依次为文件名和镜像)。
给 skopeo copy 的目标写成 docker-archive:路径:名称,就能不经注册表、直接下载成一个文件。要把名称一并写上,导入之后 Pod 才能用同样的名称找到它。哈希要在搬运之前测好,接收一侧才能核对。
阻断出站,造出隔离网络
用重复运行规则也只留一份的 /root/airgap/egress.sh 阻断出站,并把阻断前后的 HTTP 状态码用 before=、after= 写入 /root/airgap/egress-proof.txt。
新建一条链,在 OUTPUT 和 FORWARD 的最前面转入它,用先清空再重新填充的方式,无论运行多少次都是同样的样子。先用 RETURN 写下要保留的,最后再阻断。这台 VM 的评分是从外部通过 8899 进来的连接,所以如果阻断已建立的连接,评分就会中断。
被阻断的集群里出现 ImagePullBackOff
在命名空间 airgap 中用 busybox 镜像创建 Pod shop-api,观察拉取失败,并把 uid=<파드 UID>(占位符为 Pod UID)以及状态和事件保存到 /root/airgap/pull-failure.txt。
固定了标签的镜像,如果不写 imagePullPolicy,就是 IfNotPresent。运行时里没有,就会出去拉取而被挡住。Pod 很快会被重新创建,所以请现在就把 UID 和事件留下来——kubelet 的记录在 k3s 日志中也有。
用 ctr 直接导入运行时
用 k3s ctr images import 导入 busybox.tar,把输出保存到 /root/airgap/import-busybox.txt,并让 shop-api 变为 Running。
Kubernetes 使用的镜像必须在 containerd 的 k8s.io 命名空间里才看得见。即使导入了,kubelet 在失败不断重复时,到下一次尝试之前也要等得越来越久(最长 5 分钟),如果不想等,就删除 Pod 并用同一份清单重新创建。
通过 k3s 镜像目录导入
把 nginx.tar 放进 k3s agent 镜像目录完成导入,并用该镜像让 Pod shop-web 变为 Running。
k3s 会把放在 agent 的 images 目录里的 tar 导入 containerd。请在文档的版本表中确认,运行中放入的文件是否也会被导入。等到运行时里能看见之后,再创建 Pod。
导入了却依然失败的 Pod
用已导入的 busybox 镜像创建 imagePullPolicy 为 imagePullPolicy: Always 的 Pod shop-always,观察失败,并把 uid=、policy= 和状态保存到 /root/airgap/always.txt。
Always 即使镜像在本地,也每次都会向注册表询问,试图把标签解析成摘要。如果连不上注册表,就会在这一步失败。也请想起,标签是 latest 或没有标签时,默认策略是 Always。
阻断保持不变,修正策略
在保持出站阻断的情况下,把 shop-always 用 imagePullPolicy: IfNotPresent 重新创建,并让它变为 Running。
Pod 容器的 imagePullPolicy 是创建之后不能修改的字段。解除阻断来蒙混过关,是客户隔离网络里做不到的解决办法,所以请修改清单后重新创建。
把导入记录与运行时核对后留下
在 /root/airgap/import-record.json 中留下所导入的两个镜像的记录以及 egress_blocked。
记录里的值不要用手抄,要从命令输出里提取。运行时镜像 ID 由 crictl inspecti 告知,使用该镜像的 Running Pod 从 kubectl 的 JSON 里汇总。导入路径是你实际使用的方法。