资讯动态

离线环境使用 kubeadm 安装 Kubernetes 集群实战指南

发布时间:2026/10/15 13:24:32 来源:尧图企业网站定制
1. 为什么内网环境还要折腾 kubeadm很多人第一次听到“离线安装 Kubernetes”会觉得多此一举——现在容器镜像拉取这么方便一条kubeadm init不就搞定了但真正在生产环境待过的人都知道大量企业内网、专有云、边缘机房是不允许直接访问外网镜像仓库的。尤其是金融、制造、能源这类行业网络隔离是硬性要求服务器连公网 DNS 都解析不了更别提去registry.k8s.io拉镜像了。我最早接触离线部署是在一个边缘计算项目里现场只有一台跳板机能连外网其余十几台节点全是纯内网。当时第一反应是“把镜像docker save出来再load进去不就行了”结果一上手才发现坑远比想象的多kubeadm 初始化时会去拉一堆控制面镜像版本号还得和 kubeadm 二进制严格对应CNI 插件、CoreDNS、etcd 这些组件的镜像散落在不同仓库节点加入集群时 kubelet 还要拉 pause 镜像。任何一个环节缺镜像整个集群就卡在NotReady或者CrashLoopBackOff。所以这篇内容我想把“离线使用 kubeadm 装 K8s”这件事从头到尾讲透。它适合两类人一是需要在隔离网络里搭建集群的运维和平台工程师二是想搞明白 kubeadm 到底在背后做了哪些事、依赖了哪些镜像的学习者。我会把镜像清单怎么来、离线包怎么打、初始化怎么改参数、节点怎么加入、出问题怎么排查全部按我实际操作的顺序讲一遍。你照着做基本能在一个完全断网的环境里把集群跑起来。需要先说明一点Kubernetes 版本迭代很快镜像仓库地址、组件命名在不同版本间有变化。我下面以较通用的 1.2x 系列为例具体版本号你替换成自己用的即可思路是通用的。2. 离线部署前必须想清楚的几件事2.1 版本对齐kubeadm、kubelet、kubectl 三者不能乱离线环境最怕的就是版本错配。kubeadm 在初始化时会根据自身版本去推导需要哪些控制面镜像比如kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy、etcd、pause这些它们的 tag 通常和 kubeadm 版本一致etcd 和 pause 有独立版本号。如果你 kubeadm 是 1.24却准备了 1.26 的镜像初始化时就会报找不到镜像。我的做法是先确定一个目标版本然后所有二进制、镜像、CNI 插件全部围绕这个版本准备。三个核心二进制kubeadm、kubelet、kubectl版本必须完全一致差一个小版本都可能出问题。你可以用下面命令确认kubeadm version -o short kubelet --version kubectl version --client --short三个输出应该指向同一个v1.x.y。如果对不上宁可重新下二进制也别抱着“差不多能用”的心态离线环境排查版本问题非常痛苦。2.2 镜像清单从哪来kubeadm config images list 是钥匙很多人不知道 kubeadm 自带一个“告诉我需要哪些镜像”的命令这是离线准备的核心工具kubeadm config images list --kubernetes-version v1.24.0它会输出类似这样的清单registry.k8s.io/kube-apiserver:v1.24.0 registry.k8s.io/kube-controller-manager:v1.24.0 registry.k8s.io/kube-scheduler:v1.24.0 registry.k8s.io/kube-proxy:v1.24.0 registry.k8s.io/pause:3.7 registry.k8s.io/etcd:3.5.3-0 registry.k8s.io/coredns/coredns:v1.8.6这份清单就是你的“采购单”。注意--kubernetes-version一定要写全不写的话它会用默认版本可能和你实际要装的不一致。拿到清单后在一台能联网的机器上把这些镜像逐个pull下来再save成 tar 包。2.3 网络与主机名离线环境更容易忽略的基础项离线不代表可以跳过基础配置。相反因为没法临时上网查资料基础项更要一次做对。几个必须提前处理的点主机名唯一且可解析每个节点的 hostname 不能重复且要在/etc/hosts里互相能解析。kubeadm 加入集群时靠主机名通信解析不了直接失败。关闭 swapkubelet 默认不允许 swap 开启swapoff -a并注释掉/etc/fstab里的 swap 行。内核参数br_netfilter模块要加载net.bridge.bridge-nf-call-iptables和ip_forward要设为 1否则 Pod 网络不通。时间同步内网如果有 NTP 服务器就对上没有的话至少保证各节点时间差在几秒内否则证书校验和 etcd 选举会出问题。这些配置我在联网环境经常偷懒用脚本一把梭但离线环境建议手动逐条确认因为一旦集群起来后发现问题回退成本很高。3. 制作离线镜像包与二进制包3.1 用一台“中转机”批量拉取并打包镜像中转机就是那台能连外网的机器架构要和目标节点一致都是 x86_64 或都是 arm64。我习惯写一个脚本批量处理避免手敲出错#!/bin/bash IMAGES( registry.k8s.io/kube-apiserver:v1.24.0 registry.k8s.io/kube-controller-manager:v1.24.0 registry.k8s.io/kube-scheduler:v1.24.0 registry.k8s.io/kube-proxy:v1.24.0 registry.k8s.io/pause:3.7 registry.k8s.io/etcd:3.5.3-0 registry.k8s.io/coredns/coredns:v1.8.6 ) for img in ${IMAGES[]}; do docker pull $img done docker save -o k8s-images.tar ${IMAGES[]}这里有个细节docker save后面跟多个镜像名打出来是一个合并的 tar 包比每个镜像单独 save 再传要省事。tar 包大小通常在 1GB 上下取决于版本和是否包含 CNI 镜像。CNI 插件镜像别忘了一起打。以 Calico 为例它涉及calico/node、calico/cni、calico/kube-controllers等多个镜像具体清单看你的 CNI 选型和对应版本的部署 yaml。我一般会把 CNI 的 yaml 文件也一起下载好因为离线环境改 yaml 里的镜像地址是必须的。3.2 二进制包kubeadm、kubelet、kubectl 一个都不能少镜像之外三个二进制也要离线准备。可以从官方提供的二进制包或者系统包管理器缓存里拿。我倾向于直接用二进制因为不依赖发行版的包管理跨系统通用性好。# 在中转机下载 curl -LO https://dl.k8s.io/v1.24.0/bin/linux/amd64/kubeadm curl -LO https://dl.k8s.io/v1.24.0/bin/linux/amd64/kubelet curl -LO https://dl.k8s.io/v1.24.0/bin/linux/amd64/kubectl chmod x kubeadm kubelet kubectl传到目标节点后放到/usr/local/bin/下。kubelet 还需要一个 systemd 服务文件这个文件内容比较长建议从中转机上已经装好的环境里直接拷贝/usr/lib/systemd/system/kubelet.service或者从官方仓库里取。没有这个 service 文件kubelet 起不来集群节点永远是 NotReady。3.3 打包与传输校验和一定要做把所有东西归到一个目录打个总包tar czvf k8s-offline-v1.24.0.tar.gz \ k8s-images.tar \ kubeadm kubelet kubectl \ kubelet.service \ calico.yaml传输前算个校验和传到目标机后再算一次对比sha256sum k8s-offline-v1.24.0.tar.gz别嫌这一步麻烦。离线包动辄上 G传输过程中损坏的概率不低如果没校验到了现场解压报错或者镜像 load 失败你连问题出在传输还是打包都分不清。我踩过一次tar 包传了一半docker load报 unexpected EOF排查了半天才发现是传输问题。4. 控制面节点的初始化实操4.1 导入镜像与放置二进制在控制面节点上先解包、导入镜像tar xzvf k8s-offline-v1.24.0.tar.gz docker load -i k8s-images.tar导入完用docker images确认一下清单里的镜像应该都在。然后把三个二进制放到/usr/local/bin/service 文件放到/usr/lib/systemd/system/执行systemctl daemon-reload。这里有个容易忽略的点kubelet 的 service 文件里可能引用了/etc/kubernetes/kubelet.conf等配置这些配置是 kubeadm init 之后才生成的。所以顺序是先放好二进制和 service 文件但先别急着systemctl start kubelet等 kubeadm init 跑完它会自动拉起。4.2 编写 kubeadm 配置文件指定离线镜像仓库离线环境的关键在于告诉 kubeadm“别去外网拉镜像用我本地的”。有两种做法一是把镜像重新 tag 成 kubeadm 期望的地址二是用配置文件指定imageRepository。我更推荐第二种干净且可追溯。新建kubeadm-config.yamlapiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.1.10 bindPort: 6443 nodeRegistration: name: master-01 criSocket: unix:///var/run/dockershim.sock --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.24.0 imageRepository: registry.k8s.io controlPlaneEndpoint: 192.168.1.10:6443 networking: podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/12imageRepository这里填什么取决于你镜像的 tag。如果你docker load进来的镜像名就是registry.k8s.io/xxx那这里保持原样即可kubeadm 会优先用本地已有的镜像不会去拉。如果本地镜像名不同要么改 tag要么把imageRepository改成你本地的命名前缀。注意criSocket这一项在 1.24 之后变化较大因为 dockershim 被移除了。如果你用的是 containerd这里要写unix:///run/containerd/containerd.sock。用错 socket 是初始化失败的高频原因。4.3 执行 init 并观察真实报错配置写好就可以初始化了kubeadm init --config kubeadm-config.yaml --upload-certs--upload-certs是为了后续加入其他控制面节点时能自动分发证书单控制面可以不加。初始化过程中如果卡住多半是镜像问题。这时候别慌看 kubelet 日志journalctl -xeu kubelet -n 100常见报错和对应原因我整理成表报错关键词大概率原因处理方式failed to pull image本地没有对应镜像检查镜像 tag 与 kubeadm 期望是否一致connection refusedcriSocket 配错确认容器运行时 socket 路径port 6443 already in use端口被占检查是否有残留进程或重复 initswap is onswap 没关swapoff -a并改 fstabhostname not resolvablehosts 没配补/etc/hosts初始化成功后按提示配置 kubectlmkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config然后kubectl get nodes应该能看到控制面节点状态是NotReady——这是正常的因为还没装 CNI。5. 网络插件与工作节点的接入5.1 离线安装 CNI改 yaml 里的镜像地址是核心CNI 是让节点从 NotReady 变 Ready 的关键。以 Calico 为例官方 yaml 里的镜像地址是外网的离线环境必须替换成本地已有的镜像名。操作步骤在中转机下载对应版本的calico.yaml。用sed或手动把里面所有image:字段改成你本地docker load进来的镜像名。把改好的 yaml 传到控制面节点kubectl apply -f calico.yaml。kubectl apply -f calico.yaml kubectl get pods -n kube-system -w盯着calico-node和calico-kube-controllers的 Pod等它们都 Running再kubectl get nodes控制面节点应该变 Ready 了。这里有个经验Calico 的 Pod 如果一直Init:0/1或者CrashLoopBackOff八成是镜像没找到或者网卡名不对。Calico 默认自动探测网卡多网卡机器上可能探测错需要在 yaml 里加IP_AUTODETECTION_METHOD环境变量指定网卡。5.2 工作节点加入join 命令与镜像预导入控制面 Ready 后kubeadm init输出的最后一行就是 join 命令形如kubeadm join 192.168.1.10:6443 --token xxxx \ --discovery-token-ca-cert-hash sha256:xxxx工作节点加入前同样要先把镜像 load 进去、二进制放好、基础配置做完。join 过程中 kubelet 会拉pause镜像和kube-proxy镜像如果本地没有就会卡住。所以工作节点也要导入完整的镜像包不能只导控制面镜像。join 成功后回到控制面kubectl get nodes新节点会先 NotReady等 CNI 的 Pod 在该节点起来后变 Ready。如果长时间 NotReady去工作节点看 kubelet 日志通常是镜像或网络问题。5.3 验证集群别只看节点状态节点 Ready 只是第一步。我习惯做几个验证kubectl get pods -n kube-system kubectl run test-nginx --imagenginx --port80 kubectl get pods -o wide如果test-nginx能 Running 且分布在不同节点说明调度和网络基本通了。再进一步可以测跨节点 Pod 通信用kubectl exec进一个 Pod ping 另一个 Pod 的 IP。这一步能验证 CNI 是否真的工作而不只是 Pod 起来了。6. 离线环境里那些容易翻车的细节6.1 镜像 tag 与 kubeadm 期望不一致这是最高频的坑。kubeadm 内部硬编码了镜像名和 tag比如它期望registry.k8s.io/kube-apiserver:v1.24.0你本地 load 进来的是k8s.gcr.io/kube-apiserver:v1.24.0老版本仓库地址名字对不上kubeadm 就会尝试去拉然后失败。解决办法有两个一是docker tag重命名把本地镜像改成 kubeadm 期望的名字二是在配置文件里用imageRepository统一前缀。我一般用第二种因为改一处就行不用逐个 tag。但要注意imageRepository只改前缀tag 部分还是 kubeadm 决定的所以镜像的 tag 必须和 kubeadm 期望的完全一致。6.2 证书有效期与时间不同步离线环境经常没有可靠的时间源节点时间一乱证书校验就失败。kubeadm 生成的证书默认有效期一年如果节点时间跳到证书生效之前或过期之后API Server 直接起不来。我的做法是部署前用date确认所有节点时间一致有条件就配个内网 NTP。另外kubeadm init时可以加--certificate-validity参数延长证书时间减少后期维护频率。6.3 容器运行时选型containerd 与 docker 的取舍1.24 之后 dockershim 被移除新集群建议直接用 containerd。但离线环境里 containerd 的镜像导入方式和 docker 不同docker load的 tar 包 containerd 不能直接用需要用ctr命令ctr -n k8s.io images import k8s-images.tar注意-n k8s.io这个命名空间containerd 给 K8s 用的镜像都在这个 namespace 下不加的话 kubelet 找不到。这个细节坑过很多人镜像明明导入了kubelet 却说没有。6.4 离线包的版本管理集群不是装完就不管了后续升级、扩容都要用到离线包。我建议按版本建目录每个版本一个完整的包里面附一个manifest.txt记录包含哪些镜像、二进制版本、CNI 版本。这样半年后回头看能立刻知道当时装的是什么不用去猜。7. 我踩过的几个真实坑与排查思路7.1 初始化卡在 etcd 起不来有一次kubeadm init卡了很久最后报 etcd 健康检查失败。看 etcd 容器日志发现是数据目录权限问题——之前有人手动在/var/lib/etcd下建过文件属主不对。清理掉重新 init 就好了。这个坑的教训是离线环境初始化前把/var/lib/etcd、/etc/kubernetes这些目录清干净别留历史残留。7.2 节点加入后 Pod 跨节点不通节点都 Ready 了但跨节点 Pod 互相 ping 不通。排查发现是防火墙没放行 CNI 用的协议。Calico 默认用 IPIP 或 VXLAN需要放行对应协议IPIP 是协议号 4VXLAN 是 UDP 4789。离线环境防火墙策略往往很严部署前一定要和网络团队确认放行规则否则集群看起来正常实际网络是断的。7.3 kube-proxy 反复重启kube-proxy 的 Pod 一直重启日志报Failed to load kernel module ip_vs。这是因为节点内核没加载 IPVS 相关模块而 kube-proxy 配置成了 IPVS 模式。解决方式是modprobe加载模块或者把 kube-proxy 改回 iptables 模式。离线环境改配置要重新 apply所以最好在初始化前就确认好 kube-proxy 的模式。7.4 排查思路的通用套路离线环境排查问题我的通用套路是先看 Pod 状态再看 describe最后看日志。kubectl get pods -n kube-system kubectl describe pod pod-name -n kube-system kubectl logs pod-name -n kube-system如果是节点层面的问题直接上节点看journalctl -xeu kubelet。离线环境没有外网查资料所以平时要把常见报错和对应处理记在自己的笔记里现场才能快速定位。8. 关于离线部署的一点个人体会做了几次离线集群之后我最大的感受是离线部署的难点不在技术而在准备。联网环境里一条命令能解决的事离线环境要拆成“下载、打包、传输、导入、配置”五步每一步都可能出问题。所以我现在做离线部署会先列一个 checklist把镜像、二进制、CNI、基础配置、防火墙规则全部列出来逐项打勾缺一项都不开始 init。另一个体会是离线包要当成“资产”来管理。每次部署完把这次用的完整包和配置文件归档标注版本和日期。下次扩容或者升级直接拿对应的包不用重新从中转机拉一遍。这个习惯帮我省了很多重复劳动也避免了“上次用的哪个版本”这种低级问题。最后说个实用技巧如果条件允许在中转机上先用同样的配置跑一遍联网部署确认配置文件和镜像清单没问题再打包搬到离线环境。这样能把大部分配置错误提前暴露在联网环境里离线现场只处理传输和导入的问题效率高很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