ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

麒麟V10+ARM64离线高可用K8S 1.26.15部署实践

麒麟V10+ARM64离线高可用K8S 1.26.15部署实践

简介:基于麒麟V10操作系统与ARM64架构CPU的一键离线部署工具,面向信创内网环境的运维和实施人员,解决无外网条件下Kubernetes容器集群初始化难、依赖组件版本匹配复杂的问题。整套工具封装为88个文件,类型涵盖shell脚本、rpm安装包、yaml配置模板、service服务文件,以及containerd、kubeadm、kubectl等关键二进制组件,压缩包整体大小约621MB,支持一键部署、集群检查、集群移除,并支持worker节点的一键扩容和缩容。工具适配单机、一主多从、三主多从三种部署架构,内置证书有效期长达99年,并预先集成Calico网络、kube-lb负载均衡和Nginx入口等配置模板,在完全离线环境中也能快速拉起高可用生产级集群。目前已有289人学习下载,适合需要在内网批量交付或复现Kubernetes环境的运维工程师参考使用,能显著降低信创环境下的交付门槛与排错成本,提升集群交付效率。

1. 麒麟 V10+ARM64 离线高可用 K8S 1.26.15:一键部署工具到底解决什么

基于 Kylin V10 + ARM64 架构 CPU + containerd 一键离线部署容器版 K8S 1.26.15 高可用集群工具,很多信创交付工程师看到这个标题的第一反应是:终于不用再手搓四十条命令了。这类工具解决的核心问题很明确——在完全没外网的内网环境里,面对三台麒麟 V10(或 SP1/SP2)的鲲鹏、飞腾机器,要把 kubeadm 拉起一套三个 master、N 个 worker 的高可用集群,镜像怎么传、rpm 依赖怎么装、VIP 怎么配、证书怎么续,全部压到一个入口脚本里完成。适合正在做国产化平台交付、K8S 运维迁移的人,也适合刚接触信创环境但要独立交付集群的工程师。这篇文章不依赖某个仓库源码,按这类工具最常见的形态,把物料准备、运行初始化、HA 配置和踩坑路径讲清楚。

2. 攒离线物料包:ARM64 镜像清单、rpm 内网源与四类文件

2.1 离线包里的四类物料:二进制、镜像、rpm 和模板

真正到内网交付过的人都知道,离线包的成败不在脚本写得多漂亮,而在物料全不全。一个能复用的离线交付包,通常拆成四个目录,分别是二进制、容器镜像、rpm 依赖、配置模板。

二进制指的是 kubeadm、kubelet、kubectl 三个主程序,注意版本必须精确锁在 1.26.15,并且是 linux/arm64 平台。很多人在这栽过:从 x86 的机器下载了 amd64 的 kubelet 拷过去,文件也能执行,但 kubelet 一跑起来各种参数不识别、健康检查失败,最后查二进制平台才发现问题。

镜像目录是离线包的大头,包含控制面镜像和网络插件镜像。控制面镜像由 kubeadm 固定清单决定,包括 kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy、etcd、coredns、pause 这几个。网络插件如果选 Calico,还得带 calico/node、calico/cni、calico/kube-controllers 等,这是另一个容易漏的类别。

rpm 目录解决系统依赖。kubelet 跑起来强依赖 conntrack、socat、ebtables、ipset、iptables 等命令,containerd 又依赖 libseccomp。很多交付现场连 kernel 版本都不一样,所以靠谱的离线包会把装 runtime 和装 K8S 的 rpm 全部打进 rpm 目录,到现场做本地 yum 源。配置模板则是直接复用而不是全靠手改的部分,比如 /etc/crictl.yaml、kubeadm-init.yaml、haproxy.cfg、keepalived.conf。

工具化设计上,我一般建议入口脚本按install.sh 2>&1 | tee /var/log/k8s-offline-install.log方式留全程日志,并且每个子步骤单独写脚本,用 marker 文件标记完成状态,后面第 3 章会展开。

2.2 在 ARM64 机器上用 kubeadm 拉全 1.26.15 镜像并导出 tar

离线镜像的攒法,最常见、最不容易翻车的方式是:找一台能联网的 ARM64 机器,先写 kubeadm-init.yaml,用kubeadm config images list拿到目标版本的精确镜像清单,再逐张拉取、一起导出成 tar。

