简介:面向银河麒麟Server-V10与ARM架构服务器的Kubernetes v1.23.4完整离线部署资源,适用于无法连接外网或安全要求较高的机房、内网及国产化替代等场景,可帮助运维与研发人员完成容器集群的快速准备与部署。压缩包共118个文件,整体约664MB,包含28个RPM格式安装包、23个tar组件镜像包、13个shell辅助脚本以及4个YAML资源配置文件:RPM提供系统服务与基础依赖,tar包覆盖kube-apiserver、kube-controller-manager、kube-scheduler、kubelet和kube-proxy等核心镜像,sh脚本用于环境初始化和安装编排,YAML文件预置了Pod、Service、Deployment等对象定义。此外还附带Word版安装文档,按环境检查、依赖安装、配置调整、服务启动与验证的顺序详细展开,基本可脱离互联网完成整套K8s集群搭建。该资源已有3501人学习浏览,对需要离线交付、ARM平台适配或国产操作系统迁移的团队而言,能显著降低实际部署与排障成本。
1. 离线包的真正价值:ARM 环境上最稀缺的不是 k8s,而是镜像
k8s-v1.23.4-arm版本的离线包,说白了就是一套在无外网的 ARM 服务器上把 Kubernetes 1.23.4 跑起来所需的全部二进制和容器镜像的集合。很多团队第一次接触它,是在信创、内网隔离或边缘机房场景:机器是鲲鹏 920、飞腾 S2500 或 RK3588,架构是 aarch64,手里只有一块能拷贝文件的上网机。你很快会发现,kubeadm 安装本身不需要外网,真正的黑匣子是那些镜像——pause、etcd、coredns、网络插件,每一层都在依赖 registry.k8s.io。这套离线包解决的就是「镜像怎么来、依赖怎么补齐、版本怎么选」三个问题,适合一个人在没有外网的情况下白手起家把集群拉起来。下文你会看到,成功的关键不是 k8s 本身的配置,而是对 ARM 镜像的甄别、版本配对和网络插件的选择。
2. 版本拆解:为什么偏偏是 v1.23.4,以及 ARM 离线包到底装什么
2.1 v1.23.4 作为 ARM 落地版本的技术理由
先回答一个选型问题:为什么很多人执着于 v1.23.4 的 ARM 包,而不是直接上新版。v1.23 是 k8s 历史上一个非常特殊的分水岭版本。它是最早一批广泛支持 cgroup v2 和 containerd 1.6 配套的稳定版本,又是最后一个附带 pod 安全策略(PSP,Pod Security Policy)可用的发布线,同时它的 kubeadm 初始化逻辑相对温和,对早期 ARM 服务器上常见的旧内核、旧 systemd 兼容性更好。在各行业做 ARM 迁移的时候,很多底层基础环境停留在 2022 年左右的基线,v1.23.4 恰好是那个波长上最稳的落点。更新的 1.24+ 要求移除 Dockershim 后必须全套 containerd 或 CRI-O,对离线场景来说依赖面反而更宽;更早的 1.22 又缺少对 ARM 多核 NUMA 场景的一些调度优化。所以 v1.23.4 是「兼容老硬件、配合新运行时、社区文档最多」三者的交集。
从二进制角度拆,一个完整的 ARM 离线包至少包含三类东西:
- kubeadm、kubelet、kubectl 三个二进制,必须是 arm64 版本,这一点排查起来最隐蔽——很多人下载了 amd64 的包手工替换,导致 kubelet 直接 segfault;
- 一组系统镜像,包括 registry.k8s.io 下的 kube-apiserver、kube-controller-manager、kube-scheduler、etcd、coredns、pause 等,全部需要 arm64/v8 架构标签;
- 容器运行时的离线安装介质,绝大多数场景是 containerd 的 arm64 deb/rpm 包及其依赖。
这个三角构成缺一个,离线包就是废包。我见过太多人在网上找到「k8s-v1.23.4.tar」压缩包,导入镜像后发现 pause 拉不起来,因为那个包是 x86 架构的,根本没注意到 digests 里的架构区分。
2.2 ARM 架构标签:arm64、aarch64、armhf 的边界
这是离线包踩坑的大头。arm64和aarch64是同一个东西,都是 64 位 ARM 指令集,容器镜像的 architecture 字段通常写arm64,而操作系统的 uname 输出是aarch64,二者完全可以互通。但armhf不是——它是 32 位 ARMv7 指令集带硬浮点,和 arm64 完全不兼容。如果目标硬件是树莓派 4 或 RK3399,那跑 64 位系统没问题;如果目标硬件是树莓派 3B 老系统或某些 32 位 only 的网关设备,那你需要的其实是armhf版,但这个版本在 k8s 1.20 之后官方就不太维护了,各个镜像的 armhf 标签经常缺失。所以拿到离线包第一件事:在目标机器上跑uname -m确认是aarch64还是armv7l,如果是后者,v1.23.4 的官方镜像大概率缺号,需要考虑换发行版方案而不是死磕 k8s。
容器镜像层面的架构识别靠的是 OCI 索引,docker manifest inspect或者skopeo inspect能看到architecture: arm64字段。离线包如果是从某台 ARM 机器上直接docker save出来的,那它天然是 arm64 的,不需要额外确认。但如果是自己组装离线包、从网上下载各个 layer 再合成的,就务必要逐个镜像检查。另一个容易被忽略的坑是「多架构镜像」——pause:3.9是 multi-arch 的,在 x86 机器上 save 下来再把 tar 拷到 ARM 服务器上 import,内容是完整的多架构索引还好,但如果你用了--platform参数只拉 arm64 层,然后在 x86 上 save,那个 tar 就只剩一个镜像标签指向 arm64 层,这在离线环境反而是合理的。我一般建议原机房拉取时直接加--platform linux/arm64并保存全套 manifest,避免里面混进amd64引用记录,导致离线导入时遇到个别节点去查run.googleapis.com之类的远程 registry 而出错。
注意:术语上请统一用
arm64,不要用aarch64-linux-gnu这类工具链术语去搜镜像,因为 Docker Hub 的架构标签是arm64,而aarch64只在uname -m和某些编译环境里出现,混用很浪费时间。
2.3 离线包编制流程:从干净仓库到 tar 文件的整体链路
不管你是从网上找现成离线包,还是自己编一个,链路都是类似的。按「二进制 → 镜像 → 运行时 → 部署脚本」四层来做。第一步,在有外网的 ARM 开发机或 x86 开发机上准备一套「净化」的镜像仓库,把 v1.23.4 控制面组件、coredns、pause 的网络插件镜像统一放进去。第二步用skopeo copy或docker save把镜像导出为 tar,注意docker save会把多个镜像压成一个 tar,而ctr -n=k8s.io images import导入时会丢失 repotag 的docker.io/前缀,这是另一个坑。第三步编译或者下载 kubeadm、kubelet、kubectl 的 arm64 二进制,chmod +x,放入 bin 目录。第四步做一个 preload 脚本,把镜像注入到 containerd 中,再把组件二进制复制到/usr/bin,把 systemd unit 落好。这样整个离线包目录结构就清晰了:
k8s-offline/ ├── bin/ # kubeadm/kubelet/kubectl, arm64 ├── cri/ # containerd 及依赖 deb/rpm 包 ├── images/ │ └── k8s-v1.23.4-arm64.tar # docker save 的多镜像 tar ├── config/ │ ├── kubeadm-init.yaml │ └── containerd/config.toml └── scripts/ ├── 01-load-images.sh ├── 02-install-binaries.sh └── 03-init-cluster.sh这套结构覆盖了大多数生产预案。下一步就是把它落到每一条命令上。
3. 在 ARM 服务器上跑通离线安装:镜像仓库加载与 kubeadm 最小化配置
3.1 离线镜像的导入:ctr的一条命令为什么比 docker 更适合
离线包装载必须先落地 containerd,因为 kubeadm 1.23 默认检测的是 CRI socket,而 docker 只是兼容入口。用ctr命令导入镜像到 k8s 命名空间是标准做法。这里要强调-n=k8s.io这个参数的重要性:kubelet 通过 CRI 读取镜像是从 containerd 的k8s.io命名空间去查的,如果你import 到了默认default命名空间,kubelet 会报image not found,这个错引发的「coredns CrashLoopBackOff」曾让很多人误判为镜像损坏。
# 在每台 node 上执行,将离线包中的 tar 导入 containerd 的 k8s.io 命名空间 sudo ctr -n=k8s.io images import k8s-v1.23.4-arm64.tar # 验证是否导入完整,关键看 REPOSITORY 字段是否保留了完整的 registry 前缀 sudo ctr -n=k8s.io images list | grep -E 'kube-apiserver|pause|etcd'导入完成后,建议不要直接用kubeadm config images list去核对,因为它的输出格式和镜像仓库的 tag 未必完全一致。更可靠的做法是把kubeadm-init.yaml中imageRepository指向你本地导入时的原始仓库名。很多离线包把镜像 tag 改成了172.16.0.2:5000/...的内部仓库格式,这没问题,只要kubeadm init配置里的imageRepository和之一致即可。我遇到过好多机器,因为原始 tar 里pause:3.9被docker save打成了pause:3.9(没有仓库前缀),而kubeadm默认去找registry.k8s.io/pause:3.9,导致 init 时永远卡在Pulling,所以ctr命令加--no-unpack不需要,直接让他解包就行。
3.2 非高可用初始化配置文件一览
拿到离线包后,原生二进制和镜像齐了,剩下的就是给 kubeadm 写一套适配 ARM 的 init 配置。ARM 机器常见情形是 swap 未关、cgroup driver 不匹配。配置如下:
apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.1.10 bindPort: 6443 --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.23.4 imageRepository: registry.k8s.io controlPlaneEndpoint: 192.168.1.10:6443 networking: podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/12 --- apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration cgroupDriver: systemdcgroupDriver: systemd这个参数是 ARM 环境的最大加分项。因为大多数 ARM 服务器操作系统(openEuler、麒麟、Ubuntu 22.04)都默认用 systemd 作为 cgroup 管理器,而/etc/containerd/config.toml里的SystemdCgroup = true必须和它配对,否则 kubelet 与 containerd 之间会报「failed to run Kubelet: failed to validate kubelet cgroup driver = cgroupfs, but the CRI implementation is systemd」这个错。advertiseAddress要填内网实际 IP,不能填 127.0.0.1,ARM 服务器尤其要注意 eth0 是否绑定到了enP4p1s0这种接口名,IP 写错会导致 apiserver 健康检查 4 分钟超时,也就是热词里那条 「the api server is not healthy after 4m0.00747357s」。
3.3 内核参数与 swap 的预置检查
执行kubeadm init之前,ARM 服务器有两样必须处理:swap 必须全关,防火墙或安全组策略可能干扰 6443/2379 端口。更关键的内核参数是net.bridge.bridge-nf-call-iptables和net.ipv4.ip_forward在 ARM 上经常默认关闭,而 containerd 网络流量走的是 iptables NAT,不改这两个参数,pod 之间通不了。
# 调整内核参数 sudo tee /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 sudo sysctl --system这里有个 ARM 特有的细节:很多 ARM 服务器带br_netfilter内核模块,但modprobe br_netfilter不加载,即使上面参数设了 1,bridge 网桥的数据包也过不了 iptables。所以还要执行sudo modprobe br_netfilter,并且把它写入/etc/modules-load.d/k8s.conf保证重启后还在。不做这步,flannel/calico 的 VXLAN 流量大概率黑火。
4. 离线环境如何选网络插件:calico 与 flannel 在 ARM 集群的配置差异
4.1 网络插件镜像的架构属性与导入方式
k8s 网络插件是离线部署的重灾区。很多离线包只包含 kubeadm 默认的控制面镜像,放弃了 CNI 插件,导致集群 init 完成后 node 全部 NotReady。ARM 离线包成败,跟网络插件是否选对强相关。这里有两个阵营:flannel 走 VXLAN,简单纯三层,适合单网段内网;calico 走 BGP + overlay,功能全,但镜像重、ARM 版本需要更多调试手段。无论选哪一个,关键在于镜像仓库的完整名字。比如常用的是docker.io/flannelcni/flannel:v0.21.0和docker.io/calico/cni:v3.25.0等。离线包下载时你要确定一个网络插件版本和 k8s 1.23.4 兼容,最稳妥是对照官网给出的 tested versions 表格。
导入方式和ctr的语法完全一致,不需要额外改名,但 calico 的 manifest 里通常imagePullPolicy: Always,离线环境下如果镜像不预先在本地,kubelet 永远会尝试远程拉取,这就会看到事件里一直转圈。解决办法是创建 manifest 时给 deployment 的容器打个补丁,把imagePullPolicy改成IfNotPresent。这种小坑在离线部署中最磨人。
4.2 flannel 在 ARM 上最小化启动的 configmap 调整
如果你在内网小规模环境(10 台以内),我建议用 flannel。它的优势是简单,arm64 的 flannel 镜像官方维护得很好,VXLAN 的封装对 CPU 开销虽高,但 ARM 服务器单核性能弱的问题下,简单即正义。flannel 的 manifest 默认从docker.io/flannelcni/拉镜像,离线包导入后直接kubectl apply -f kube-flannel.yml。部署完后,常见问题是在 ARM 上因为publicIP判断错误导致 node 之间无法互通,这时可以在 manifest 的环境变量里加:
env: - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: FLANNEL_NETWORK value: "10.244.0.0/16"这一行配置不是必选,但当节点网卡有多个 IP(这是 ARM 服务器常见现象,比如 BMC 管理口和业务口都在同一台机器),flannel 选错接口时,用--iface=eth0参数强制绑定。这个参数可以从 flannel daemonset 的 args 里加,也可以直接改/etc/kube-flannel/net-conf.json,后者更直接。
4.3 calico 在 ARM 的架构完整性验证与 BGP 模式
如果你的集群要跑生产负载、需要网络安全策略,那用 calico。但 calico 在 ARM 上有一个「看起来对,实则不全」的坑——它的镜像 calico/cni、calico/node、calico/kube-controllers 三个必须同时存在,而且 tag 必须一致。三者经常出现一个是 arm64 的 pull 到了,另一个 pull 不到或 fallback 到 amd64。离线包在制作时可能没注意到 calico 对flexvol和cni是独立 entrypoint 的,不齐会导致自动为 node 打上NoSchedule的污点,而 pod 调度不上去。部署前用ctr images list | grep calico把这三个 tag 对齐,再 apply manifest,这差不多是十次里面九次的坑。
5. 离线装 k8s 后必冒烟的 4 个坑:直接从现场扒出来的现象、原因与止血手段
5.1the api server is not healthy after 4m0.00747357s
现象:kubeadm init卡住 4 分钟后直接失败,日志结尾就是这条。
原因:apiserver 的容器起不来或健康检查连不上。ARM 服务器上常见根因是 pause 镜像缺失导致 apiserver 容器无法创建。这个报错很多人以为是 kubelet 问题,但本质是kube-apiserver静态 Pod 启动时,先要拉 pause,而离线包没导入 pause。另一层原因是advertiseAddress配了本机不存在的 IP,apiserver 监听失败。
解决:先ctr -n=k8s.io images list | grep pause,没有就导入;确认后再kubectl get pods -n kube-system -o wide看 apiserver 的容器事件,如果监听地址错了,改InitConfiguration里的localAPIEndpoint.advertiseAddress。最后用kubeadm reset -f清理后重新 init。这个错在热词里出现频率这么高,说明十有八九都是前两步没走对。
5.2 coredns 一直 CrashLoopBackOff,日志是less且无 error
现象:coredns 调度起来,但反复重启;describe pod 看事件只有Back-off restarting failed container,日志里没内容。
原因:arm64 架构下 coredns 的 configmap 默认使用forward . /etc/resolv.conf,而宿主机的/etc/resolv.conf是nameserver 127.0.0.1或空文件(很多 ARM 服务器默认 systemd-resolved 的 stub),coredns 无法上游转发。
解决:把/etc/resolv.conf里的 nameserver 改成真实 DNS(如 114.114.114.114 或内网 DNS),或者在 coredns 的 configmap 里直接写forward . 10.0.0.2。更进一步,可以将kubelet的--resolv-conf指向一个手工生成的 resolv.conf。不少 ARM 机器上的 systemd-resolved 与容器网络不兼容,这是离线包环境里的隐形杀手。
5.3 node 显示 NotReady,但kubelet服务在跑
现象:kubectl get node看到节点 NotReady,systemctl status kubelet又显示 active。
原因:kubelet 起不来,通常是因为/var/lib/kubelet/config.yaml里的cgroupDriver与 containerd 不一致,或者 CNI 插件没有正确安装。ARM 上还有一种可能是 CRIConfig 里sandbox_image指向registry.k8s.io/pause:3.6,但离线包只有 pause 3.9。
解决:journalctl -u kubelet -n 50直接看真实错误,确认 cgroup driver 或 sandbox_image 哪个不匹配;改 containerd 的/etc/containerd/config.toml里sandbox_image = "registry.k8s.io/pause:3.9"后重启 containerd,再重启 kubelet。这个错在 x86 上也有,但 ARM 的 kubelet 有时默认从/etc/default/kubelet读取环境变量配置,那里的KUBELET_EXTRA_ARGS可能会覆盖掉初始化的 config,务必检查。
5.4 离线包解压后 tar 导入报archive/tar: invalid tar header
现象:ctr images import xxx.tar直接报错,说 tar 头无效。
原因:制作离线包时用了docker save但 tar 包在压缩状态下直接保存,或者跨平台传输时用了 FTP 的 ascii 模式导致二进制损坏。另一个常见原因是为了「省事」把多个 tar 直接cat拼接了。
解决:先用file xxx.tar看是不是 gzip 压缩,.tar是未压缩而.tar.gz是压缩的,ctr能处理 gzip 但需要后缀名正确。拼接的 tar 必须通过临时目录解开后重新tar cf打包。这里提醒离线包使用者:最稳妥的开箱方式是skopeo copy docker://registry.k8s.io/pause:3.9 dir:/tmp/pause以后用 sync 目录方式而非 tar 传递。
6. 离线包理论基于「自举」:一个人怎么把 ARM 离线包升级成运维资产
当你把这套离线包跑通、集群 Ready,后续的维护才是亮点。这一章不写安装步骤,而是写一个「离线包自维护」的实践:拿到手之后,不仅解包,还要有验证手段,知道它跟线上版本差在哪儿。
先建立集群健康检查基线。最小验证是三个:kubectl get nodes全是 Ready、kubectl get pods -n kube-system全 Running、kubectl run busybox --image=busybox:1.34 -it --rm -- /bin/sh里 ping 一下 DNS 能通。这三条过了,离线包的核心链路基本没问题。但更进一步,我会建议写一个check.sh脚本,把ctr images list的 grep 结果存成清单文件,和 kubeadm 的config images list --config做 diff,这个 diff 能准确暴露出「多版本休眠镜像」这类污染源。
镜像仓库的内网化是离线包永恒的下一步。不建议把 tar 永远留在每台机器上手动敲ctr import。常见做法是在内网装一个 Harbor 或只读 registry,把离线包里的镜像全部 push 进去,然后修改每台节点的/etc/containerd/config.toml中registry.configs以及kubeadm-init.yaml的imageRepository: 172.16.0.3:8443。这样一来,以后加节点只需给新机器装 containerd,其他镜像全从内网 registry 拉,不用再拷十 GB 的包。ARM 在 registry 这块没有任何特别,无非是对--platform的区分要更敏感。
我最想分享的是一个血泪习惯:离线包拿到手不要信「最新」「全量」的标签,第一件事在测试机(或者原地一台新节点)跑一遍kubeadm init,全程开着journalctl -u kubelet -f看日志。任何宣称兼容 ARM 的离线包,如果连 init 都跑不顺,直接丢弃,不要修——因为镜像的架构错误不是配置能修回来的,是编译源头就错了。这个「先 trusted、后 confirmed」的判断方式,让我少走了太多弯路。另外一个习惯是把离线包的版本记录跟内核版本、系统版本绑定存档,比如「中兴服务器 + 麒麟 V10 SP2 + 内核 4.19 + k8s-v1.23.4-arm64.tar + flannel v0.21.0」是一组已验证组合,换掉任意一项都要重新走一遍验证流程。ARM 的玄学就在这——x86 换个内核版本大概率还能跑,ARM 不敢赌。
希望这套方案能让你手里的 ARM 集群一次性爬起来,不必再反复kubeadm reset。
本文还有配套的精品资源,点击获取