如果用kubeadm初始化一个K8s集群,最容易让人心态爆炸的步骤就是kubeadm init卡在 Pulling images 这一步。终端上几十个镜像名轮番显示Failed to pull image,重试一遍还是失败,最后只剩一个冷冰冰的[kubelet-check] It seems like the kubelet isn't running。这篇文章只讲一件事:kubeadm init 初始化镜像 pull 不下来的时候,怎么通过修改 imageRepository 指向国内可访问的镜像仓库来解决,整个过程不需要使用tag,不需要手动 docker tag 改名,更不需要一台节点一台节点去折腾镜像重命名。
先说清楚适合谁看。如果你是正在搭测试环境、公司内网集群,或者容器运行时刚配好不到三天的新手,这篇文章能让你省下至少一小时。如果你已经折腾过docker tag、把k8s.gcr.io手动改了一轮但发现 crictl 不认,那这篇文章正好解释为什么那条路走不通。
1. 先弄明白:kubeadm init 卡在“pull image”到底卡在哪
1.1 kubeadm init 到底在拉哪些镜像
很多第一次接触 kubeadm 的人会误以为它只是把二进制文件部署到服务器上就完事了,实际上 kubeadm 的职责是“把整个控制平面组件以容器方式跑起来”。kubeadm init 的过程中,它会按照当前指定的 Kubernetes 版本,自动计算出一批固定镜像的版本号,然后去官方仓库拉取。
以常见的 v1.28 系列为例,控制平面节点至少需要这些镜像:
| 组件 | 默认镜像名 |
|---|---|
| kube-apiserver | registry.k8s.io/kube-apiserver:v1.28.x |
| kube-controller-manager | registry.k8s.io/kube-controller-manager:v1.28.x |
| kube-scheduler | registry.k8s.io/kube-scheduler:v1.28.x |
| kube-proxy | registry.k8s.io/kube-proxy:v1.28.x |
| etcd | registry.k8s.io/etcd:3.5.x |
| coredns | registry.k8s.io/coredns:v1.10.x |
| pause | registry.k8s.io/pause:3.9 |
这些镜像全部放在registry.k8s.io这个官方仓库下。注意这个仓库和 docker hub 是两码事,它由不同的CDN和域名解析策略控制,国内网络访问它的时候经常超时、连接重置或者下载到一半卡死。
1.2 错误信息其实已经告诉你答案了
kubeadm init 失败时的日志往往很长,但关键信息就几句:
[preflight] Some fatal error occurred: failed to pull image "registry.k8s.io/kube-apiserver:v1.28.2": failed to pull and unpack image ... write /var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/... no space left on device还有一种是直接超时的:
failed to do request: Get "https://registry.k8s.io/v2/": dial tcp ... i/o timeout看到registry.k8s.io就能确认,问题出在镜像源。这里有个很多人没注意的细节:失败的不一定只有大镜像,像pause:3.9这种不到一百KB的镜像也可能拉不下来。原因不在镜像大小,而是域名整体的网络连通性,连不通就连不通,跟你拉的是大文件还是小文件没关系。
1.3 为什么“手动改tag”听起来靠谱实际很坑
网上有一种流传甚广的做法:先从某个国内镜像仓库把镜像 docker pull 下来,然后docker tag改成官方仓库的名字,最后再执行kubeadm init,让 kubeadm 以为镜像已经存在。
这套思路本身没错,但实操起来非常难受。控制平面至少涉及7个镜像,你得一个个 pull、一个个 tag。更麻烦的是,容器的运行时如果用的 containerd,kubeadm 并不会通过 docker 命令去找镜像,而是通过crictl直接和 containerd 交互。如果你只是用 docker tag 改了名,但 containerd 的命名空间里根本没有这个镜像,kubeadm 照样会重新去拉。
退一步说,就算你成功绕过了一次,后续升级版本时镜像版本一变,又得重新手动来一轮。这个方向的本质问题是把“仓库地址不可达”这个简单问题,硬生生转换成了“手动改一堆 tag 的繁琐问题”。
2. 正解思路:修改 imageRepository,而不是去改tag
2.1 kubeadm 的镜像名是怎么拼出来的
kubeadm 在计算镜像地址时,并不是把一个个完整的镜像名写死在代码里,而是通过配置项动态拼接。镜像全名由三部分组成:
imageRepository + 组件名 + 版本号其中imageRepository就是镜像仓库地址的前缀,默认值是registry.k8s.io。组件名和版本号是 kubeadm 根据你指定的kubernetesVersion、各种组件的默认版本自动算出来的。
举个例子,如果你在初始化时指定了--kubernetes-version=v1.28.2,那么 kubeadm 会自动计算出类似kube-apiserver:v1.28.2、etcd:3.5.7这样的完整镜像名。组件名和版本号你不需要改,也不需要去关心它每个版本对应的 tag 到底是什么,kubeadm 会处理好。你只需要解决一个问题:让那个镜像仓库地址可达。
2.2 一个配置项解决核心问题
kubeadm 支持把初始化参数写进配置文件,其中有一个字段叫imageRepository。你可以在初始化前生成默认配置,然后把这个字段的值改成你本地网络可以访问到的仓库地址。
不少国内云厂商和第三方镜像站都维护了 Kubernetes 官方镜像的同步仓库,地址一般是某个镜像站域名下的子路径。这里的关键逻辑是:仓库地址变了,但镜像名里的组件名和 tag 完全不用动。这正好就是标题里“不需要使用tag”的含义。你不需要知道某一个镜像的tag精确是多少,也不需要把镜像重新打一个带registry.k8s.io前缀的 tag,只需要让 kubeadm 换一个数据源去拉。
2.3 为什么这种方案比“先pull再tag”更稳
两个原因。第一,它把改动集中在一个配置项上,而不是分散在7个甚至更多的镜像上。后期升级的时候,你只需要重新执行 init 前的准备步骤,新的版本对应新的 tag 会自动拼接出来,不用重新去对照版本号。
第二,它不需要依赖 docker 命令。用 containerd 作为容器运行时的情况下,kubeadm 通过 CRI 插件直接拉镜像,只要仓库地址可达,kubeadm 就能用 containerd 自己的镜像管理能力拉取,不会出现 docker tag 完但是 crictl 不认的边界情况。
提示:这里说的“不需要使用tag”,不能误解为完全不关心版本号。版本号由 kubeadm 决定,你只需要把仓库地址换掉,不要自己去手动改掉镜像的 tag。
3. 实操过程:一条龙解决 kubeadm init 镜像拉取问题
3.1 生成默认初始化配置
在已经装好 kubeadm、kubelet、kubectl 的机器上,先用下面的命令生成一份默认的初始化配置:
kubeadm config print init-defaults > kubeadm-init.yaml生成的配置文件很长,大部分内容你暂时用不上,但先不用管,我们要做的只是改其中一个字段。
打开这个文件后,定位到这几行:
apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration ... imageRepository: registry.k8s.io不同版本的 kubeadm 生成的配置模板结构可能会有一点差异,imageRepository可能在InitConfiguration里,也可能在ClusterConfiguration里,需要留意一下上下文。如果文件里没有直接出现,也可以自己补上一个ClusterConfiguration段。
3.2 修改 imageRepository 为可访问的仓库
把imageRepository的值改成你本地网络可以访问到的镜像同步仓库。例如:
imageRepository: registry.example.com/google_containers这里的registry.example.com只是一个占位示例,实际操作时请替换为你所在网络环境中可达的镜像仓库地址。国内常用的做法是选择距离自己网络较近、并且维护了 Kubernetes 官方镜像同步的仓库,具体地址可以在对应镜像服务商的文档里查到。
注意:
google_containers这个子路径要保留。很多镜像同步仓库把 Kubernetes 官方镜像放在这个路径下,路径不同会导致镜像 404。
3.3 用配置预拉取镜像验证方案
不要直接kubeadm init,先执行预拉取命令验证一下仓库地址和网络是否可用:
kubeadm config images pull --config kubeadm-init.yaml这条命令会按照配置文件计算出来的镜像列表逐个拉取。如果拉取过程一路绿灯,说明问题已经解决了。如果某些镜像仍然失败,可以从报错信息里看到具体是哪一步出了问题,常见的几种情况我会在下一章展开。
拉完之后,可以用crictl images ls确认一下当前节点上的镜像列表:
crictl images ls如果看到所有需要的镜像都出现在列表里,说明节点本地已经ready了。
3.4 执行 kubeadm init
镜像就位之后,就可以正式初始化了。继续使用刚才修改好的配置文件:
kubeadm init --config kubeadm-init.yaml初始化过程中,kubelet 会自动启动,Apiserver、Controller-Manager、Scheduler 等组件会以静态Pod的形式出现在节点上。如果前面镜像预拉成功了,走到这一步基本不会再出现Failed to pull image的错误。
3.5 如果 init 到一半失败了怎么处理
初始化失败后不要慌,也不要直接重新执行kubeadm init。先执行kubeadm reset把环境恢复到干净状态,再检查日志定位原因:
kubeadm reset -f rm -rf /etc/cni/net.d /var/lib/kubelet/*然后重新kubeadm config images pull --config kubeadm-init.yaml确认镜像全部就位,再尝试 init。
4. 实操中常见的坑:我踩过的那些问题
4.1 改了 imageRepository 但仍然拉不下来
这种情况多半是配置没生效。检查一下你是不是真的在使用那份配置文件。如果执行的是kubeadm init --config kubeadm-init.yaml,但配置文件里同时存在多个imageRepository字段,kubeadm init 只读取指定位置的值,另一个位置可能还是registry.k8s.io。
解决方式就是打开配置文件,把所有imageRepository都搜一遍,确保没有遗漏的默认值。
还有一种情况是镜像仓库的域名能ping通,但 https 证书或者访问路径有问题。可以用crictl pull手动拉一个单独镜像来测试:
crictl pull registry.example.com/google_containers/pause:3.9如果这条命令报错,仔细看报错内容,大概率是仓库路径不对、仓库不支持该镜像路径,或者仓库本身需要登录。
4.2 kubeadm 在 containerd 环境里找不到镜像
如果你之前用过 docker 手动拉镜像并改了 tag,然后用 containerd 作为运行时,会碰到一个很诡异的现象:docker images能看到镜像,但kubeadm init仍然拉取失败。
因为 docker 的镜像列表和 containerd 的镜像列表是两套数据,互相看不到。containerd 通过自己的命名空间管理镜像,docker 操作的是 docker 自己的命名空间。解决思路就是不要混用,用我前面说的方法,让 kubeadm 自己用 containerd 的接口去拉镜像。
4.3 pause 镜像拉下来但其他镜像报错
pause 镜像比较特殊,它和 kubelet 的沙箱容器绑定在一起。如果在初始化时提示 pause 镜像失败,可以先单独确认一下 containerd 的配置文件里sandbox_image指定的地址:
cat /etc/containerd/config.toml | grep sandbox_image有些版本默认的sandbox_image指向的是registry.k8s.io/pause:3.x,和 kubeadm 配置里走的仓库地址不一致。这两个地方最好保持一致的仓库前缀,否则即使 kubeadm init 成功,后续创建Pod时也可能一直拉不到 pause 镜像,导致Pod无限ContainerCreating。
4.4 只改了控制平面配置,但工作节点还是拉不到
kubeadm init 成功之后,生成kubeadm join指令时,控制平面上的配置信息会包含镜像仓库相关的设置。工作节点执行kubeadm join时,同样需要拉取kube-proxy、pause等镜像。如果工作节点访问官方仓库也不通,join 过程就会卡住。
提前在每一台工作节点上也执行一次kubeadm config images pull,需要指定相同的仓库地址。如果不方便在每台节点上放配置文件,也可以直接在 join 命令后面用--image-repository参数指定:
kubeadm join <控制平面地址>:6443 --token xxx --discovery-token-ca-cert-hash sha256:xxx --image-repository registry.example.com/google_containers4.5 初始化成功了,但节点NotReady
这种情况比较难排查,因为镜像都齐了不一定代表容器运行时没问题。先看节点状态:
kubectl get nodes如果节点状态是NotReady,大概率是 kubelet 和容器运行时之间的连接出了问题。查看 kubelet 日志:
journalctl -u kubelet -f常见的原因是 containerd 没有正确启用 CRI 插件,或者/etc/containerd/config.toml里的disabled_plugins包含cri。这时候需要把 containerd 的配置重置为默认配置并重启,然后再进行 kubeadm init。
我自己就踩过一次特别低级的坑:kubeadm reset之后没清理/var/lib/cni/目录,导致接下来初始化时 CNI 插件配置残留,kubelet 一直报错,怎么拉镜像都没用。后来把残留目录清掉,重置一下就正常了。
5. 离线环境或者仓库不稳定时:用镜像导出导入代替pull
5.1 为什么离线环境也可以不用改tag
本文标题强调“不需要使用tag”,但在真正的离线环境里,这种优势体现得更明显。你只需要在一台能访问外部网络的机器上,用ctr或者crictl把镜像保存成 tar 文件,然后拿到目标节点上用同样工具导入。这个过程中镜像的仓库地址和tag完全不需要改动。镜像内部记录的仓库地址在运行时并不影响容器的启动,容器运行时只看镜像内容本身。
5.2 ctr 导出导入的具体操作
如果目标节点也是 containerd 运行时,使用ctr命令操作最顺滑。首先在源机器上拉取镜像并导出:
ctr -n=k8s.io images pull registry.example.com/google_containers/pause:3.9 ctr -n=k8s.io images export pause.tar registry.example.com/google_containers/pause:3.9然后拷贝 tar 文件到目标节点,导入:
ctr -n=k8s.io images import pause.tar这里有个关键点:要用-n=k8s.io指定命名空间。kubelet 和 kubeadm 使用的镜像都在k8s.io这个命名空间下,如果你不指定命名空间,镜像会导入到默认命名空间,kubeadm 扫不到。
多个镜像可以一次性导出,用空格分隔:
ctr -n=k8s.io images export all-images.tar \ registry.example.com/google_containers/pause:3.9 \ registry.example.com/google_containers/etcd:3.5.7导入的时候同理,一条指令搞定。
5.3 在控制平面节点提前预载
在实际的集群环境里,多节点的情况下,没必要每台节点都手动导入镜像。控制平面节点的镜像准备好之后,join 命令参数里同样指定--image-repository,让工作节点按需拉取即可。如果工作节点是纯内网,无法访问任何外部仓库,那就在每台工作节点上执行一次导入,所有需要的镜像 tar 都传过去。
经验:离线环境最稳妥的做法就是先在控制平面节点把所有镜像
ctr images export导出成一个 tar 包,然后再分发到其他节点用ctr images import导入。这个流程不依赖kubeadm、不需要改tag,出了问题也容易回退。
6. 用一次完整流程验证:从报错到成功初始化
为了让你看得更直观,我模拟一次完整的处理过程。假设一台节点上执行kubeadm init --kubernetes-version=v1.28.2,结果卡在镜像拉取:
failed to pull image "registry.k8s.io/kube-apiserver:v1.28.2": failed to pull and unpack image: ...第一步,生成配置:
kubeadm config print init-defaults > kubeadm-init.yaml第二步,修改配置文件。原文件中:
imageRepository: registry.k8s.io改为:
imageRepository: registry.example.com/google_containers第三步,执行预拉取:
kubeadm config images pull --config kubeadm-init.yaml如果这一步成功,终端会依次显示每个镜像开始拉取和完成的信息。如果出现某个镜像 404,基本就是仓库路径或者版本不对;出现超时,就是网络到该仓库不稳定,换一个离自己更近的仓库地址。
第四步,初始化:
kubeadm init --config kubeadm-init.yaml成功之后,按提示执行:
mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config然后安装 CNI 插件,比如 Calico 或者 Flannel,等待 Pod 起来。
kubectl get pods -n kube-system看到 coredns、etcd、apiserver 等都处于 Running 状态,这个集群就真正可用了。
在我看来,kubeadm init 镜像拉取的问题,本质上不是技术难题,而是很多人在错误的路径上反复尝试。手动去改 tag、手动去 docker pull 再 push,都是把问题复杂化的做法。如果从一开始就明白imageRepository这个配置项的作用,把仓库地址换成可访问的同步源,整个过程能缩短到几分钟。
最后再分享一个小技巧:kubeadm config images list --config kubeadm-init.yaml可以先列出需要拉取的镜像列表,不用等失败就能提前看到数量和版本。在初始化之前执行一下,心里就有底了。如果列表里的镜像来源已经是你改过的仓库地址,那下一步的 pull 基本不会出意外。