kubeadm离线安装Kubernetes集群:镜像准备与常见故障排查
最近有个朋友问我能不能在不折腾网络的前提下用 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.2kubeadm 才会去拉取这个精确的镜像。版本号必须完全匹配少写一个.2它会去拉一个不存在的 tag直接报NotFound。1.2 节点环境准备清单我建议每台节点都提前完成下面这些基础工作不要等到 kubeadm 预检报错了再回来补操作系统Ubuntu 22.04 或者 CentOS 7.9 以上都行内核不要太老。资源要求控制平面节点至少 2 核 2G工作节点至少 2 核 2G这是底线。关闭 swapkubelet 默认不支持 swap 开启的环境kubeadm init预检会直接提示失败。需要swapoff -a并且把/etc/fstab里的 swap 条目注释掉否则重启又回来了。加载内核模块overlay和br_netfilter这两个必须启用否则后续 Pod 网络和流量转发会有问题。设置系统参数net.bridge.bridge-nf-call-iptables1、net.ipv4.ip_forward1这类参数建议写进/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.iocontainerd 在背后自动去配置的镜像源拉取。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://你的镜像仓库地址]注意修改完一定要重启 containerdsystemctl 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-versionv1.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 上需要手动移除这个 taintkubectl 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即使节点已经显示 Readykube-proxy 的 Pod 也会报ImagePullBackOff。这种问题最容易被忽略因为节点状态看起来一切正常实际上 Pod 网络转发完全不可用。5.3 验证节点加入结果在工作节点上执行kubeadm join之后回到 master 节点执行kubectl get nodes正常情况下新节点会先处于NotReady随后转为Ready。同时检查一次所有系统命名空间的 Podkubectl 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-versionv1.28.2看列表里 pause 的 tag然后对照 containerd 配置里的sandbox_image。如果不一样以 kubeadm 列表里的为准修改 containerd 配置。这里不要想当然用最新版 pauseKubernetes 版本和 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 kubeadm1.28.2-00 kubelet1.28.2-00 kubectl1.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上那是效率最低的排错方式。