前阵子被拉去给一批新到的麒麟V10服务器装Kubernetes,机房在隔离网里,没法访问外网,版本还要卡在1.32.11。网上在线安装的教程一抓一大把,可真到了离线环境,几乎所有教程都会卡死在第一步:下载命令根本没法定外网跑。这篇就把我在麒麟V10上离线安装k8s 1.32.11的完整过程写下来,从离线介质准备、系统适配、部署实操到踩坑复盘,适合正在隔离网或离线环境里做K8s集群部署的同学参考。
环境先交代一下:我这批服务器是麒麟V10服务器版(x86_64架构),控制平面一台,工作节点两台,共享一套内网yum源,但内容很旧,只有基础系统包。整个过程最麻烦的不是装K8s本身,而是把所有该带的依赖、二进制、镜像都想全,然后一次性搬进内网。
1. 离线部署的总体设计:版本、架构与介质清单
离线安装最忌讳上来就装,装到一半发现缺东西。别问我怎么知道的,我第一遍就是因为少了一个conntrack-tools的RPM包,卡在kubeadm init前面整整半天。所以第一步,先把版本组合、节点规划、介质清单全部定下来。
1.1 先聊版本组合:为什么是 1.32.11 + containerd 1.7.x
Kubernetes从1.24之后就彻底移除了dockershim,容器运行时基本只有containerd和CRI-O这类选择。生产环境我一般直接用containerd,理由很简单:RPM包和二进制都方便离线准备,kubelet对接稳定,社区默认就是它。
K8s 1.32.11在1.32.x这个补丁系列里处于比较靠后的位置,稳定性已经经过多轮验证。containerd我选的是1.7.x系列,这个版本对SystemdCgroup支持成熟,和1.32的kubelet配合没什么坑。CNI选Flannel,没选Calico,原因后面会说。
这里有一个关键点:K8s的版本选择不是越新越好,而是要和操作系统生态匹配。麒麟V10的内核一般不会太新,而K8s 1.32要求Linux内核足够支持overlayfs、iptables、ipvs这些基础能力,4.19+基本都能满足。版本太高反而可能出现内核模块不匹配的问题。
1.2 节点规划:控制平面、工作节点与资源建议
规划节点时先确认架构和角色分配。我这边是3台:
| 节点角色 | 数量 | 最低建议配置 | 主要负载 |
|---|---|---|---|
| 控制平面 | 1 | 4核8G,系统盘100G | kube-apiserver、etcd、controller-manager、scheduler |
| 工作节点 | 2 | 4核8G,系统盘100G+数据盘100G | kubelet、containerd、业务Pod |
| 介质制作机 | 1(有外网) | 2核4G即可 | 下载RPM、二进制、镜像并打包 |
这里要特别提醒:控制平面的磁盘不要抠门。etcd默认数据目录在/var/lib/etcd,虽然一开始占用不大,但集群运行半年后日志和事件数据会慢慢膨胀。工作节点的数据盘建议单独挂给containerd,别跟系统盘混在一起,这个我后面会展开讲。
1.3 离线介质的内容清单:一个文件都不能少
离线介质包不是简单地拷几个RPM就完事,里面至少包含四类东西:
- K8s核心二进制:kubeadm、kubelet、kubectl,我直接下载官方RPM包,省去手动解压二进制再写systemd服务文件的麻烦。
- 容器运行时:containerd本体加
cri-containerd可能涉及的文件,还有CNI插件(cni-plugins)。 - 系统依赖RPM:kubelet、containerd运行时依赖的库和工具,比如
libseccomp、conntrack-tools、socat、ipvsadm等,内网yum源里不一定全,必须提前准备。 - 容器镜像:K8s控制面组件镜像(kube-apiserver、etcd、coredns、pause等)、网络组件镜像(flannel)、业务测试镜像(nginx)。
- CNI部署manifest:比如flannel的yaml文件,离线环境没法从GitHub直接拉,必须提前下好。
我把这些写成了一张Excel清单,每项都标注了版本号和来源,后面所有机器都按这张表干活。这个习惯建议养成,尤其多人协作时,清单能避免“我以为你拷了”这种悲剧。
2. 麒麟V10系统侧准备:装之前必须改掉的四个地方
麒麟V10虽然整体跟RHEL系的CentOS很像,但细节上有自己的脾气。系统侧我踩过的坑多半是“看起来没问题,装到一半就报错”。
2.1 内核与架构摸底
麒麟V10的服务器版内核通常基于4.19或5.x,x86_64架构。动手前先跑一下这几个命令:
uname -a cat /etc/os-release arch根据输出确认发行版本、内核和架构,再去选对应版本的RPM和二进制包。比如arch是x86_64,下载K8s二进制时就要选linux-amd64,不要搞成arm64,这个低级错误真有人犯过。
另外看看/var/lib/dpkg或/var/lib/rpm是否存在,确定包管理器是dpkg还是rpm。麒麟V10不同子版本可能一个基于Debian系,一个基于RPM系,这个直接影响离线RPM的兼容性。我这边全是RPM系,下面所有命令也都按RPM系来写。
2.2 加载 overlay 与 br_netfilter,配好内核参数
containerd需要overlayfs支持镜像分层,K8s的网络转发需要br_netfilter。这俩内核模块不加载,后面会出现各种玄学问题,比如Pod网络不通、service不能访问。
写一个加载模块的配置:
cat > /etc/modules-load.d/k8s.conf <<'EOF' overlay br_netfilter EOF modprobe overlay modprobe br_netfilter然后配置内核参数:
cat > /etc/sysctl.d/k8s.conf <<'EOF' net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.ipv4.ip_forward = 1 EOF sysctl --system注意net.ipv4.ip_forward一定要设成1。默认情况下很多Linux发行版ipv4转发是关的,K8s的Pod跨节点通信依赖这个开关,不然后面node之间互相访问Pod会超时。
2.3 swap、防火墙与 SELinux:该关的关,该放行的放行
Kubelet默认不允许swap开着,至少1.32版本上还是这样。所以先关掉swap:
swapoff -a sed -i '/ swap / s/^/#/' /etc/fstab别只执行swapoff -a就完事,重启后fstab还会把它挂回来。记得注释fstab里的swap行。
防火墙和SELinux是另一对常客。安全要求高的环境不建议直接firewalld stop,可以把K8s需要的端口放行:
- 控制平面:6443(apiserver),2379-2380(etcd),10250(kubelet),10259(scheduler),10257(controller-manager)
- 工作节点:10250,30000-32767(NodePort)
如果只想快速验证,直接关掉firewalld:
systemctl stop firewalld && systemctl disable firewalldSELinux也一样,要么放行,要么setenforce 0并修改/etc/selinux/config。我通常在内网测试环境选择直接关闭,但在生产环境还是建议保留SELinux并用策略放行,只是麒麟V10的SELinux策略对K8s的支持有时不太完善,需要额外调。
2.4 磁盘规划:别让系统盘被容器镜像塞满
工作节点上containerd默认数据目录是/var/lib/containerd,系统盘空间如果只有100G,跑几个镜像和数据卷就可能撑爆。我习惯单独挂一块数据盘给containerd,挂载参数同时考虑限额:
mkfs.xfs /dev/sdb mkdir -p /var/lib/containerd mount /dev/sdb /var/lib/containerd echo "/dev/sdb /var/lib/containerd xfs defaults 0 0" >> /etc/fstab为什么不建议用ext4?xfs在处理大文件和高并发写入时更稳,尤其容器镜像分层多,overlayfs在xfs下表现更好。当然ext4也能用,只是个人经验里xfs的坑少一些。
控制平面上的etcd数据目录/var/lib/etcd也可以单独挂盘,但测试环境不强制;生产环境一定挂,因为etcd是集群的命根子,磁盘满了整个集群直接不可写。
3. 制作离线介质包:在有网机器上一次准备到位
这一步需要一台能访问外网的机器,通常是运维的笔记本或者一台临时跳板机。介质包做得好不好,直接决定内网部署顺不顺利。
3.1 K8s 二进制包与 RPM 依赖下载
先建目录结构:
mkdir -p k8s-1.32.11-offline/{rpm,bin,images,manifests} cd k8s-1.32.11-offlineK8s官方RPM仓库在特定镜像站有同步,可以在介质机上配置临时yum源,然后下载。先用yumdownloader把依赖一并拉下来:
yum install -y yum-utils yumdownloader --resolve --destdir=./rpm \ kubeadm \ kubelet \ kubectl如果直接下载官方二进制,可以用:
wget https://dl.k8s.io/v1.32.11/kubernetes-server-linux-amd64.tar.gz这个Server包比较大,但里面包含完整的kube-apiserver、kube-controller-manager、kube-scheduler等二进制,用来部署控制平面非常方便。如果你倾向于RPM方式,那RPM仓库里的kubeadm、kubelet、kubectl三个包就够了,注意把它们的依赖RPM一起--resolve下来。
containerd和CNI插件也是关键:
wget https://github.com/containerd/containerd/releases/download/v1.7.x/containerd-1.7.x-linux-amd64.tar.gz wget https://github.com/containernetworking/plugins/releases/download/v1.x/cni-plugins-linux-amd64-v1.x.tgz版本号按你实际选择去release页面核对。顺便把runc的二进制也装了,containerd的tar包里通常不会自带runc,单独下载一份:
wget https://github.com/opencontainers/runc/releases/download/v1.1.x/runc.amd64最后把依赖系统工具装齐:
yum install -y yum-utils tar socat conntrack-tools ipvsadm ipset这些工具的内网系统不一定自带,先在外网机器上下载RPM包放进rpm目录。
3.2 K8s 系统镜像导出:用 kubeadm 的清单做事
K8s控制面镜像版本多且杂,手敲很容易错。最稳妥的办法是用kubeadm自身列出的镜像清单:
kubeadm config images list --kubernetes-version=v1.32.11输出大概是下面这些:
- registry.k8s.io/kube-apiserver:v1.32.11
- registry.k8s.io/kube-controller-manager:v1.32.11
- registry.k8s.io/kube-scheduler:v1.32.11
- registry.k8s.io/kube-proxy:v1.32.11
- registry.k8s.io/etcd:3.5.x
- registry.k8s.io/coredns/coredns:v1.11.x
- registry.k8s.io/pause:3.10
注意kubeadm config images list输出的版本号会随K8s依赖组件的更新变化,你要以实际输出为准。然后在介质机上用containerd或docker把这些镜像拉下来再导出:
ctr -n k8s.io images pull registry.k8s.io/kube-apiserver:v1.32.11 # 逐个pull后导出成tar ctr -n k8s.io images export ./images/k8s-images.tar \ registry.k8s.io/kube-apiserver:v1.32.11 \ registry.k8s.io/kube-controller-manager:v1.32.11 \ ...如果介质机上有docker,也可以用docker pull加docker save,效果一样。导出成一个总tar包的好处是内网导入时只需执行一次命令。
Flannel镜像同样提前拉下来:
docker pull flannel/flannel:v0.25.7 docker pull flannel/flannel-cni-plugin:v1.5.1-flannel2 docker save flannel/flannel:v0.25.7 flannel/flannel-cni-plugin:v1.5.1-flannel2 > ./images/flannel-images.tar业务测试镜像也顺手放一个:
docker pull nginx:alpine docker save nginx:alpine > ./images/nginx-test.tar3.3 校验、打包与内网落地
介质包准备好了,不能急着拷进去,先做两件事:
第一,校验sha256。把每个tar包、RPM包都算一遍哈希,内网导入后可以通过sha256sum -c验证文件完整性。离线环境没有外网下载二次校验的可能,介质机的哈希表就是唯一凭证。
第二,写README。把介质包内容、版本、导入顺序全写进一个txt文件。别嫌啰嗦,一个月后再回来维护,你看到'README'里的步骤能省很多时间。
最后打包上传到内网存储或直接U盘拷贝:
tar czf k8s-1.32.11-offline.tar.gz k8s-1.32.11-offline/内网任意一台机器解压后共享给其他节点即可。
4. 内网部署实操:从 containerd 到集群可用
介质包到了内网,部署开始。
4.1 安装 containerd 并改对三处配置
首先装依赖RPM。麒麟V10内网的yum源不完整,直接用本地目录安装:
rpm -ivh ./rpm/*.rpm --nodeps--nodeps是双刃剑,依赖缺失时它会跳过检查,但运行时会报错。我更推荐用yum localinstall:
yum localinstall -y ./rpm/*.rpm它会尝试从内网yum源补全缺失依赖,实在找不到再人工介入。我这边有几台机器缺socat和libseccomp,都是从介质包补上的。
然后解压containerd和CNI插件:
tar Cxzf /usr/local -C /tmp containerd-1.7.x-linux-amd64.tar.gz mkdir -p /opt/cni/bin tar Cxzf /opt/cni/bin -C /tmp cni-plugins-linux-amd64-v1.x.tgz生成containerd默认配置:
mkdir -p /etc/containerd containerd config default > /etc/containerd/config.toml这个默认配置要改三处。第一处是SystemdCgroup从false改成true,否则kubelet和containerd的cgroup管理方式不一致,Pod创建会报错。
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] systemd_cgroup = true第二处是sandbox_image,换成我们导入的pause镜像地址。离线网络环境下如果保持默认值,kubelet一旦需要创建sandbox就会试图去联网拉镜像。
[plugins."io.containerd.grpc.v1.cri"] sandbox_image = "registry.k8s.io/pause:3.10"第三处是registry.mirrors配置,为了内网如果有私有镜像仓库时能优先走内网地址,避免每次拉镜像都超时。如果没有私有仓库,可以留空或指定镜像站。
改完启动containerd:
systemctl daemon-reload systemctl enable --now containerd4.2 导入镜像:namespace 错了等于白导
containerd的镜像导入必须注意namespace。默认ctr命令操作的namespace是default,但kubelet通过CRI调用containerd时使用的是k8s.io这个namespace。用ctr images import导入到default,kubelet照样看不到。
正确的导入姿势:
ctr -n k8s.io images import ./images/k8s-images.tar ctr -n k8s.io images import ./images/flannel-images.tar ctr -n k8s.io images import ./images/nginx-test.tar导入后用以下命令确认:
ctr -n k8s.io images list | grep registry.k8s.io这个坑我第一遍就踩了,当时导完半天,结果Pod一直ImagePullBackOff,查到最后发现是namespace不对。记住:CRI用的namespace是k8s.io。
4.3 kubeadm init 与配置文件
不推荐直接写一长串命令行参数,建议把配置写进yaml文件,方便留存和审计。先创建kubeadm.yml:
apiVersion: kubeadm.k8s.io/v1beta4 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.10.11 bindPort: 6443 nodeRegistration: name: master01 criSocket: unix:///var/run/containerd/containerd.sock --- apiVersion: kubeadm.k8s.io/v1beta4 kind: ClusterConfiguration kubernetesVersion: v1.32.11 controlPlaneEndpoint: "192.168.10.11:6443" networking: podSubnet: "10.244.0.0/16" serviceSubnet: "10.96.0.0/12"podSubnet用的是10.244.0.0/16,这个网段要和后面Flannel的默认网段一致,不然装完网络组件后Pod拿不到IP。controlPlaneEndpoint填高可用虚拟IP或第一个控制平面节点IP,即使单控制平面也建议写明白。
执行初始化:
kubeadm init --config=kubeadm.yml如果一切顺利,最后会输出一段kubeadm join命令。记得保存,或者用kubeadm token create --print-join-command重新生成。
初始化完成后,按提示配置kubeconfig:
mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config4.4 工作节点 join 与 CNI 网络一次搞定
工作节点上先装好同样的依赖RPM、containerd,并且导入介质包里的镜像。然后执行控制平面生成的join命令:
kubeadm join 192.168.10.11:6443 --token xxx \ --discovery-token-ca-cert-hash sha256:xxxjoin成功后再装CNI。我选Flannel的原因就是简单,就一个yaml文件加几个镜像,离线部署压力小:
kubectl apply -f ./manifests/kube-flannel.yml注意kube-flannel.yml要在介质准备阶段下载好,或者在控制平面节点能联网时下载,否则离线环境手抖都抖不出来。Flannel装完后默认使用10.244.0.0/16网段,和kubeadm.yml里的podSubnet吻合。
等每个节点上的kube-flannel-dsPod变为Running,基本上网络就通了。
4.5 集群健康检查:别急着跑业务
装完之后先看一轮状态:
kubectl get nodes kubectl get pods -A然后跑到工作节点上看Pod网络情况:
kubectl run ngx --image=nginx:alpine --port=80 kubectl get pod -o wide从工作节点或控制平面去curl这个Pod的IP,如果能通,说明Pod间网络正常。再尝试创建一个NodePort Service,验证节点端口转发能力。这套检查流程虽然简单,但能在业务部署前把底层网络问题暴露干净。
5. 离线安装踩坑记录:五个让我多花了一个周末的问题
单看步骤很顺,但中间我实际踩了不少坑。这些坑每个都能让部署暂停半小时以上,一一列出来,希望各位能绕开。
5.1 Pod 卡在 ContainerCreating,罪魁祸首是 pause 镜像
现象:kubeadm init完成后,kube-system里的Pod全部卡在ContainerCreating,describe一看是sandbox创建失败。
排查:执行ctr -n k8s.io images list | grep pause,结果里没找到pause镜像。原因是我在一台机器上导镜像时只导了控制面组件镜像,忘了把pause单独拉出来。而containerd默认配置里的sandbox_image指向registry.k8s.io/pause:3.10,离线环境下无法拉取,Pod就永远建不起来。
解决:介质机补下pause镜像,docker pull registry.k8s.io/pause:3.10,导出,再到内网导入,重启containerd。另外我建议在containerd配置里显式写清楚sandbox_image,不要依赖默认值——版本升级时默认值可能变,显式配置能减少差异。
5.2 join 失败却提示 timeout?先查 /etc/hosts 和主机名
现象:工作节点执行kubeadm join,报timeout waiting for the kubelet这类错误,信息很泛,让人无从下手。
排查链路:先看systemctl status kubelet,发现kubelet在反复重启,日志里报证书或节点注册相关的错误。再看/etc/hosts,发现工作节点的主机名并没有解析到本机IP。Kubelet启动后要注册到apiserver,它得能把自己地址报上去,而hostname解析不畅时注册就卡住。
解决:在所有节点/etc/hosts里加上:
192.168.10.11 master01 192.168.10.21 node01 192.168.10.22 node02然后重启kubelet。这个问题在纯内网环境特别常见,因为内网DNS不一定建得全。
5.3 kube-proxy 引用 ipvs 模式,系统却缺模块
现象:kube-system里kube-proxyPod反复CrashLoopBackOff,日志中提示can not find ipvs或者ipset相关错误。
原因:1.32版本的kube-proxy默认或配置文件中可能使用ipvs转发模式,但麒麟V10系统默认没有加载ip_vs内核模块,也没有安装ipvsadm、ipset这些工具。
解决:
yum install -y ipvsadm ipset cat >> /etc/modules-load.d/k8s.conf <<'EOF' ip_vs ip_vs_rr ip_vs_wrr ip_vs_sh nf_conntrack EOF modprobe ip_vs modprobe ip_vs_rr modprobe ip_vs_wrr modprobe ip_vs_sh modprobe nf_conntrack如果还是不行,就把kube-proxy的mode改回iptables,配置里把mode: iptables显式写好。生产环境用iptables虽然性能略低,但胜在依赖少、好排查,小规模集群完全够用。
5.4 RPM 安装顺序带来的依赖连环坑
现象:直接执行rpm -ivh kubelet-1.32.11.rpm,系统提示缺少kubernetes-cni或libseccomp依赖。
原因:RPM安装不像yum会自动解析依赖,离线介质包里的RPM必须一次性安装或者按依赖顺序装。
解决:其实介质机在下载时就可以把依赖一起下全。内网安装时最好用:
yum localinstall -y ./rpm/*.rpm如果某些RPM依然被yum拒绝,再手动指定依赖顺序:先装libseccomp、conntrack-tools、socat、kubernetes-cni,最后装kubelet、kubeadm、kubectl。依赖顺序这个事,介质机上下载多花五分钟,内网能省两小时。
5.5 重启之后集群失联:enable 从左往右都要配
现象:全部装完主备确认没问题,结果晚上机房断电重启,第二天集群连不上了。
排查:kubectl get nodes超时,登录控制平面看systemctl status kubelet,发现kubelet是inactive状态。再看containerd,也是没开机自启。
原因:安装过程中我手动启动过服务,但没有执行systemctl enable。重启后containerd和kubelet都没拉起来,kubelet的enable是RPM包安装时配好的,但我用的二进制解压方式没有systemd服务文件。
解决:用RPM方式安装的跑:
systemctl enable --now containerd systemctl enable --now kubelet用二进制方式安装的,需要自己准备systemd service文件。检查一下/usr/lib/systemd/system/下有没有kubelet.service,没有就从介质包里补一个。
另外etcd是静态Pod,由kubelet拉起,所以kubelet没起来时etcd也会一起“消失”,这也是重启后连不上集群的核心原因。
6. 收尾:这套离线方案后续怎么维护
集群跑起来只是开始,后续的维护才见真章。离线环境没有外网,一切都要靠介质包和备份来兜底。
6.1 介质包是“活化石”,升级扩容都靠它
离线部署的介质包必须留好,它是你这个集群版本的“活化石”。以后要扩容节点,直接拿同一套介质包按第4章的流程再走一遍;要升级补丁版本,则在介质机上重新拉新的二进制和镜像,打包后再上传内网。
我建议给介质包打上版本标签,例如k8s-1.32.11-offline-2025xx.tar.gz,哪怕K8s升级了新版本,旧的介质包也别删。K8s补丁版本之间一般可以滚动升级,但你总会遇到某台老节点需要回滚或重装的情况,那时候旧介质包就是救命稻草。
6.2 该备份的文件清单
至少备份这几类东西:
/etc/kubernetes/:kubeadm配置、证书、admin.conf/etc/containerd/config.toml:containerd配置/etc/sysctl.d/k8s.conf:内核参数/var/lib/etcd/:etcd数据(如果有条件,定期做etcd快照)kubeadm.yml:初始化配置文件
建议写个cron脚本,定期把etcd快照和kubeadm配置打包放到NFS或独立备份机。离线环境最怕的是“一切都在那台机器上”,结果那台机器坏了。
6.3 有条件的话,再往前一步做内网镜像仓库
纯离线介质包适合一两次部署和几十台以内的集群。如果这套环境要长期运营、节点会不断增长,建议在条件允许时搭一个内网镜像仓库(比如基于私有Registry或Harbor),把镜像统一推送到内网仓库,各节点从仓库拉取。
介质包的导入是静态的,镜像仓库是动态的。前者适合开局,后者适合运维。两者可以并存:介质包用于节点初始化,镜像仓库用于日常调度和扩容。先问自己一个问题:“这套集群会不会活过三个月?”会的话,镜像仓库值得提前准备。
最后讲点实在的,我在这套麒麟V10离线环境上最大的体会是:离线部署的复杂度并不在K8s本身,而在“能不能把所有变量前置控制住”。版本锁定、依赖齐全、镜像清单精确,离线环境会比在线环境更容易复现结果——因为没有人会跑着跑着偷偷给你装个新版本。把介质包维护好、把备份做勤快,这套集群就能安心跑很久。