# 先把集群初始化配置写好,这里只为了取镜像清单 kubeadm config images list --config=kubeadm-init.yaml > /tmp/image-list.txt # 逐张拉取镜像,错一张都补拉 while read img; do ctr -n k8s.io images pull "$img" || echo "pull failed: $img" done < /tmp/image-list.txt # 检查是否拉全 ctr -n k8s.io images ls -q | grep -E "(apiserver|controller-manager|scheduler|proxy|etcd|coredns|pause)" # 导出整个 k8s.io 命名空间下的镜像,输出单文件 tar ctr -n k8s.io images export /opt/offline/k8s-images.tar $(ctr -n k8s.io images ls -q)

这段逻辑说明几个关键点。第一,拉镜像务必在 ARM64 原生机器上执行,不要在 x86 上拉完再传,否则 manifest 平台不匹配,后面章节专门讲。第二,这里用的是 containerd 的 ctr 而不是 docker,因为目标运行时就是 containerd,别混用工具链。第三,ctr images export后面直接展开命名空间全部镜像,输出 tar 会很大,属正常,建议按节点数算好磁盘空间。

镜像清单里的详细 tag,以本机kubeadm config images list输出为准,etcd 一般落在 3.5.x,coredns 落在 1.9.x,pause 在 1.26 系默认是 3.9。网络插件镜像用同样方式拉取,命名为 calico-images.tar,放进 images 目录即可。

2.3 把 rpm 目录变成离线 yum 源:createrepo 三步

离线 rpm 源是很多人忽略的细节。直接把 rpm 丢到服务器上再yum install ./xxx.rpm,能解决单包安装,但装 kubeadm 时它会拉依赖,依赖一旦不在本机就只能报错。因此要先把 rpm 目录做成标准 yum 源。

# 第一步:对 rpm 目录生成仓库元数据 createrepo -v /opt/offline/rpm # 第二步:写一个 offline.repo,优先级放在最前面 cat > /etc/yum.repos.d/offline.repo <<'EOF' [offline] name=offline repository baseurl=file:///opt/offline/rpm enabled=1 gpgcheck=0 EOF # 第三步:刷新缓存后直接安装 yum clean all yum makecache yum install -y containerd.io kubeadm-1.26.15 kubelet-1.26.15 kubectl-1.26.15

这里参数重点在enabled=1和gpgcheck=0。gpgcheck=0是很多安全评审会挑的点,但离线内网环境下没有官方源可验,通常先关掉保证交付,后续做基线加固时再补校验;如果打包时把 GPG key 一并放入并开启gpgcheck=1,更稳妥但复杂度高一些。createrepo生成 repodata 目录后不要移动 rpm 目录,否则元数据路径失效。另外注意 Kylin V10 自带的 bash 环境默认是 4.x,createrepo 命令若没预装,打包机上的离线 rpm 目录里要包含 createrepo 的依赖包。

3. containerd 运行时初始化:cgroup 驱动、sandbox_image 与 ctr 导入

3.1 containerd config.toml 的三个必改参数

containerd 装好后直接启动并不能用于 K8S,至少有三个参数必须改。用默认配置生成后,逐一确认。

# 生成默认配置骨架 containerd config default > /etc/containerd/config.toml # 把 CRI 的 cgroup 驱动改成 systemd sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml # 把沙箱镜像改成内网可导入的 pause 镜像 sed -i 's#sandbox_image = "registry.k8s.io/pause:3.9"#sandbox_image = "registry.k8s.io/pause:3.9"#' /etc/containerd/config.toml # 启动并验证 systemctl enable --now containerd crictl version

cgroup 驱动不一致是集群初始化和节点运行时最常见的翻车源头。kubelet 默认用 systemd cgroup 驱动,containerd 默认的是 cgroupfs,两边不一致时,kubelet 健康检查会过不了,pod 沙箱刚创建就被杀。如果你不用 sed 而用文本编辑器改,定位到plugins."io.containerd.grpc.v1.cri"段下的SystemdCgroup字段,确认值是 true。

sandbox_image 也就是 pause 镜像,它的作用是给每个 pod 提供网络和 pid 命名空间底座。离线下这个镜像必须预先导入,并且 tag 要和 kubeadm 期望一致。第 2 章已提,1.26.15 默认是 pause:3.9,但最稳的确认方式是看kubeadm config images list的输出。

很多人分不清 containerd、docker、K8S 三者关系,这里就一句话:1.26 版本的 K8S 已经移除了 dockershim,kubelet 直接通过 CRI 和 containerd 通信,所以离线包完全不需要 docker,docker 装进来反而多个守护进程占资源。

3.2 ctr 导入必须带 k8s.io 命名空间:否则 kubelet 永远找不到镜像

