刚把一套三节点的 Kubernetes 集群在完全离线、不能访问任何外部网络的内网环境里跑了起来,从准备离线物料、自建本地镜像仓库,到 kubeadm init 完成初始化、再补上网络插件和完整功能测试,整个过程踩了不少坑,也沉淀了一份可以直接照抄的操作笔记。这篇内容就围绕“kubeadm 离线部署 Kubernetes 集群 + 完整测试”展开,写给那些正在被内网环境、生产安全策略或离线学习环境困扰的运维和开发同学,希望能帮你避开我走过的弯路。
这次实践记录于 2026 年 1 月 17 日,文章里所有操作步骤、参数选型、测试用例,都是当时在真实环境里跑过、验证过的。我没有用任何公网下载源,所有 rpm 包和容器镜像都是提前在另一台有网机器上准备好、再拷贝进内网的。整个集群从裸机到可用,大概花了一下午时间,其中有将近一半时间花在“物料准备”和“排障”上,所以这篇文章的重心也会放在这两个容易被忽略的关键环节。
1. 离线部署的适用场景与整体思路
1.1 哪些环境逼着你必须用离线部署
很多人一听到“离线部署”就以为只是把安装包下载好、传进去、再 install 一下,其实真正的离线环境远没有那么简单。我在实际项目里遇到的几种典型场景:
- 生产内网隔离环境:这类网络通常与外部互联网物理隔离,或者只是开放了非常有限的访问白名单。你在里面有服务器权限,但没法执行
yum install或docker pull。 - 数据安全要求高的行业环境:金融、医疗、制造业的机房,安全规范和审计要求非常严格,不允许生产节点主动向外发起连接。
- 云上的私有网络:即使云主机本身能访问公网,但安全组、路由策略或堡垒机限制,往往也让你没法直接拉取软件源和镜像仓库。
- 断网演练或应急恢复:比如容灾机房、临时搭建的演示环境,网络不可用但业务又必须尽快跑起来。
这些场景的共同特点是:你必须在没有外网的情况下,把一套完整的集群从零搭起来,并且保证它能稳定提供计算能力。 离线部署不是炫技,而是这类环境下的唯一选择。
1.2 为什么偏偏选 kubeadm
Kubernetes 的部署工具目前主流的有 kubeadm、二进制手动部署、第三方工具(比如某商业发行版、Rancher、Kubesphere 等)。我在离线场景里优先选择 kubeadm,主要有几个非常实际的考量:
- 官方维护,参数稳定:kubeadm 是 Kubernetes 社区官方的集群引导工具,init、join、token 管理、证书轮换这些操作都有成熟标准,不容易踩到野路子的坑。
- 物料可控:kubeadm 本身依赖的组件非常清晰,无非就是
kubelet、kubectl、kubeadm三个 rpm 包,再加上containerd容器运行时,以及若干镜像。我提前在一台有网机器上把它们全部拉下来,打成离线包,再分发到内网节点,整个过程完全可复现。 - 证书和管理机制透明:kubeadm 默认生成的 etcd、kube-apiserver、kubelet 等证书有效期是一年,后续可以用
kubeadm certs renew统一续期,对于内网运维来说非常友好。 - 灵活性高:可以不装任何额外组件,自管 CNI 插件、自管负载均衡,完全按需裁剪。
相比之下,二进制手动部署虽然也能实现,但工作量和出错概率都成倍增加;而商业发行版虽然开箱即用,但在离线模式下通常需要导入整套厂商制品,反而更重。所以我的结论很直接:离线场景下,kubeadm 是性价比最高的方案。
1.3 离线部署的前置环境清单
动手之前,先列一份环境清单,避免进入实操环节才发现缺东西。我这里以三节点集群为例:一台 master,两台 worker,操作系统均为 CentOS 7.9(内核 3.10),CPU 建议 4 核以上,内存建议 master 8GB、worker 4GB 以上,磁盘剩余空间建议至少 30GB。所有节点在同一个二层网络内,关闭防火墙或放行相关端口,关闭 swap。
| 节点角色 | 主机名建议 | 配置要求 | 数量 |
|---|---|---|---|
| Master | k8s-master | 8C16G / 100G | 1 |
| Worker | k8s-worker-01 / 02 | 4C8G / 100G | 2 |
| 镜像仓库(可复用 master) | k8s-registry | 2C4G / 50G | 1 |
另外,在内网里需要一个可用的镜像仓库。这个仓库不一定要单独一台机器,也可以直接装在 master 节点上,通过 5000 端口对外提供服务,我就是这台机器上同时开了仓库服务和 master 角色。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 离线资源准备:版本选型与镜像RPM包收藏
这一章是整个离线部署的“粮草先行”阶段。如果这一步没做扎实,后面 kubeadm init 会卡在各种镜像拉取失败、依赖缺失的问题上,而且在内网环境里根本没有办法临时去下载,只能干瞪眼。
2.1 版本选型:求稳不求新
Kubernetes 版本更新节奏很快,但离线环境最大的特点就是“不能随便升级”。所以版本选型必须遵循一个原则:选择当时社区中长期维护的稳定版本,而不是最新版本。 我选的是 v1.30.6(以当时的最新稳定发行线为例,换成 1.31、1.32 系列也可以,流程完全一致)。
版本选型时要注意三个点:
- kubeadm / kubelet / kubectl 三个组件的版本必须一致,不能一个小版本里混用。
- containerd 与 Kubernetes 的兼容性:建议使用 containerd 1.7 系列,它一直是 Kubernetes 官方推荐的运行时之一,对 CRI 的支持非常稳定。
- CNI 插件的版本与内核兼容性:比如 Calico 3.28 版本对内核版本有要求,太老的内核可能跑不起 eBPF 模式或某些网络策略,需要提前查清楚。
我的版本组合是:Kubernetes v1.30.6 + containerd 1.7.23 + Calico v3.28.2。这个组合在当时的社区实践中验证过很多次,稳定性非常好。
2.2 准备 RPM 离线包(yumdownloader)
在有网的一台机器上,用 yumdownloader 工具配合本地 yum 源,把需要的 rpm 包全部下载下来。这里要特别注意的是,kubelet 依赖一些子包(比如 kubernetes-cni、cri-tools 等),如果只下载主包,在内网安装时会卡在依赖报错上。
我建议用 yumdownloader --resolve 参数递归解析依赖,把主包和所有依赖包全部下载到同一目录。
bash复制# 在有网机器上配置 Kubernetes 官方 yum 源(这里以 v1.30 为例)
cat <<EOF > /etc/yum.repos.d/kubernetes.repo
[kubernetes]
name=Kubernetes
baseurl=https://pkgs.k8s.io/core:/stable:/v1.30/rpm/
enabled=1
gpgcheck=0
EOF
# 下载 kubeadm、kubelet、kubectl 及其依赖
mkdir /root/offline-rpm
cd /root/offline-rpm
yumdownloader --resolve kubeadm-1.30.6 kubelet-1.30.6 kubectl-1.30.6
说明一点:某些发行版的 yumdownloader 没有 --resolve 参数时,也可以改用 repotrack 命令,效果类似。下载完成后检查目录里文件是否齐全,再用 sha256sum 生成校验清单,防止拷贝过程中文件损坏。
另外,containerd 的 rpm 包可以从 Docker 官方源拉取,或者用 containerd.io 包名下载,示例:
bash复制yumdownloader --resolve containerd.io
这样离线 rpm 目录里就有大约二十几个文件,包含了 kubernetes 组件、cni 插件、containerd 以及所有依赖库。这一步完成后,将这个目录打包拷贝到内网所有节点上。
2.3 准备容器镜像:先在一个有网的机器上攒齐
Kubernetes 集群运行起来需要一组核心镜像,比如 kube-apiserver、kube-controller-manager、kube-scheduler、etcd、coredns、pause 等。如果内网完全离线,这些镜像必须在有网机器上提前拉取、导出、再传送进内网。
最方便的方法是先用 kubeadm config images list 查询所需镜像列表,然后逐个 docker pull 或 ctr pull,再 docker save 导出为一个或多个 tar 文件。推荐使用 kubeadm config images list --kubernetes-version=v1.30.6 来获取准确的镜像清单。
bash复制# 在有网机器上查询镜像清单
kubeadm config images list --kubernetes-version=v1.30.6
# 拉取所有核心镜像(假设安装了 docker)
kubeadm config images pull --kubernetes-version=v1.30.6
# 导出为单个 tar 文件
docker save $(kubeadm config images list --kubernetes-version=v1.30.6 | tr '\n' ' ') -o k8s-images-v1.30.6.tar
除了核心镜像,CNI 插件镜像也需要提前拉取。Calico(或 Flannel、Cilium)的镜像通常在十几个左右,需要根据你选择的插件版本单独准备。以 Calico 为例,至少要包含 calico-kube-controllers、calico-node、calico-cni 三个镜像,再加上 calico-apiserver 之类的扩展组件。
如果环境里没有 docker,也可以使用 ctr(containerd 的命令行工具)配合 --namespace=k8s.io 完成保存和导入。这一步完成后,把导出的 tar 文件拷贝进内网节点,建议每个节点都存放一份,避免后续每台节点单独去镜像仓库拉取时因为网络问题卡住。
2.4 自建内网镜像仓库:镜像导入与分发
镜像 tar 文件准备好了,但并不是直接在每个节点上导入这么简单。如果 worker 节点挂了或重启,本地 containerd 里的镜像可能丢失,到时候还要重新导入。更可靠的做法是:在内网搭一个私有镜像仓库,所有节点统一从这个仓库拉镜像。
我用的是目前最轻量的 registry:2 镜像仓库方案,也可以用 Harbor。部署非常简单:
bash复制# 在内网一台机器(我这里是 master)上启动 registry
docker run -d --name registry --restart=always \
-p 5000:5000 \
-v /data/registry:/var/lib/registry \
registry:2
然后把之前导出的 tar 文件在新节点上导入到本地 docker:
bash复制docker load -i k8s-images-v1.30.6.tar
接着用 docker tag 重新打标签,让镜像指向内网仓库地址,再 docker push 推到仓库。比如:
bash复制docker tag registry.k8s.io/kube-apiserver:v1.30.6 192.168.100.10:5000/kube-apiserver:v1.30.6
docker push 192.168.100.10:5000/kube-apiserver:v1.30.6
这里有个非常容易踩的坑:Kubernetes 官方镜像仓库地址是 registry.k8s.io,它其实是一个重定向到不同后端仓库的“中转站”。如果你想在离线环境中直接使用官方镜像名,需要修改 containerd 的配置,或者通过 kubeadm init 的 --image-repository 参数统一指定到内网仓库地址。
我这次直接用 kubeadm init --image-repository=192.168.100.10:5000,一步到位,让 kubeadm 自动从内网仓库拉取所有核心镜像。对于 Calico 镜像,则通过修改部署 YAML 里的 image 字段,把所有镜像地址替换为内网仓库地址。
3. 部署全流程:从裸机到可用集群的实操手记
物料备好后,真正的部署环节反而相对流畅。下面我按照实际操作顺序,一步一步说明,每个关键参数都会解释用途,避免你照抄命令却不知道为什么。
3.1 节点初始化:主机名、hosts、内核参数与系统服务
所有节点先统一下基础配置。首先是主机名:
bash复制hostnamectl set-hostname k8s-master
# worker 节点依次执行 k8s-worker-01、k8s-worker-02
然后配置 /etc/hosts,加入三台机器的 IP 映射,确保彼此能通过主机名解析:
bash复制cat <<EOF >> /etc/hosts
192.168.100.10 k8s-master
192.168.100.11 k8s-worker-01
192.168.100.12 k8s-worker-02
EOF
接下来是内核参数。Kubernetes 官方文档要求必须开启桥接流量检查,同时还要启用 net.ipv4.ip_forward,否则 Pod 之间和 Pod 与 Service 之间的流量转发会失败。我维护了一个常量用下面方式写入:
bash复制cat <<EOF > /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
EOF
sysctl --system
关 swap 是硬性要求。kubelet 默认不能运行在 swap 开启的系统上,所以必须注释掉 /etc/fstab 里的 swap 行后执行 swapoff -a。内核模块方面还需要加载 br_netfilter 模块。最后关闭防火墙,否则节点之间的各种通信会莫名其妙被拦:
bash复制systemctl stop firewalld && systemctl disable firewalld
有些环境不需要真正关闭防火墙,那就要提前放行 6443、2379-2380、10250、30000-32767 等端口,麻烦且容易遗漏。我内网环境直接选择了关闭防火墙,简单可靠。
3.2 安装 containerd 与 kubeadm/kubelet/kubectl
这一步就是把提前准备好的 rpm 包拷贝到节点并安装。我习惯先安装 containerd,再安装 Kubernetes 组件,因为 containerd 的安装依赖相对独立,后续配置不影响 kubeadm 运行。
bash复制# 把离线 rpm 包拷贝到节点后
cd /root/offline-rpm
yum install -y *.rpm
安装完成后启用 containerd:
bash复制systemctl daemon-reload
systemctl enable containerd
systemctl start containerd
注意,containerd 安装后默认配置可能不太适合直接给 Kubernetes 用,特别是镜像仓库的 TLS 地址类型、sandbox_image 等都需要调整。我会在下一节详细说明。
同样,kubelet 安装后也需要立即设置开机自启,因为 kubelet 从集群初始化之前就会以静态容器的方式运行一些系统组件,先启动它不会影响大局:
bash复制systemctl enable kubelet
systemctl start kubelet
这里启动 kubelet 可能会报找不到配置文件、无法连接 apiserver 等错误,这些是正常的,等 kubeadm init 完成后会自动恢复。不必恐慌。
3.3 配置 containerd:从镜像仓库拉取离线镜像
containerd 默认生成的配置可能为空,建议直接生成完整配置文件再修改:
bash复制containerd config default > /etc/containerd/config.toml
然后重点修改三处:
SystemdCgroup改为true:Kubernetes 的 kubelet 使用 systemd 作为 cgroup 驱动,containerd 也要保持一致,否则节点会一直处于 NotReady 状态。sandbox_image指向内网仓库:默认值是registry.k8s.io/pause:3.9,如果不改,启动 Pod 时必然去公网拉镜像,直接失败。- 如果镜像仓库是 HTTP 且地址是 IP,需要在
[plugins.'io.containerd.grpc.v1.cri'.registry.configs]下配置[plugins.'io.containerd.grpc.v1.cri'.registry.configs."192.168.100.10:5000"]并设置insecure_skip_verify = true,允许 containerd 以 HTTP 方式访问内网仓库。
toml复制[plugins.'io.containerd.grpc.v1.cri'.containerd.runtimes.runc.options]
SystemdCgroup = true
[plugins.'io.containerd.grpc.v1.cri']
sandbox_image = "192.168.100.10:5000/pause:3.9"
修改后重启 containerd,再用 ctr image list 或 crictl images 查看本地镜像是否已经可见。如果之前已经导入了 tar,这里应该能看到全部镜像。
3.4 集群初始化:kubeadm init 的参数与含义
Master 节点上执行初始化之前,先确认 containerd 已正常运行,再执行:
bash复制kubeadm init \
--image-repository=192.168.100.10:5000 \
--kubernetes-version=v1.30.6 \
--pod-network-cidr=10.244.0.0/16 \
--control-plane-endpoint=192.168.100.10:6443 \
--apiserver-advertise-address=192.168.100.10
几个参数的作用分别说明一下:
--image-repository:指定拉取镜像的内网仓库地址,彻底绕开公网 registry.k8s.io。这是离线部署最关键的参数。--kubernetes-version:必须与所准备的镜像版本一致,不能随意改动。--pod-network-cidr:指定 Pod 网络的 CIDR 网段。这里选择 10.244.0.0/16 是为了适配后面部署的 Flannel;如果你计划使用 Calico,也可以改成 Calico 官方推荐的 192.168.0.0/16。这个网段必须和物理网络、Service 网段都不冲突。--control-plane-endpoint和--apiserver-advertise-address:指定集群控制平面的对外访问地址。多控制平面场景通常还会挂负载均衡,单控制平面直接用 master 节点 IP 即可。
初始化成功后会输出一段提示,包含 kubectl 的配置命令、join 命令所需的 token 和 CA 证书哈希,这些都要记下来,尤其是最后会用到。如果因为网络或配置问题失败,排查日志的方法我会在第 5 章详细讲。
3.5 kubectl 环境配置与CNI插件部署
初始化的输出里已经明确告诉你怎么配置 kubectl,直接复制执行即可:
bash复制mkdir -p $HOME/.kube
cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
chown $(id -u):$(id -g) $HOME/.kube/config
kubectl get nodes
这时能看到 master 节点处于 NotReady 状态,这是正常的,因为没有安装网络插件。接下来部署 CNI 插件。我这里以 Calico 为例,先把 Calico 的部署 YAML 下载到有网机器,替换其中的镜像地址,再在内网节点执行。
bash复制# 在有网机器上获取 calico.yaml 后
# 把所有 docker.io/calico/ 开头的镜像名前面加上 192.168.100.10:5000/
# 再传入内网节点
kubectl apply -f calico.yaml
Calico 部署后需要等待 Pod 全部 Running,可以用 kubectl get pods -n kube-system 观察。通常很快会出现 calico-node 的 Pod 在 Running 状态,节点状态随之变为 Ready。
3.6 Worker节点加入:token、证书hash与操作顺序
Worker 节点加入集群有两组前置条件:首先是 worker 节点的 containerd 配置必须与 master 一致,至少要保证系统镜像(pause)可以从内网仓库拉取;其次是 Worker 节点上也需要安装并启动 kubelet。
然后在 worker 节点上执行 kubeadm join,这里需要用之前记下的 token 和哈希:
bash复制kubeadm join 192.168.100.10:6443 \
--token <token> \
--discovery-token-ca-cert-hash sha256:<hash>
如果 token 过期了也不用紧张,在 master 上重新生成一个即可:
bash复制kubeadm token create --print-join-command
如果 CA 证书哈希忘了,可以通过 openssl 命令从 kubeconfig 里提取:
bash复制openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | awk '{print $2}'
执行 join 后,回 master 节点查看节点状态:
bash复制kubectl get nodes
当所有节点都显示 Ready 时,集群主流程就算搭建完毕。此时不要急着说“完成”,还缺少最关键的一步:完整测试。
4. 完整测试清单:部署完不等于能用,把集群跑起来才算数
很多教程到 kubectl get nodes 都 Ready 就结束了,但我个人的原则是:集群只有跑过真实业务流量、经历过故障模拟,才算真正交付。 下面是一套我实际执行的测试清单,建议你也照着跑一遍。
4.1 基础健康检查与组件状态验证
首先确认系统组件和集群角色是否正常:
bash复制kubectl get nodes -o wide
kubectl get pods -n kube-system -o wide
kubectl get componentstatuses
重点关注:
- 每个节点角色是否为 master/worker,状态是否 Ready。
- kube-system 命名空间下的 Pod 是否全部处于 Running 或 Completed 状态。特别是 etcd、kube-apiserver、kube-controller-manager、kube-scheduler、CoreDNS、Calico 控制面这几个。
kubectl get componentstatuses输出是否正常(这里注意,新版 K8s 中这个命令可能显示的是controller-manager、scheduler等组件健康状态,需要保持为 Healthy)。
如果 CoreDNS 或 Calico 出现 CrashLoopBackOff,说明前面配置有遗留问题,需要立即排查,不要带着病节点继续后面的测试。
4.2 应用发布测试:从Deployment到Service再到Ingress
这是最核心的功能测试。我会用一个简单的 Nginx 应用走完整个发布链路:创建 Deployment、暴露 Service、再通过 Ingress 访问,验证集群的应用编排能力。
先创建命名空间和 Deployment:
yaml复制apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-test
namespace: test
spec:
replicas: 3
selector:
matchLabels:
app: nginx-test
template:
metadata:
labels:
app: nginx-test
spec:
containers:
- name: nginx
image: 192.168.100.10:5000/nginx:1.25
ports:
- containerPort: 80
应用后检查副本状态:
bash复制kubectl apply -f nginx-deploy.yaml
kubectl get pods -n test -o wide
观察这三个副本是否被调度到不同节点。如果所有副本都落在同一个节点,说明调度策略需要关注,但这不是必须解决的问题。接下来创建 Service:
bash复制kubectl expose deployment nginx-test -n test --port=80 --target-port=80 --type=NodePort
kubectl get svc -n test
最后从一个 worker 节点访问 NodePort 端口(30000-32767),确认返回 Nginx 欢迎页:
bash复制curl http://192.168.100.11:32000
如果这一步通了,说明 Deployment 控制器、Pod 调度、Service 转发链路都是正常的。
4.3 网络与存储验证:跨节点互访与持久化数据
网络部分的验证分两层:第一层是 Pod 与 Pod 之间能否跨节点通信;第二层是 Service 的负载均衡是否生效。
先起一个带 shell 的 Pod,从里面 ping(或 wget)另一个节点的 Pod IP:
bash复制kubectl exec -it <nginx-pod-name> -n test -- sh
ping <另一个pod的IP>
由于部分基础镜像不带 ping 命令,也可以改用 wget -q -O- http://<pod-ip> 测试。接下来创建一个带 PVC 的 StatefulSet 或直接创建 PVC,验证存储挂载。离线环境没有动态存储控制器,可以先使用 Local 类型的 PV 来验证本地盘挂载是否正常,然后再决定是否接入 NFS 或商业存储。
我建议用 NFS 模拟真实持久化存储,因为大部分业务场景还是共享目录型存储更实用。搭建一个简单 NFS 服务,然后创建 StorageClass 指向 NFS provisioner,再创建一个 PVC,最后在 Pod 里写入数据后删除重建 Pod,验证数据是否还在。这一步是判断“集群能不能跑有状态应用”的关键。
4.4 故障恢复与自愈测试:看看K8s到底有多“稳”
集群的自愈能力是离线部署中最容易被忽略的测试点。我建议做以下几个故障模拟:
- Worker 节点宕机:直接在虚拟机上执行
shutdown -h now,等待约一分钟后,在 master 上观察 Pod 是否自动迁移到其他可用节点。 - Pod 被误删:删除某个业务 Pod,观察 Deployment 控制器是否在几秒内重建了新 Pod。
- Master 节点重启:重启 master 节点,等待集群恢复后检查所有节点状态是否恢复 Ready,etcd 是否有数据丢失。
- 网络服务中断:停掉某个节点上的 kube-proxy 或 calico-node 服务,再测试节点间通信是否恢复。
这些测试完成后,我把测试结果汇总成一张表,方便后续交付验收:
| 测试项 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|
| 节点状态 | 3 节点 Ready | 3 节点 Ready | 通过 |
| 系统 Pod 状态 | 全部 Running | 全部 Running | 通过 |
| Deployment 副本调度 | 3 副本跨节点分布 | 分别落在 2 个节点 | 通过 |
| Service 访问 | NodePort 返回 Nginx 页面 | 正常返回 | 通过 |
| 跨节点 Pod 互访 | 网络连通 | 正常 | 通过 |
| PVC 数据持久化 | 重建 Pod 后数据保留 | 保留 | 通过 |
| Worker 宕机恢复 | Pod 自动迁移 | 约 50 秒迁移完成 | 通过 |
| 误删 Pod 自愈 | 自动重建 | 3 秒内重建 | 通过 |
完整的测试过程大概 40 分钟,但这一步能发现环境里很多隐藏问题,比上线后才发现要好得多。
5. 故障排查实录与离线部署的避坑笔记
5.1 镜像拉取失败的现场还原
我这次部署过程中出现的第一起事故是 kubeadm init 报了 ErrImagePull。使用 kubectl describe pod 查看系统组件 Pod 时,发现拉取镜像的源仍然是 registry.k8s.io,而不是我指定的内网仓库地址。
排查下来发现,问题出在我没有把 --image-repository 传递给所有组件。对于使用 kubeadm 拉取的镜像,这个参数是生效的,但 Calico 这类额外组件的镜像仍然保留了官方源地址。解决办法是全局替换:先下载 Calico YAML,把所有 calico/xxx:版本 前缀改成 192.168.100.10:5000/calico/xxx:版本,再执行 kubectl apply。
另外还有一个小坑:如果 containerd 配置文件没有设置 sandbox_image,即使你指定了镜像仓库,pause 容器也依然会去公网拉取。所以 containerd 配置修改一定要在初始化之前完成。
5.2 containerd启动失败的版本陷阱
另一处问题发生在 worker 节点上,systemctl start containerd 后服务没有正常起来,journalctl -u containerd 显示缺失 runc 二进制或 OCI runtime 文件。
原因是 CentOS 7.9 自带的 runc 版本比较旧,与 containerd 1.7 系列不完全兼容。解决办法是在有网机器上下载新版的 runc rpm 包(或二进制文件),一并放入离线 rpm 目录,安装时强制覆盖旧版本。命令如下:
bash复制yumdownloader --resolve runc
yum install -y runc-*.rpm --replacefiles
如果还是不行,可以手动将 runc 二进制替换到 /usr/sbin/runc 并赋予可执行权限。这里踩坑的经验是:离线部署不要只想着 Kubernetes 本身,还要把你系统上所有缺失或过期的底层依赖一并准备好。
5.3 节点加入失败的token问题
在添加 worker 节点时,我遇到了 kubeadm join 超时、提示 token 无效的情况,因为这是第二天重新继续操作,之前的 token 已经过期了。解决方式很简单:
bash复制kubeadm token create --print-join-command
但要注意,kubeadm token create 生成的新 token 必须与之前的 CA 证书哈希配合使用,所以执行打印出来的命令可以直接复制到 worker 上运行,否则会报错。如果你换了 token 但没有重新获取 CA 哈希,严格来说也可能会失败。最省事的方法就是,把 --print-join-command 输出的整条命令原样拿到 worker 节点上执行,不需要我手工拼参数。
5.4 CoreDNS解析异常的内核参数排查
有一次我在测试 Service 访问时,发现应用可以 wget 到 Service IP,但 nslookup kubernetes.default.svc.cluster.local 解析失败。检查 CoreDNS Pod 发现它反复重启。
最终定位到是内核 netfilter 配置有问题,之前配置 net.bridge.bridge-nf-call-iptables = 1 的节点上,br_netfilter 内核模块没有加载成功。解决方式:
bash复制modprobe br_netfilter
echo "br_netfilter" > /etc/modules-load.d/br_netfilter.conf
sysctl --system
这个问题非常隐蔽,因为很多教程只写了内核参数,但没提需要加载模块。离线环境下尤其容易漏掉。
5.5 离线部署经验汇总:三份清单帮你少走弯路
把这次(以及之前多次离线部署)的经验浓缩成三份清单,每次做离线项目前对着检查一遍:
- 资源清单:确认
kubeadm config images list的镜像全部备齐、所有节点的 containerd 配置统一、镜像仓库可访问、rpm 依赖已--resolve下载完整。不要只准备主 rpm 包,最好把依赖一起带上。 - 网络清单:确认
ip_forward开启、swap 关闭、防火墙规则放行(或关闭)、br_netfilter模块已加载。这三件事少一件,集群起来以后都会有随机性故障。 - 测试清单:不要只看节点 Ready,一定要按第 4 节的流程把功能、网络、存储、故障自愈全部过一遍。哪怕环境时间紧张,至少要把 NodePort 访问和节点重启两项测完。
另外,建议在离线物料拷贝进内网后,对每个 tar 包执行一次 sha256sum -c 校验。尤其在大批量拷贝时,U盘、共享存储、跳板机中转都可能造成文件损坏,提前校验能省掉后面一大堆莫名其妙的报错。
这套流程跑完,我最大的感受是:离线部署并不复杂,只是把原本“随手就能下载”的动作全部前置化,把不确定性提前消化掉。只要物料准备足够细、测试阶段做得足够全,内网环境里的 Kubernetes 集群完全可以做到和生产环境同等稳定。
