
简介面向银河麒麟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 安全策略PSPPod 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 -nk8s.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 命名空间是标准做法。这里要强调-nk8s.io这个参数的重要性kubelet 通过 CRI 读取镜像是从 containerd 的k8s.io命名空间去查的如果你import 到了默认default命名空间kubelet 会报image not found这个错引发的「coredns CrashLoopBackOff」曾让很多人误判为镜像损坏。# 在每台 node 上执行将离线包中的 tar 导入 containerd 的 k8s.io 命名空间 sudo ctr -nk8s.io images import k8s-v1.23.4-arm64.tar # 验证是否导入完整关键看 REPOSITORY 字段是否保留了完整的 registry 前缀 sudo ctr -nk8s.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.1ARM 服务器尤其要注意 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不加载即使上面参数设了 1bridge 网桥的数据包也过不了 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 选错接口时用--ifaceeth0参数强制绑定。这个参数可以从 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配了本机不存在的 IPapiserver 监听失败。解决先ctr -nk8s.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 的 stubcoredns 无法上游转发。解决把/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看到节点 NotReadysystemctl 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 --imagebusybox: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。本文还有配套的精品资源点击获取