ctr 导入镜像时,命名空间是玄学,也是新手翻车最狠的一处。kubelet 通过 containerd 的 CRI 插件工作,默认使用k8s.io命名空间,而 ctr 默认操作的却是default命名空间。所以如果你直接执行下面第一条命令,后面 kubelet 会一直报拉取镜像失败,明明本地有镜像却不用,特别憋屈。

# 错误示范:会把镜像导入到 default 命名空间 ctr images import /opt/offline/k8s-images.tar # 正确做法:显式指定 k8s.io 命名空间 ctr -n k8s.io images import /opt/offline/k8s-images.tar # 验证:用 crictl 看的是 kubelet 视角的镜像列表 crictl images

参数-n k8s.io是关键,crictl images则是验证手段。crictl 的配置文件/etc/crictl.yaml也要在离线包模板里带上,至少三行内容:runtime-endpoint: unix:///run/containerd/containerd.sock、image-endpoint: unix:///run/containerd/containerd.sock、timeout: 10。没有这个文件时 crictl 会尝试连 docker socket,报错后让人误以为 containerd 没起来。

导入 tar 文件时如果报no matching image,多是 tar 本身损坏或导出时命名空间搞错了,重新在打包机导出即可。导入过程对磁盘 IO 要求高,建议在目标节点目录预留足空间。

3.3 一键脚本的幂等设计:失败后能断点续跑

「一键部署」这个词看着简单,实际工程里最难的是失败重跑。内网交付现场网络差、机器配置参差,脚本跑到一半断了是常事,如果脚本没有幂等设计,就会反复 kubeadm init 导致/etc/kubernetes/残留旧证书,越跑越烂。我见过这类工具的标准做法是阶段 marker。

#!/usr/bin/env bash set -euo pipefail BASE_DIR=/opt/offline MARK_DIR="${BASE_DIR}/.markers" mkdir -p "$MARK_DIR" run_step() { local step_name="$1" local marker_file="$MARK_DIR/$step_name.done" if [ -f "$marker_file" ]; then echo "skip: $step_name" return 0 fi echo "run: $step_name" bash "$BASE_DIR/scripts/${step_name}.sh" touch "$marker_file" } run_step 01-prepare run_step 02-containerd run_step 03-k8s-init run_step 04-join

run_step函数里先看 marker 文件,失败时当前脚本退出,修复问题后重新执行入口脚本,已经完成的步骤直接跳过。这个设计带来的好处是:kubeadm init不重复执行,containerd 配置不重复覆盖,证书目录不会因为二次 init 产生冲突。注意 marker 文件必须在该步骤全部命令成功且经过自检后才 touch,宁可多验证一步,不要省。

这套脚本结构里,我习惯把每个步骤脚本都加上set -e,同时把节点角色信息放到conf/node-info.env,这样同一套包既能初始化第一个 master,也能在其他节点上只执行 join 步骤。

4. 高可用集群初始化与节点加入:VIP、kubeadm init 和两种 join

4.1 keepalived+haproxy 搭建 apiserver 接入 VIP

高可用方案选型上,对内网信创环境最常见的组合是 keepalived + haproxy。三个 master 节点各装一个 haproxy,监听 6443 端口,后端指向三台 master 的 6443;keepalived 在三个 haproxy 之上再虚拟出一个 VIP,作为 kubeadm 的 controlPlaneEndpoint。这样 apiserver 的接入地址始终是 VIP,单台 master 挂掉不影响控制面访问。

# 每台 master 部署,后端列表写全三台 listen k8s-apiserver bind 0.0.0.0:6443 mode tcp option tcplog balance roundrobin server master-1 10.10.1.11:6443 check inter 3s fall 3 rise 2 server master-2 10.10.1.12:6443 check inter 3s fall 3 rise 2 server master-3 10.10.1.13:6443 check inter 3s fall 3 rise 2

bind 0.0.0.0:6443会让 haproxy 本机也监听 6443,但 kubelet 在 master 节点上访问的 6443 是给 apiserver 的,这里存在一个细节冲突。常见做法是 haproxy 监听一个独立端口如 16443,再用 keepalived 的 VIP 把 6443 流量引到各节点 haproxy,但这个架构链路长。更直接的方案是 haproxy 直接用 6443,保证后端 apiserver 也监听 6443,传输模式 tcp 纯四层转发,不做任何 HTTP 解析。

keepalived 主节点配置如下,priority 决定谁持有 VIP。

vrrp_instance VI_K8S { state MASTER interface ens3 virtual_router_id 50 priority 100 advert_int 1 unicast_src_ip 10.10.1.11 unicast_peer { 10.10.1.12 10.10.1.13 } virtual_ipaddress { 10.10.1.100/24 dev ens3 } }

