最近有个朋友问我,能不能在不折腾网络的前提下,用 kubeadm 装一个指定版本的 Kubernetes 集群。他试过几次kubeadm init,每次都在拉取registry.k8s.io这一步超时,要么就是预检阶段卡了几分钟直接报错。这个问题在受网络限制的环境里非常典型:kubeadm 本身设计简单,安装失败往往不是因为命令写错,而是组件镜像拿不到。这篇文章我会把一条不依赖“海外直连”、完全基于国内可见镜像仓库和离线镜像包导入的 kubeadm 安装路径完整写一遍。从版本对应关系、镜像准备、初始化配置,到工作节点加入和常见故障排查,全部覆盖。适合需要固定 Kubernetes 版本、打算部署测试环境或者建设内网集群的运维和开发同学参考。我尽量把每一步的“为什么这么做”也讲清楚,这样你换一个版本或者换一套网络环境,也能自己推导出正确做法。1. 动手前先把版本关系和高版本陷阱理清1.1 组件版本必须锁成一个整体很多人装集群喜欢“先装最新版”,或者随手apt install kubeadm,然后发现装出来的 kubelet 是 1.29,初始化配置却写的 1.28,部署出来控制面经常出现各种奇怪的兼容问题。Kubernetes 的版本策略是:kubelet 允许比 kube-apiserver 低一个小版本,但 kubeadm、kubelet、kubectl 三者最好保持同一个版本,控制面核心组件(apiserver、controller-manager、scheduler)也必须和 kubeadm 里指定的kubernetesVersion保持一致。在我实际部署中,最省心的做法是“先选 Kubernetes 版本,再反推工具版本”。比如我要装 1.28.x,就统一安装这三个二进制:kubeadm 1.28.xkubelet 1.28.xkubectl 1.28.x然后把容器运行时也固定下来。现在 containerd 1.7.x 搭配 Kubernetes 1.28 是非常成熟稳定的组合,比 Docker 加 cri-dockerd 少一层转换,配置也简单。建议不要在这个环节引入“最新稳定版”,网络环境本来就受限,没必要再给排查兼容性增加难度。这里有一个容易被忽略的细节:控制面的镜像 tag 里带着 Kubernetes 版本号,比如kube-apiserver:v1.28.2。如果你的 kubeadm 版本是 1.28.2,而kubernetesVersion写的是v1.28.2,kubeadm 才会去拉取这个精确的镜像。版本号必须完全匹配,少写一个.2,它会去拉一个不存在的 tag,直接报NotFound。1.2 节点环境准备清单我建议每台节点都提前完成下面这些基础工作,不要等到 kubeadm 预检报错了再回来补:操作系统:Ubuntu 22.04 或者 CentOS 7.9 以上都行,内核不要太老。资源要求:控制平面节点至少 2 核 2G,工作节点至少 2 核 2G,这是底线。关闭 swap:kubelet 默认不支持 swap 开启的环境,kubeadm init预检会直接提示失败。需要swapoff -a,并且把/etc/fstab里的 swap 条目注释掉,否则重启又回来了。加载内核模块:overlay和br_netfilter这两个必须启用,否则后续 Pod 网络和流量转发会有问题。设置系统参数:net.bridge.bridge-nf-call-iptables=1、net.ipv4.ip_forward=1这类参数建议写进/etc/sysctl.d/k8s.conf。这些操作本身没有难度,但值得花十分钟核对一遍。预检阶段是可以自动发现大部分问题的,真正麻烦的是预检通过之后,容器运行时和 kubelet 之间出现隐性问题,那种报错信息绕来绕去,排查成本高得多。2. 镜像获取的三种思路:mirror 配置、离线包导入与 imageRepository 替换Kubernetes 控制面启动时会用到一组固定镜像:kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy、etcd、coredns、pause。默认情况下,kubeadm 从registry.k8s.io这个地址下拉取它们。网络受限时问题就出在这里。2.1 方案一:为 containerd 配置镜像加速如果你有可用的国内公共镜像仓库,最顺滑的办法是把 containerd 配置成“从镜像仓库拉取官方镜像”,也就是 pull-through mirror。这样 kubeadm 和 kubelet 完全不用改,仍然写registry.k8s.io,containerd 在背后自动去配置的镜像源拉取。containerd 的配置文件默认在/etc/containerd/config.toml。如果你还没有生成过它,先执行containerd config default > /etc/containerd/config.toml,然后在配置里找到 registry.mirrors 部分,为registry.k8s.io增加一个 endpoint:[plugins."io.containerd.grpc.v1.cri".registry.mirrors] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."registry.k8s.io"] endpoint = ["https://你的镜像仓库地址"]注意修改完一定要重启 containerd:systemctl restart containerd。这个操作不会破坏已有镜像,可以放心做。这个方案对 kubeadm 完全透明,后续加节点、跑工作负载都不用再操心镜像地址。唯一的风险是,你选的镜像仓库必须持续可用,而且同步的镜像路径要和官方一致。我的习惯是先在命令行验证一下:用ctr -n k8s.io images pull registry.k8s.io/pause:3.9拉一次,能成功说明路径没问题。2.2 方案二:离线镜像包整体导入对于完全隔离内网的环境,mirror 方案也失效了,只能用“离线包”思路。流程很简单:在一台能访问镜像仓库的机器上,用ctr把需要的镜像全部导出成 tar 文件,拷到目标机器上,再用ctr导入到 containerd。获取镜像列表最稳妥的命令是:kubeadm config images list --kubernetes-version=v1.28.2这里不建议手动拼镜像名,因为很容易漏掉pause或者etcd。拿到列表后,逐条拉取并导出:ctr -n k8s.io images pull registry.k8s.io/kube-apiserver:v1.28.2 ctr -n k8s.io images export kube-apiserver.tar registry.k8s.io/kube-apiserver:v1.28.2在目标机器上导入:ctr -n k8s.io images import kube-apiserver.tar这里有两个关键点。第一,必须指定命名空间k8s.io,因为 containerd 的 CRI 插件默认使用k8s.io这个命名空间,kubelet 创建的容器镜像都在这里面。导入到别的命名空间,kubelet 根本看不到。第二,镜像的完整名称要保持不变,包括registry.k8s.io/这个前缀,否则 containerd 会认为这是另一个镜像,仍然尝试去远端拉取。这个方案对网络要求最低,也最可控。缺点是更新版本时要重新导出导入一遍,稍微麻烦一点。如果只是偶尔部署一次集群,我反而推荐这个方案,因为它把“能不能装上”和“网络好不好”彻底解耦了。2.3 方案三:直接替换 imageRepositorykubeadm 的初始化配置里有一个imageRepository字段,默认是registry.k8s.io。如果你用的是某个已经同步了 Kubernetes 官方镜像的国内仓库,可以直接把imageRepository改成那个仓库地址。kubeadm 拉取的镜像地址就会变成你的仓库地址/kube-apiserver:v1.28.2。需要注意路径问题。官方镜像路径是registry.k8s.io/kube-apiserver,很多镜像仓库并不会原样保留这个路径,而是放在类似你的仓库地址/google_containers/kube-apiserver的路径下。如果你的镜像仓库布局不一样,光改imageRepository是不够的,还要保证镜像的“仓库路径 + 镜像名”和 containerd 里实际存在的完整名称一致。最稳妥的办法是先用ctr -n k8s.io images pull 你的仓库地址/kube-apiserver:v1.28.2验证一遍,确认能拉下来再写进配置。2.4 我的选型建议经常有人问我“到底用哪个方案”。我的判断标准很简单:场景推荐方案理由测试环境 + 网络尚可mirror 配置改动最小,加节点最方便隔离内网离线包导入不依赖任何外部服务,绝对可控已有内部镜像仓库替换 imageRepository统一管理,后续版本升级路径清晰我个人最常用的是方案二,也就是离线包导入。原因很实在:它不依赖镜像仓库的稳定性,排障路径最短。出了任何镜像相关的问题,我只需要检查 containerd 的k8s.io命名空间里有没有对应的镜像,其他的不用考虑。3. kubeadm init 配置逐字段拆解:从生成配置到正式开始安装3.1 一个完整可用的初始化配置文件kubeadm 支持纯命令行参数,但版本多、参数多的时候很容易漏,而且不方便审计。我强烈建议提前写好一个 YAML 文件,所有节点都用同一套配置。下面是一个以 1.28.2 为例的配置:apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.1.10 bindPort: 6443 nodeRegistration: criSocket: unix:///var/run/containerd/containerd.sock name: master01 imagePullPolicy: IfNotPresent --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.28.2 imageRepository: registry.k8s.io networking: podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/12 --- apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration cgroupDriver: systemd三个文档块分别对应 InitConfiguration、ClusterConfiguration、KubeletConfiguration,用---分隔。如果你之前对 kubeadm 配置文件不熟,可以执行kubeadm config print init-defaults生成一份默认配置作为底稿,再照着上面把关键字段改掉就行。3.2 几个关键字段为什么要这么写先看advertiseAddress。它必须写当前节点的内网 IP,不能写 127.0.0.1,也不能写公网 IP,否则 kube-apiserver 通告给其他节点的地址不对,工作节点 join 之后会连不上 API Server。这是一个很常见但是报错信息不太直观的问题。criSocket指向/var/run/containerd/containerd.sock,这是 containerd 的标准 socket 路径。如果你用的是 Docker,这个值就要改成unix:///var/run/cri-dockerd.sock,但正如前面说的,我建议直接使用 containerd,省掉一层转换。imagePullPolicy: IfNotPresent这个字段容易被忽略。默认情况下,kubeadm 在某些版本组合中会尝试先拉取远端镜像,即使本地已经导入了一份。对于离线场景,显式设置成IfNotPresent可以强制 kubeadm 优先检查本地 containerd,这是一个很实用的细节,网上不少离线安装教程都会漏掉这一步,导致镜像明明导入了,预检仍然报拉取失败。podSubnet写10.244.0.0/16是 Calico 网络插件的惯例网段。并不是 kubeadm 强制要求这个值,而是 Calico 的默认配置期望在这个网段内分配 Pod IP。如果用其他网段,后面安装网络插件时要额外修改 IPPool 配置,容易出错。新手建议直接沿用这个网段。cgroupDriver: systemd必须和 containerd 的 cgroup 驱动一致。这个后面专门讲,这里先记住一个结论:Kubernetes 1.24 之后,官方推荐并且默认使用systemd作为 cgroup 驱动,containerd 也要配合启用SystemdCgroup = true。3.3 先拉镜像再初始化,避免预检卡死如果走 mirror 或离线导入方案,建议先手动预拉一遍镜像,确认镜像全部就绪,再执行初始化:kubeadm config images pull --config kubeadm-config.yaml这条命令会按照配置里的kubernetesVersion和imageRepository去拉取所有需要的镜像。它执行成功,说明镜像层面已经没有问题,接下来初始化大概率不会被卡住。如果网络确实不通,预拉会失败,没关系,只要你确认镜像已经通过ctr导入到k8s.io命名空间,同样可以直接进入初始化:kubeadm init --config kubeadm-config.yaml初始化过程会做很多事,包括生成证书、写入 kubeconfig、启动控制面静态 Pod。通常几分钟内会看到类似Your Kubernetes control-plane has initialized successfully的输出。看到这个输出,控制面节点就已经起来了。4. 控制平面起来之后,立刻要处理的网络插件和本机调度4.1 配置 kubectl 访问集群初始化完成后,kubeadm 会给出三行提示,让你把 kubeconfig 拷到当前用户目录下:mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config这一步不做的话,kubectl 默认找不到集群,执行任何命令都会报connection refused或者unable to connect。在 root 用户下,很多人习惯直接用环境变量:export KUBECONFIG=/etc/kubernetes/admin.conf两种方式都可以。但我建议把 kubeconfig 放到$HOME/.kube/config,因为后续 kubectl 的各种插件和工具都会默认读这个位置,省得每次开终端都要 export。4.2 安装 Calico 网络插件控制面初始化成功不等于集群可用。这时候kubectl get nodes会发现 master 节点的状态是NotReady,这是正常的,因为没有安装 CNI 网络插件,Node 的 network readiness 检查一直无法通过。安装 Calico 的方式有很多种,我习惯用官方提供的 manifest 文件。到一个有外网访问能力的机器上下载对应版本的 calico.yaml,然后拷贝到集群节点上执行:kubectl apply -f calico.yamlCalico 的镜像需要从 Docker Hub 拉取,离线环境下一样会拉不下来。如果你走的是离线导入方案,记得把 Calico 相关的镜像也一并导出导入到所有节点,否则 Calico Pod 会一直停留在ImagePullBackOff。装完之后,可以看 Pod 状态:kubectl get pods -n kube-system等待calico-node和calico-kube-controllers都变成Running,再执行kubectl get nodes,你会发现节点状态变成了Ready。这里需要一点耐心,CNI 组件从启动到就绪通常需要几十秒到几分钟。4.3 单节点集群是否需要移除 taint默认情况下,控制面节点带有一个node-role.kubernetes.io/control-plane的 taint,普通工作负载不会调度到 master 节点上。如果你只是搭一个单节点的测试环境,希望 Pod 也能跑在 master 上,需要手动移除这个 taint:kubectl taint nodes --all node-role.kubernetes.io/control-plane-如果你后续一定会加入工作节点,这个操作可以跳过,保持控制面节点“只跑系统组件”的干净状态。很多人在单节点测试环境里忘了这一步,部署业务 Deployment 之后发现 Pod 一直处于Pending,其实就是 taint 导致的。5. 工作节点加入:预导镜像比现场拉取省心得多5.1 获取 join 命令控制面初始化成功时,kubeadm 会在输出末尾打印一段kubeadm join命令,包含了 token 和 CA 证书哈希。如果当时忘了保存,也可以随时重新生成:kubeadm token create --print-join-command拿到的命令大致长这样:kubeadm join 192.168.1.10:6443 --token 你的token --discovery-token-ca-cert-hash sha256:你的哈希token 默认有效期是 24 小时,过期之后重新生成即可。5.2 工作节点的准备工作工作节点需要安装和 master 相同版本的 kubeadm、kubelet,并且配置好 containerd。这里的重点还是镜像。kubeadm join本身不负责拉取业务镜像,但工作节点上的 kubelet、kube-proxy 依然会用到 Kubernetes 组件镜像。如果你走离线导入方案,我建议把 master 上预拉的那一批镜像 tar 包直接拷到工作节点,用同样的ctr -n k8s.io images import命令导入一遍。另外一个容易被忽略的是pause镜像。containerd 启动每个 Pod 之前需要先创建一个pause容器,这个镜像的地址由 containerd 配置里的sandbox_image决定。如果工作节点上缺失 pause 镜像,且无法访问远端镜像源,节点 join 成功后,调度到该节点的 Pod 会全部卡在ContainerCreating。还有一个细节:kube-proxy 是 DaemonSet,会在每个节点上以 Pod 形式运行,它的镜像拉取发生在工作节点本地。如果工作节点没有 kube-proxy 镜像,也没有配置 mirror,即使节点已经显示 Ready,kube-proxy 的 Pod 也会报ImagePullBackOff。这种问题最容易被忽略,因为节点状态看起来一切正常,实际上 Pod 网络转发完全不可用。5.3 验证节点加入结果在工作节点上执行kubeadm join之后,回到 master 节点执行:kubectl get nodes正常情况下,新节点会先处于NotReady,随后转为Ready。同时检查一次所有系统命名空间的 Pod:kubectl get pods -A重点关注 kube-system 下是否有非 Running 状态的 Pod。如果发现 kube-flannel 或者 calico-node 没起来,大概率是网络插件镜像缺失或者版本不兼容。逐个排查比最后统一排查轻松得多,我通常每加入一个工作节点就检查一次,不攒着。6. 初始化过程中最容易翻车的三个细节:cgroup、sandbox 镜像和版本漂移6.1 cgroup 驱动不匹配,kubelet 陷入重启循环这是新手踩坑率最高的问题。kubelet 默认使用systemd作为 cgroup 驱动,而 containerd 默认配置可能用的还是cgroupfs。两者不一致时,kubelet 启动会报类似failed to run Kubelet: misconfiguration: kubelet cgroup driver: "systemd" is different from docker cgroup driver: "cgroupfs"的错误。排查起来也很直接。containerd 配置文件里搜SystemdCgroup,把它设为true:[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] runtime_type = "io.containerd.runc.v2" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] SystemdCgroup = true实际操作中我发现一个更隐蔽的情况:配置文件里SystemdCgroup前面被注释掉了,表面上字段存在,实际上 containerd 还在用默认的false。所以改完配置后一定要执行containerd config dump检查一下生效值,再重启 containerd 和 kubelet。6.2 sandbox 镜像版本不一致导致 Pod 起不来Pod 创建的底层机制是先启动一个 sandbox 容器(pause 容器),再往里塞业务容器。如果 containerd 配置里的sandbox_image和 kubeadm 期望的 pause 镜像版本不一致,kubelet 会反复尝试拉取错误的镜像,Pod 一直处于ContainerCreating,事件里会看到FailedCreatePodSandBox。解决办法是让 containerd 的sandbox_image和 kubeadm 的 pause 版本保持统一。最直接的验证方式:kubeadm config images list --kubernetes-version=v1.28.2看列表里 pause 的 tag,然后对照 containerd 配置里的sandbox_image。如果不一样,以 kubeadm 列表里的为准修改 containerd 配置。这里不要想当然用最新版 pause,Kubernetes 版本和 pause 版本之间有一个兼容矩阵,跟着 kubeadm 走最安全。6.3 版本漂移:系统包源和集群配置不一致版本漂移这个问题比较隐蔽,常见于使用系统包管理器安装 kubeadm 的场景。你初始化配置里写的是v1.28.2,但是操作系统的软件源里 kubeadm 和 kubelet 已经自动升级到了 1.29.x。初始化过程中,kubeadm 二进制是 1.29 写的证书和配置,但是控制面镜像按照v1.28.2拉取,最后 kubelet 的二进制版本和镜像版本对不上,Node 会反复上报错误。解决思路是把软件源的版本固定住。以 apt 为例,安装时直接指定版本号:apt-get install -y kubeadm=1.28.2-00 kubelet=1.28.2-00 kubectl=1.28.2-00这样后续执行apt-get upgrade也不会把你带偏。如果集群已经装到一半发现版本对不上,先统一好二进制版本,再考虑是否重新初始化。我的建议是,发现版本漂移时不要硬着头皮继续,清理掉已有配置重新初始化一次,二十分钟能解决的事,继续排查可能要花两个小时。7. 初始化失败后的快速恢复路径不管前期准备做得多充分,初始化都有可能失败。失败本身不可怕,最怕的是“半初始化”状态残留,导致第二次初始化时报一些莫名其妙的冲突。我的习惯是准备一个一键清理脚本,内容大约如下:kubeadm reset -f rm -rf /etc/kubernetes /var/lib/kubelet /var/lib/etcd rm -rf /etc/cni/net.d iptables -F && iptables -t nat -F && iptables -t mangle -F systemctl restart containerd这个清理由重到轻,kubeadm reset负责回收组件,删除目录是清理证书和状态数据,清 iptables 是去掉残留的规则。清理完再检查一遍swapoff -a和ip link,确认网络设备也被释放干净。很多人第二次初始化失败,都是因为残留配置文件,比如/etc/kubernetes/admin.conf已经存在,kubeadm 会认为集群已经存在而拒绝执行。清理脚本跑一遍,基本能排除大部分杂音。8. 最后再分享一点个人体会这套流程我用过很多次,从 1.26 到 1.28 都跑通过。最大的感触是:kubeadm 安装集群,真正的难点从来不在 kubeadm 本身,而在于“镜像能不能到位”和“底层配置是否一致”这两件事。网络受限反而帮你把每一步都逼着走扎实,因为你没法靠“拉一个新的镜像”来绕过问题,只能老老实实把版本、cgroup、sandbox 这些细节对齐。如果你现在正准备搭一个指定版本的集群,我的建议是先花半小时做镜像准备,再花十分钟检查 cgroup 和版本一致性,最后再执行初始化。前面准备做得越充分,初始化那一遍就越顺利。千万别把时间花在反复kubeadm init和kubeadm reset上,那是效率最低的排错方式。
最近有个朋友问我,能不能在不折腾网络的前提下,用 kubeadm 装一个指定版本的 Kubernetes 集群。他试过几次kubeadm init,每次都在拉取registry.k8s.io这一步超时,要么就是预检阶段卡了几分钟直接报错。这个问题在受网络限制的环境里非常典型:kubeadm 本身设计简单,安装失败往往不是因为命令写错,而是组件镜像拿不到。这篇文章我会把一条不依赖“海外直连”、完全基于国内可见镜像仓库和离线镜像包导入的 kubeadm 安装路径完整写一遍。从版本对应关系、镜像准备、初始化配置,到工作节点加入和常见故障排查,全部覆盖。适合需要固定 Kubernetes 版本、打算部署测试环境或者建设内网集群的运维和开发同学参考。我尽量把每一步的“为什么这么做”也讲清楚,这样你换一个版本或者换一套网络环境,也能自己推导出正确做法。1. 动手前先把版本关系和高版本陷阱理清1.1 组件版本必须锁成一个整体很多人装集群喜欢“先装最新版”,或者随手apt install kubeadm,然后发现装出来的 kubelet 是 1.29,初始化配置却写的 1.28,部署出来控制面经常出现各种奇怪的兼容问题。Kubernetes 的版本策略是:kubelet 允许比 kube-apiserver 低一个小版本,但 kubeadm、kubelet、kubectl 三者最好保持同一个版本,控制面核心组件(apiserver、controller-manager、scheduler)也必须和 kubeadm 里指定的kubernetesVersion保持一致。在我实际部署中,最省心的做法是“先选 Kubernetes 版本,再反推工具版本”。比如我要装 1.28.x,就统一安装这三个二进制:kubeadm 1.28.xkubelet 1.28.xkubectl 1.28.x然后把容器运行时也固定下来。现在 containerd 1.7.x 搭配 Kubernetes 1.28 是非常成熟稳定的组合,比 Docker 加 cri-dockerd 少一层转换,配置也简单。建议不要在这个环节引入“最新稳定版”,网络环境本来就受限,没必要再给排查兼容性增加难度。这里有一个容易被忽略的细节:控制面的镜像 tag 里带着 Kubernetes 版本号,比如kube-apiserver:v1.28.2。如果你的 kubeadm 版本是 1.28.2,而kubernetesVersion写的是v1.28.2,kubeadm 才会去拉取这个精确的镜像。版本号必须完全匹配,少写一个.2,它会去拉一个不存在的 tag,直接报NotFound。1.2 节点环境准备清单我建议每台节点都提前完成下面这些基础工作,不要等到 kubeadm 预检报错了再回来补:操作系统:Ubuntu 22.04 或者 CentOS 7.9 以上都行,内核不要太老。资源要求:控制平面节点至少 2 核 2G,工作节点至少 2 核 2G,这是底线。关闭 swap:kubelet 默认不支持 swap 开启的环境,kubeadm init预检会直接提示失败。需要swapoff -a,并且把/etc/fstab里的 swap 条目注释掉,否则重启又回来了。加载内核模块:overlay和br_netfilter这两个必须启用,否则后续 Pod 网络和流量转发会有问题。设置系统参数:net.bridge.bridge-nf-call-iptables=1、net.ipv4.ip_forward=1这类参数建议写进/etc/sysctl.d/k8s.conf。这些操作本身没有难度,但值得花十分钟核对一遍。预检阶段是可以自动发现大部分问题的,真正麻烦的是预检通过之后,容器运行时和 kubelet 之间出现隐性问题,那种报错信息绕来绕去,排查成本高得多。2. 镜像获取的三种思路:mirror 配置、离线包导入与 imageRepository 替换Kubernetes 控制面启动时会用到一组固定镜像:kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy、etcd、coredns、pause。默认情况下,kubeadm 从registry.k8s.io这个地址下拉取它们。网络受限时问题就出在这里。2.1 方案一:为 containerd 配置镜像加速如果你有可用的国内公共镜像仓库,最顺滑的办法是把 containerd 配置成“从镜像仓库拉取官方镜像”,也就是 pull-through mirror。这样 kubeadm 和 kubelet 完全不用改,仍然写registry.k8s.io,containerd 在背后自动去配置的镜像源拉取。containerd 的配置文件默认在/etc/containerd/config.toml。如果你还没有生成过它,先执行containerd config default > /etc/containerd/config.toml,然后在配置里找到 registry.mirrors 部分,为registry.k8s.io增加一个 endpoint:[plugins."io.containerd.grpc.v1.cri".registry.mirrors] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."registry.k8s.io"] endpoint = ["https://你的镜像仓库地址"]注意修改完一定要重启 containerd:systemctl restart containerd。这个操作不会破坏已有镜像,可以放心做。这个方案对 kubeadm 完全透明,后续加节点、跑工作负载都不用再操心镜像地址。唯一的风险是,你选的镜像仓库必须持续可用,而且同步的镜像路径要和官方一致。我的习惯是先在命令行验证一下:用ctr -n k8s.io images pull registry.k8s.io/pause:3.9拉一次,能成功说明路径没问题。2.2 方案二:离线镜像包整体导入对于完全隔离内网的环境,mirror 方案也失效了,只能用“离线包”思路。流程很简单:在一台能访问镜像仓库的机器上,用ctr把需要的镜像全部导出成 tar 文件,拷到目标机器上,再用ctr导入到 containerd。获取镜像列表最稳妥的命令是:kubeadm config images list --kubernetes-version=v1.28.2这里不建议手动拼镜像名,因为很容易漏掉pause或者etcd。拿到列表后,逐条拉取并导出:ctr -n k8s.io images pull registry.k8s.io/kube-apiserver:v1.28.2 ctr -n k8s.io images export kube-apiserver.tar registry.k8s.io/kube-apiserver:v1.28.2在目标机器上导入:ctr -n k8s.io images import kube-apiserver.tar这里有两个关键点。第一,必须指定命名空间k8s.io,因为 containerd 的 CRI 插件默认使用k8s.io这个命名空间,kubelet 创建的容器镜像都在这里面。导入到别的命名空间,kubelet 根本看不到。第二,镜像的完整名称要保持不变,包括registry.k8s.io/这个前缀,否则 containerd 会认为这是另一个镜像,仍然尝试去远端拉取。这个方案对网络要求最低,也最可控。缺点是更新版本时要重新导出导入一遍,稍微麻烦一点。如果只是偶尔部署一次集群,我反而推荐这个方案,因为它把“能不能装上”和“网络好不好”彻底解耦了。2.3 方案三:直接替换 imageRepositorykubeadm 的初始化配置里有一个imageRepository字段,默认是registry.k8s.io。如果你用的是某个已经同步了 Kubernetes 官方镜像的国内仓库,可以直接把imageRepository改成那个仓库地址。kubeadm 拉取的镜像地址就会变成你的仓库地址/kube-apiserver:v1.28.2。需要注意路径问题。官方镜像路径是registry.k8s.io/kube-apiserver,很多镜像仓库并不会原样保留这个路径,而是放在类似你的仓库地址/google_containers/kube-apiserver的路径下。如果你的镜像仓库布局不一样,光改imageRepository是不够的,还要保证镜像的“仓库路径 + 镜像名”和 containerd 里实际存在的完整名称一致。最稳妥的办法是先用ctr -n k8s.io images pull 你的仓库地址/kube-apiserver:v1.28.2验证一遍,确认能拉下来再写进配置。2.4 我的选型建议经常有人问我“到底用哪个方案”。我的判断标准很简单:场景推荐方案理由测试环境 + 网络尚可mirror 配置改动最小,加节点最方便隔离内网离线包导入不依赖任何外部服务,绝对可控已有内部镜像仓库替换 imageRepository统一管理,后续版本升级路径清晰我个人最常用的是方案二,也就是离线包导入。原因很实在:它不依赖镜像仓库的稳定性,排障路径最短。出了任何镜像相关的问题,我只需要检查 containerd 的k8s.io命名空间里有没有对应的镜像,其他的不用考虑。3. kubeadm init 配置逐字段拆解:从生成配置到正式开始安装3.1 一个完整可用的初始化配置文件kubeadm 支持纯命令行参数,但版本多、参数多的时候很容易漏,而且不方便审计。我强烈建议提前写好一个 YAML 文件,所有节点都用同一套配置。下面是一个以 1.28.2 为例的配置:apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.1.10 bindPort: 6443 nodeRegistration: criSocket: unix:///var/run/containerd/containerd.sock name: master01 imagePullPolicy: IfNotPresent --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.28.2 imageRepository: registry.k8s.io networking: podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/12 --- apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration cgroupDriver: systemd三个文档块分别对应 InitConfiguration、ClusterConfiguration、KubeletConfiguration,用---分隔。如果你之前对 kubeadm 配置文件不熟,可以执行kubeadm config print init-defaults生成一份默认配置作为底稿,再照着上面把关键字段改掉就行。3.2 几个关键字段为什么要这么写先看advertiseAddress。它必须写当前节点的内网 IP,不能写 127.0.0.1,也不能写公网 IP,否则 kube-apiserver 通告给其他节点的地址不对,工作节点 join 之后会连不上 API Server。这是一个很常见但是报错信息不太直观的问题。criSocket指向/var/run/containerd/containerd.sock,这是 containerd 的标准 socket 路径。如果你用的是 Docker,这个值就要改成unix:///var/run/cri-dockerd.sock,但正如前面说的,我建议直接使用 containerd,省掉一层转换。imagePullPolicy: IfNotPresent这个字段容易被忽略。默认情况下,kubeadm 在某些版本组合中会尝试先拉取远端镜像,即使本地已经导入了一份。对于离线场景,显式设置成IfNotPresent可以强制 kubeadm 优先检查本地 containerd,这是一个很实用的细节,网上不少离线安装教程都会漏掉这一步,导致镜像明明导入了,预检仍然报拉取失败。podSubnet写10.244.0.0/16是 Calico 网络插件的惯例网段。并不是 kubeadm 强制要求这个值,而是 Calico 的默认配置期望在这个网段内分配 Pod IP。如果用其他网段,后面安装网络插件时要额外修改 IPPool 配置,容易出错。新手建议直接沿用这个网段。cgroupDriver: systemd必须和 containerd 的 cgroup 驱动一致。这个后面专门讲,这里先记住一个结论:Kubernetes 1.24 之后,官方推荐并且默认使用systemd作为 cgroup 驱动,containerd 也要配合启用SystemdCgroup = true。3.3 先拉镜像再初始化,避免预检卡死如果走 mirror 或离线导入方案,建议先手动预拉一遍镜像,确认镜像全部就绪,再执行初始化:kubeadm config images pull --config kubeadm-config.yaml这条命令会按照配置里的kubernetesVersion和imageRepository去拉取所有需要的镜像。它执行成功,说明镜像层面已经没有问题,接下来初始化大概率不会被卡住。如果网络确实不通,预拉会失败,没关系,只要你确认镜像已经通过ctr导入到k8s.io命名空间,同样可以直接进入初始化:kubeadm init --config kubeadm-config.yaml初始化过程会做很多事,包括生成证书、写入 kubeconfig、启动控制面静态 Pod。通常几分钟内会看到类似Your Kubernetes control-plane has initialized successfully的输出。看到这个输出,控制面节点就已经起来了。4. 控制平面起来之后,立刻要处理的网络插件和本机调度4.1 配置 kubectl 访问集群初始化完成后,kubeadm 会给出三行提示,让你把 kubeconfig 拷到当前用户目录下:mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config这一步不做的话,kubectl 默认找不到集群,执行任何命令都会报connection refused或者unable to connect。在 root 用户下,很多人习惯直接用环境变量:export KUBECONFIG=/etc/kubernetes/admin.conf两种方式都可以。但我建议把 kubeconfig 放到$HOME/.kube/config,因为后续 kubectl 的各种插件和工具都会默认读这个位置,省得每次开终端都要 export。4.2 安装 Calico 网络插件控制面初始化成功不等于集群可用。这时候kubectl get nodes会发现 master 节点的状态是NotReady,这是正常的,因为没有安装 CNI 网络插件,Node 的 network readiness 检查一直无法通过。安装 Calico 的方式有很多种,我习惯用官方提供的 manifest 文件。到一个有外网访问能力的机器上下载对应版本的 calico.yaml,然后拷贝到集群节点上执行:kubectl apply -f calico.yamlCalico 的镜像需要从 Docker Hub 拉取,离线环境下一样会拉不下来。如果你走的是离线导入方案,记得把 Calico 相关的镜像也一并导出导入到所有节点,否则 Calico Pod 会一直停留在ImagePullBackOff。装完之后,可以看 Pod 状态:kubectl get pods -n kube-system等待calico-node和calico-kube-controllers都变成Running,再执行kubectl get nodes,你会发现节点状态变成了Ready。这里需要一点耐心,CNI 组件从启动到就绪通常需要几十秒到几分钟。4.3 单节点集群是否需要移除 taint默认情况下,控制面节点带有一个node-role.kubernetes.io/control-plane的 taint,普通工作负载不会调度到 master 节点上。如果你只是搭一个单节点的测试环境,希望 Pod 也能跑在 master 上,需要手动移除这个 taint:kubectl taint nodes --all node-role.kubernetes.io/control-plane-如果你后续一定会加入工作节点,这个操作可以跳过,保持控制面节点“只跑系统组件”的干净状态。很多人在单节点测试环境里忘了这一步,部署业务 Deployment 之后发现 Pod 一直处于Pending,其实就是 taint 导致的。5. 工作节点加入:预导镜像比现场拉取省心得多5.1 获取 join 命令控制面初始化成功时,kubeadm 会在输出末尾打印一段kubeadm join命令,包含了 token 和 CA 证书哈希。如果当时忘了保存,也可以随时重新生成:kubeadm token create --print-join-command拿到的命令大致长这样:kubeadm join 192.168.1.10:6443 --token 你的token --discovery-token-ca-cert-hash sha256:你的哈希token 默认有效期是 24 小时,过期之后重新生成即可。5.2 工作节点的准备工作工作节点需要安装和 master 相同版本的 kubeadm、kubelet,并且配置好 containerd。这里的重点还是镜像。kubeadm join本身不负责拉取业务镜像,但工作节点上的 kubelet、kube-proxy 依然会用到 Kubernetes 组件镜像。如果你走离线导入方案,我建议把 master 上预拉的那一批镜像 tar 包直接拷到工作节点,用同样的ctr -n k8s.io images import命令导入一遍。另外一个容易被忽略的是pause镜像。containerd 启动每个 Pod 之前需要先创建一个pause容器,这个镜像的地址由 containerd 配置里的sandbox_image决定。如果工作节点上缺失 pause 镜像,且无法访问远端镜像源,节点 join 成功后,调度到该节点的 Pod 会全部卡在ContainerCreating。还有一个细节:kube-proxy 是 DaemonSet,会在每个节点上以 Pod 形式运行,它的镜像拉取发生在工作节点本地。如果工作节点没有 kube-proxy 镜像,也没有配置 mirror,即使节点已经显示 Ready,kube-proxy 的 Pod 也会报ImagePullBackOff。这种问题最容易被忽略,因为节点状态看起来一切正常,实际上 Pod 网络转发完全不可用。5.3 验证节点加入结果在工作节点上执行kubeadm join之后,回到 master 节点执行:kubectl get nodes正常情况下,新节点会先处于NotReady,随后转为Ready。同时检查一次所有系统命名空间的 Pod:kubectl get pods -A重点关注 kube-system 下是否有非 Running 状态的 Pod。如果发现 kube-flannel 或者 calico-node 没起来,大概率是网络插件镜像缺失或者版本不兼容。逐个排查比最后统一排查轻松得多,我通常每加入一个工作节点就检查一次,不攒着。6. 初始化过程中最容易翻车的三个细节:cgroup、sandbox 镜像和版本漂移6.1 cgroup 驱动不匹配,kubelet 陷入重启循环这是新手踩坑率最高的问题。kubelet 默认使用systemd作为 cgroup 驱动,而 containerd 默认配置可能用的还是cgroupfs。两者不一致时,kubelet 启动会报类似failed to run Kubelet: misconfiguration: kubelet cgroup driver: "systemd" is different from docker cgroup driver: "cgroupfs"的错误。排查起来也很直接。containerd 配置文件里搜SystemdCgroup,把它设为true:[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] runtime_type = "io.containerd.runc.v2" [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] SystemdCgroup = true实际操作中我发现一个更隐蔽的情况:配置文件里SystemdCgroup前面被注释掉了,表面上字段存在,实际上 containerd 还在用默认的false。所以改完配置后一定要执行containerd config dump检查一下生效值,再重启 containerd 和 kubelet。6.2 sandbox 镜像版本不一致导致 Pod 起不来Pod 创建的底层机制是先启动一个 sandbox 容器(pause 容器),再往里塞业务容器。如果 containerd 配置里的sandbox_image和 kubeadm 期望的 pause 镜像版本不一致,kubelet 会反复尝试拉取错误的镜像,Pod 一直处于ContainerCreating,事件里会看到FailedCreatePodSandBox。解决办法是让 containerd 的sandbox_image和 kubeadm 的 pause 版本保持统一。最直接的验证方式:kubeadm config images list --kubernetes-version=v1.28.2看列表里 pause 的 tag,然后对照 containerd 配置里的sandbox_image。如果不一样,以 kubeadm 列表里的为准修改 containerd 配置。这里不要想当然用最新版 pause,Kubernetes 版本和 pause 版本之间有一个兼容矩阵,跟着 kubeadm 走最安全。6.3 版本漂移:系统包源和集群配置不一致版本漂移这个问题比较隐蔽,常见于使用系统包管理器安装 kubeadm 的场景。你初始化配置里写的是v1.28.2,但是操作系统的软件源里 kubeadm 和 kubelet 已经自动升级到了 1.29.x。初始化过程中,kubeadm 二进制是 1.29 写的证书和配置,但是控制面镜像按照v1.28.2拉取,最后 kubelet 的二进制版本和镜像版本对不上,Node 会反复上报错误。解决思路是把软件源的版本固定住。以 apt 为例,安装时直接指定版本号:apt-get install -y kubeadm=1.28.2-00 kubelet=1.28.2-00 kubectl=1.28.2-00这样后续执行apt-get upgrade也不会把你带偏。如果集群已经装到一半发现版本对不上,先统一好二进制版本,再考虑是否重新初始化。我的建议是,发现版本漂移时不要硬着头皮继续,清理掉已有配置重新初始化一次,二十分钟能解决的事,继续排查可能要花两个小时。7. 初始化失败后的快速恢复路径不管前期准备做得多充分,初始化都有可能失败。失败本身不可怕,最怕的是“半初始化”状态残留,导致第二次初始化时报一些莫名其妙的冲突。我的习惯是准备一个一键清理脚本,内容大约如下:kubeadm reset -f rm -rf /etc/kubernetes /var/lib/kubelet /var/lib/etcd rm -rf /etc/cni/net.d iptables -F && iptables -t nat -F && iptables -t mangle -F systemctl restart containerd这个清理由重到轻,kubeadm reset负责回收组件,删除目录是清理证书和状态数据,清 iptables 是去掉残留的规则。清理完再检查一遍swapoff -a和ip link,确认网络设备也被释放干净。很多人第二次初始化失败,都是因为残留配置文件,比如/etc/kubernetes/admin.conf已经存在,kubeadm 会认为集群已经存在而拒绝执行。清理脚本跑一遍,基本能排除大部分杂音。8. 最后再分享一点个人体会这套流程我用过很多次,从 1.26 到 1.28 都跑通过。最大的感触是:kubeadm 安装集群,真正的难点从来不在 kubeadm 本身,而在于“镜像能不能到位”和“底层配置是否一致”这两件事。网络受限反而帮你把每一步都逼着走扎实,因为你没法靠“拉一个新的镜像”来绕过问题,只能老老实实把版本、cgroup、sandbox 这些细节对齐。如果你现在正准备搭一个指定版本的集群,我的建议是先花半小时做镜像准备,再花十分钟检查 cgroup 和版本一致性,最后再执行初始化。前面准备做得越充分,初始化那一遍就越顺利。千万别把时间花在反复kubeadm init和kubeadm reset上,那是效率最低的排错方式。