interface必须改成实际网卡名,鲲鹏服务器上常见是 enp 开头或 bond 网卡,不要照抄 ens3。unicast_peer是内网里规避组播不通的稳妥写法,很多物理机交换机禁组播,靠这个字段点对点通信。virtual_ipaddress里的dev ens3限定了 VIP 落在哪块网卡,多网卡机器上必须指定。

4.2 kubeadm init 配置:controlPlaneEndpoint 与 podSubnet 的写法

物料和运行时就绪后,第一个 master 节点执行 init。不要直接跑一长串命令行参数,配置文件方式更可控,也方便离线包复用。

# kubeadm-init.yaml apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: "1.26.15" controlPlaneEndpoint: "10.10.1.100:6443" 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

controlPlaneEndpoint写的是 VIP:6443,而不是本机 IP。kubernetesVersion必须精确到补丁版本,kubeadm 会按这个版本去本地镜像列表里找对应 tag。imageRepository保持registry.k8s.io不动,因为离线包导入时保持的是这个原始路径,如果你在打包阶段改写成了内网 harbor 地址,这里要同步改,前后不一致会触发拉取失败。

podSubnet的选择要跟网络插件对应。用 Flannel 常见 10.244.0.0/16,用 Calico 默认则是 192.168.0.0/16。这两个网段将来直接决定 pod IP 分配,上线后很难改,选定后所有节点的 CNI 配置都要对齐。

kubeadm init --config=kubeadm-init.yaml --upload-certs | tee /tmp/kubeadm-init.log

--upload-certs是关键参数,它会把控制面证书加密上传到集群,后续追加 master 节点时要靠它生成 certificate key,这一步日志要妥善保存。init 完成后按日志提示拷贝 kubeconfig:

mkdir -p $HOME/.kube cp /etc/kubernetes/admin.conf $HOME/.kube/config kubectl get nodes

如果这里kubectl get nodes能看到 master 节点但状态 NotReady,别急着排查,先装 CNI。CNI 装完节点才会 Ready。

4.3 追加 master 和 worker:control-plane join 与普通 join 的区别

高可用集群的最小规模是三个 master,所以 join 要分成两种形态。追加 master 的 join 命令由 init 日志给出,典型如下。

kubeadm join 10.10.1.100:6443 --token xxxxx \ --discovery-token-ca-cert-hash sha256:xxxx \ --control-plane --certificate-key xxxx

追加 worker 则是同一段前面的部分去掉两个参数。

kubeadm join 10.10.1.100:6443 --token xxxxx \ --discovery-token-ca-cert-hash sha256:xxxx

--control-plane告诉 kubeadm 这是第三个 master,会多执行控制面组件部署和 etcd 扩展。--certificate-key对应 init 时的--upload-certs,没有了它 master join 会要求手动把证书拷贝过去。另外注意 token 默认有效期只有 24 小时,离线交付节奏慢时就容易踩这个坑。超时后用kubeadm token create --print-join-command重新生成,certificate-key 也要重新获取。

master 追加完成后,可以用kubectl get nodes看到三台 master 都在,且kubectl get pods -n kube-system里没有 CrashLoopBackOff。如果第三台 master 的 etcd 一直起不来,多半是证书 key 错了或 etcd 数据目录残留,清掉/var/lib/etcd再重新 join 最直接。

5. 离线部署翻车排查:4 个高频问题与定位清单

5.1 ctr 导入了镜像,kubelet 仍报拉取失败?

现象:kubeadm init 执行到一半,kubelet 日志里反复出现Failed to pull image "registry.k8s.io/pause:3.9",但你在本机用ctr images ls能看到 pause 镜像。原因:镜像被导入了default命名空间,而 kubelet 走 CRI 查的是k8s.io。解决:用crictl images看 kubelet 视角,如果列表为空,说明命名空间错了,重新执行ctr -n k8s.io images import /opt/offline/k8s-images.tar。这类工具里,导入脚本必须硬编码-n k8s.io,不能依赖默认值。

5.2 kubeadm init 卡在 the apiserver is not healthy after 4m0.00747357s

现象:init 日志里出现[kubelet-check] The HTTP call equal to 'http://localhost:10248/healthz' failed,并伴随the apiserver is not healthy after 4m0.00747357s。这一步主要不是 apiserver 本身的问题,而是 kubelet 健康检查不过。原因按出现频率排:pause 沙箱镜像缺失或 tag 不匹配,导致所有静态 Pod 起不来;containerd 的 SystemdCgroup 和 kubelet 不一致;本机 6443 端口被安全策略挡住。

解决顺序先看沙箱:crictl ps -a | grep pause,如果没输出,回第 3 章确认命名空间导入。再看journalctl -u kubelet --since "5 minutes ago"里的最后 20 行,如果提到sandbox image ... not found,把 pause:3.9 重新导入;如果提到Failed to start CRI,改 SystemdCgroup 后重启 containerd。最后检查systemctl status firewalld,麒麟 V10 在某些模板里默认开启 firewalld,6443 被 drop 时表现为健康检查超时,临时systemctl stop firewalld测一下即可定位。

5.3 ARM64 节点装进了 x86 架构镜像:manifest unknown

现象:ctr images import成功,ctr images ls也看得到镜像,但容器一直创建失败,事件报manifest unknown或failed to resolve reference ... not found。原因:拉镜像的打包机是 x86,导出的镜像平台是 amd64,ARM64 节点识别到 manifest 里没有 linux/arm64 入口就拒绝使用。解决:换一台 ARM64 打包机重新拉取导出,或者用支持跨架构拉取的 skopeo 显式指定 arch=arm64;导出后做一次crictl pull registry.k8s.io/pause:3.9验证节点能拉到,再跑集群初始化。交付包的 README 里我会刻意写上「离线包在 ARM64 环境生成,禁止跨架构拷贝镜像目录」这一条。

5.4 节点 join 成功但 NotReady:CNI 镜像与 podSubnet 冲突

现象:kubectl get nodes显示节点 NotReady,kubectl get pods -n kube-system里 coredns 一直 Pending,calico-node Pod 处于 CrashLoopBackOff。原因主要有两类:第一,离线包里根本没有包含 CNI 镜像,kubelet 的 pod 网卡创建失败;第二,podSubnet 写的是 10.244.0.0/16,但 Calico 默认 IP 池也是它,两者一般能对上,真正容易乱的是多网卡机器上 Calico 自动探测到 VIP 所在网卡,导致 BGP 和隧道全部错乱。

解决:先确认crictl images | grep calico输出是否完整,缺镜像就把 calico-images.tar 导入;如果镜像齐全,查看kubectl logs -n kube-system calico-node-xxxx,定位到报错后,在 calico 安装 yaml 的环境变量里加IP_AUTODETECTION_METHOD=interface=ens.*,把 VIP 所在网卡排除掉,再滚动重启 calico 节点 Pod。NotReady 状态里还有一种特殊情况是 node 本身资源不足,kubelet 启动后压力过大被驱逐,kubectl describe node xxx能看到对应的压力条件。

6. 上线前最后两件事:VIP 故障切换验证与证书续期演练

6.1 不停机验证 VIP 漂移与 apiserver 探活

集群交付不等于验收通过。我会在三个 master 节点分别确认 VIP 归属,然后用一个循环请求观察 VIP 切换时集群控制面是否短暂中断。

# 在任意一台能访问 VIP 的机器上执行 while true; do curl -sk -o /dev/null -w "%{http_code}\n" https://10.10.1.100:6443/healthz sleep 1 done

另开一个窗口在 VIP 当前所在的 master 上执行systemctl stop keepalived,观察上面循环里 healthz 的返回。正常情况会断几个请求,VIP 漂移到下一台 master 后恢复 200。这里如果出现长时间中断,问题多在 haproxy 后端check参数没生效,haproxy 不摘除故障节点导致流量打到坏后端。

6.2 kubeadm certs renew all 之后的静态 Pod 重启细节

K8S 1.26.15 的证书默认一年到期,生产集群上线前我会把续期流程完整演练一遍。核心命令只有两个,坑在续期后静态 Pod 不会自动重启。

# 续期所有证书 kubeadm certs renew all # 查看到期时间,验证续期成功 kubeadm certs check-expiration

续期生成的证书是新的,但运行中的 apiserver、controller-manager 等静态 Pod 还持有旧证书文件的内存副本。最稳的做法是把/etc/kubernetes/manifests/下的 yaml 文件临时移走,等 kubelet 清理掉旧容器后再移回来,触发用新证书重建。

mkdir -p /tmp/manifests-backup mv /etc/kubernetes/manifests/*.yaml /tmp/manifests-backup/ sleep 5 mv /tmp/manifests-backup/*.yaml /etc/kubernetes/manifests/

这个技巧我每次做证书维护都用,比 crictl stop 一个个容器要干净。整个操作期间保持一个已认证的 kubectl 窗口,重启完确认kubectl get nodes和kubectl get pods -n kube-system全部恢复。离线工具的最后一公里,往往不是脚本本身,而是这些验收细节,希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进