我们做项目交付或者内部研发的时候,经常会碰到一个很头疼的场景:客户的机房是内网隔离的,没有外网,更不可能让你随便连Docker Hub和Google的镜像仓库。但业务又跑在Kubernetes上,还得在开发测试环境里把整套东西落地。我这些年经手了不少这类环境,从最早手动rpm一个个装,到后来整理出一套相对标准的离线部署流程,踩过的坑确实不少。这篇就专门聊一下,Kubernetes离线部署到开发测试环境的完整操作思路和实操细节。
这篇文章适合谁看?准备在内网、隔离网络、或者临时无外网环境搭K8s集群的运维和开发同学,也包括需要在离线环境部署Superset、OnlyOffice、甚至大模型推理服务的场景。总体思路是——把“在线安装”里所有依赖的外部资源,提前打包成内网可用的离线物料,然后通过本地仓库分发。整个过程看起来环节多,但只要把物料清单理清楚,实际执行起来比在线安装还稳,因为不受网络波动影响。
1. 项目背景与离线部署的整体思路
1.1 为什么开发测试环境也要做离线部署
很多人觉得,开发测试环境嘛,机器都在公司内网,能上外网,直接在线装不就行了?但真实情况往往不是这样。大一点的公司都有安全策略,研发网和办公网隔离,生产环境更是严格禁止外网连接。开发测试环境作为生产环境的预演,很多时候也需要模拟和它一致的部署条件,这就有两个刚性需求:一是环境一致性,二是可复现性。
还有一个容易被忽略的点——软件供应链的确定性。在线部署Kubernetes集群,版本是浮动的,今天装可能是v1.28.3,过两个月再装就成了v1.28.5,如果刚好碰到上游仓库调整或者依赖版本不兼容,整个环境都起不来。离线部署把所有的安装包、镜像、依赖都固化在一个物料包里,版本一锁定,什么时候装、由谁来装,结果都是一样的。这个确定性,对开发测试环境的稳定性非常关键。
1.2 离线部署的范围边界与核心挑战
离线部署Kubernetes,要处理的东西可以拆成三层:一层是二进制和系统依赖,一层是容器镜像,最后一层是配置和运维工具。
具体到组件,包括kubeadm、kubelet、kubectl三个核心二进制,容器运行时(containerd或Docker),CNI网络插件(Flannel或Calico),CoreDNS等集群插件,以及后续要用的Ingress Controller、存储插件、监控组件。如果还要跑应用,那Superset、OnlyOffice、DeepSeek这类服务的镜像也得一并准备好。
离线部署的核心挑战,一句话概括就是“物料管理”。在线安装时,kubeadm会自动到官方仓库拉取所需镜像,网络通就行;离线环境下,这些全都得自己解决。镜像的导出、导入、版本对齐,是最容易出问题的环节。比如你在联网机器上docker pull拉取的镜像,版本必须和kubeadm默认拉取的版本严格一致,否则kubeadm init的时候会直接报错,连不上镜像仓库。
2. 离线部署前的物料准备与基础环境配置
2.1 基础运行环境与版本选型
离线部署第一步,是确定一套基础环境的“黄金组合”。我在多个项目里验证下来,比较稳的组合是:操作系统用CentOS 7.9或Rocky Linux 8.x,架构x86_64,内核版本不低于3.10,Kubernetes版本选1.23到1.28之间的稳定版本,容器运行时选containerd 1.6以上版本,网络插件用Flannel或Calico二选一。
为什么把版本范围框得这么死?因为新老版本之间,API和能力变化很大。比如Kubernetes从1.24版本开始彻底移除了dockershim支持,如果你用1.24以上版本还打算直接用Docker作为运行时,就需要额外装cri-dockerd适配器。开发测试环境追求的是稳定可复现,没必要追新,选一个自己熟悉的稳定版,把流程跑通比什么都重要。
两个关键选型逻辑先说明白:容器运行时,新版本建议直接上containerd,少一层Docker到containerd的转换,排错也简单;网络插件,Flannel配置简单、适合开发测试环境,Calico功能强大但配置项多,离线场景下建议先用Flannel跑通基础集群,后续再按需切换。
2.2 系统初始化与离线源配置
在开始装Kubernetes之前,系统层面的初始化必须先做好,这些操作和在线部署一样,但有几个注意事项。操作系统装好后,需要关闭SELinux,关闭swap,配置好主机名和hosts解析。具体操作如下:
# 关闭SELinux sudo setenforce 0 sudo sed -i 's/^SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config # 关闭swap sudo swapoff -a sudo sed -i '/ swap / s/^/#/' /etc/fstab # 主机名与hosts sudo hostnamectl set-hostname k8s-master01 cat >> /etc/hosts <<EOF 192.168.10.11 k8s-master01 192.168.10.12 k8s-node01 192.168.10.13 k8s-node02 EOF内核网络参数也需要调整,主要是让iptables能正确处理K8s的流量转发:
cat <<EOF | sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF sudo modprobe overlay sudo modprobe br_netfilter cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-iptables = 1 net.ipv4.ip_forward = 1 net.bridge.bridge-nf-call-ip6tables = 1 EOF sudo sysctl --system这里我踩过一个坑:如果在云服务器上做,有些云厂商的安全组策略会影响overlay网络通信,K8s集群即使起来了,跨节点的Pod网络也可能不通。所以在初始化之前,先确认节点之间的端口放行情况,TCP 6443、2379-2380、10250、10255这些端口必须互通。
2.3 离线软件物料清单的整理
这是离线部署最核心的一步,要把所有需要用到的安装包、镜像提前准备好。我一般会建一个offline-k8s目录,目录结构大概是这样的:
offline-k8s/ ├── bin/ # kubeadm, kubelet, kubectl ├── images/ # 所有容器镜像tar包 ├── rpm/ # 系统依赖包 ├── containerd/ # 容器运行时安装包 ├── cni/ # CNI插件二进制 └── helm/ # Helm工具及安装包rpm依赖包怎么收集?找一个同样操作系统的能联网的机器,用yumdownloader把需要的包全部下载下来,再拷贝到离线机器上。
yum install -y yum-utils yumdownloader --resolve --destdir=/tmp/k8s-rpm kubeadm kubelet kubectl镜像准备这里多说几句。Kubeadm初始化集群时,会拉取一堆镜像,包括kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy、coredns、etcd、pause。具体版本可以先用kubeadm config images list --kubernetes-version=v1.28.2查出来,然后在联网机器上拉取并导出:
kubeadm config images list --kubernetes-version=v1.28.2 docker pull registry.k8s.io/kube-apiserver:v1.28.2 docker save registry.k8s.io/kube-apiserver:v1.28.2 -o kube-apiserver-v1.28.2.tar注意,如果你有代理或者镜像加速源,导出的镜像仓路径必须和离线环境里kubeadm配置的image-repository一致,否则kubeadm实际拉取时还是找不到。我通常建议整个集群的镜像统一导入到自建的Harbor私有仓库,然后kubeadm init时指定--image-repository=registry.offline.local,这样版本和路径都在自己的掌控之内。
3. 集群组件的离线安装与初始化
3.1 containerd与kubeadm的安装
物料准备好之后,就进入正式的安装阶段。先装containerd,我推荐用官方提供的rpm包或者tar包安装,不要用yum源自带的版本,因为版本可能比较老,和K8s的兼容性没有保证。
# 解压并安装containerd tar -C /usr/local -xzf containerd-1.7.13-linux-amd64.tar.gz mkdir -p /etc/containerd containerd config default | sudo tee /etc/containerd/config.toml这里有个关键配置要改:containerd默认的sandbox_image是指向registry.k8s.io的pause镜像,离线环境必须改成我们自己镜像仓库里的pause镜像路径。修改/etc/containerd/config.toml:
[plugins."io.containerd.grpc.v1.cri"] sandbox_image = "registry.offline.local/k8s/pause:3.9"同时,为了能让containerd正常从私有仓库拉镜像,还得在config.toml里配置好镜像仓库的endpoint和认证信息。改完配置,重启containerd:
sudo systemctl enable containerd sudo systemctl restart containerd接着安装kubeadm、kubelet、kubectl。离线机器上没法直接用yum源装,所以直接把之前准备的文件拷过去装:
# rpm包安装 rpm -ivh kubeadm-1.28.2-0.x86_64.rpm kubelet-1.28.2-0.x86_64.rpm kubectl-1.28.2-0.x86_64.rpm # 或者使用二进制方式 cp kubeadm kubelet kubectl /usr/local/bin/ systemctl enable kubelet注意,kubelet装好后默认是起不来的,因为还没有集群配置,这是正常现象,不用慌。下一步才是关键。
3.2 kubeadm init初始化与网络插件部署
集群初始化之前,先创建配置文件,这里建议不要全用命令行参数,而是写一个完整的kubeadm配置文件,方便后续追溯和重复执行。配置文件示例如下:
# kubeadm-config.yaml apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.28.2 imageRepository: registry.offline.local/k8s controlPlaneEndpoint: "192.168.10.11:6443" # VIP或Master节点IP networking: podSubnet: "10.244.0.0/16" serviceSubnet: "10.96.0.0/12" --- apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: "192.168.10.11" bindPort: 6443有个容易踩坑的点:imageRepository一定要和你在私有仓库里的路径完全匹配,包括前缀和层级。比如我在Harbor里建了k8s这个项目,镜像路径是registry.offline.local/k8s/kube-apiserver,那配置里就写registry.offline.local/k8s。如果多了或少了层级,初始化必失败。
执行初始化:
kubeadm init --config kubeadm-config.yaml --upload-certs初始化成功后,会输出一段kubeadm join命令,务必保存好。如果中途失败,不要直接重复init,先排查,用kubeadm reset清理残留,再继续。
网络插件部署,在离线环境里也要用离线镜像。对于Flannel,可以直接用它的manifest文件,把镜像地址替换成私有仓库地址后kubectl apply -f。manifest文件本身可以从联网机器上提前下载,也可以从GitHub Releases获取。我会在准备阶段把常用的Deployment YAML都下好,统一放到offline-k8s目录。
安装完成后,验证集群状态:
kubectl get nodes kubectl get pods -n kube-system全部Running状态,就说明集群基础架构搭起来了。
3.3 工作节点加入集群
工作节点的加入流程和Master节点基本一致,唯一区别是不用执行kubeadm init,直接执行join命令就行。但要注意,工作节点上的containerd配置必须和Master一致,特别是sandbox_image要指向同一个私有仓库的pause镜像。
kubeadm join 192.168.10.11:6443 --token <token> --discovery-token-ca-cert-hash sha256:<hash>如果token过期了,用下面的命令重新生成:
kubeadm token create --print-join-command还有一个常见的坑,工作节点加入后状态卡在NotReady。这时候不要慌,先看节点上的kubelet日志:journalctl -u kubelet -f,大部分情况不是CNI没部署好,就是pause镜像拉不下来,要么就是节点时间不同步。开发测试环境里节点时间不同步其实是特别常见的隐蔽问题,K8s对证书和TLS非常敏感,节点间时间差超过5分钟,很多请求都会报证书校验错误。建议在所有节点上都配好NTP或者至少手动校时。
4. 容器镜像离线导入与开发测试环境的常用组件部署
4.1 镜像仓库选型与离线镜像导入方案
集群起来了,后面的事情其实更磨人——各种业务镜像怎么在离线环境里分发。
方案一:不搭私有仓库,直接在每台节点上导入镜像。用ctr命令把导出的tar包导入到K8s使用的命名空间:
ctr -n k8s.io images import kube-apiserver-v1.28.2.tar这个方式适合集群规模小、节点少的场景,但如果你有几十个节点,每个节点手动导入,效率太低,镜像多了管理起来也是一团乱麻。
方案二(推荐):搭一个Harbor私有仓库。Harbor本身支持离线安装,安装包里包含了它自己需要的所有镜像,按照官方文档就能跑起来。装好Harbor之后,在联网机器上把需要的镜像统一打好tag,push到Harbor。然后所有节点上的containerd配置都指向这个Harbor。这样后续不管是部署Superset、OnlyOffice,还是大模型推理服务,都是标准的pull和push流程,跟在线环境没有任何区别。
Harbor离线安装包可以从官方GitHub Releases下载,里面有个harbor-offline-installer-v2.9.0.tgz,解压后修改harbor.yml,把hostname改成你的内网IP,执行install.sh就行。它依赖Docker和Docker Compose,这两样也要在离线环境下先准备好。
镜像导出和导入的过程,有一个细节特别重要:tag的完整路径。比如你要部署Superset,原始的镜像路径可能是apache/superset:4.0.1,你推到私有仓库后变成registry.offline.local/library/apache-superset:4.0.1,那么在Deployment的YAML里,image字段就必须写这个完整的私有仓库路径。很多同学在这个环节翻车,拉下来用了原始的镜像名,私有仓库又没有这个路径,自然就ImagePullBackOff了。
4.2 典型开发测试服务的离线部署实操
开发测试环境里,最常碰到的三类离线需求:BI可视化工具、在线文档服务、大模型推理。
以Superset为例,离线部署其实不复杂,但有几个坑比较典型。Superset依赖一个元数据库,默认是SQLite,生产建议用PostgreSQL,我们需要把Superset和PostgreSQL两个镜像都准备好。在联网机器上:
docker pull apache/superset:4.0.1 docker pull postgres:15 docker tag apache/superset:4.0.1 registry.offline.local/library/apache-superset:4.0.1 docker push registry.offline.local/library/apache-superset:4.0.1然后在K8s里部署一个简单的StatefulSet或Deployment,把Superset的7777端口暴露出来,配置好环境变量和持久化存储就行了。Superset需要初始化数据库,启动命令里要加superset db upgrade && superset init,这两个步骤很容易被忽略,导致服务看似起来了,但访问的时候一堆报错。
OnlyOffice的离线部署也类似,它依赖PostgreSQL、RabbitMQ和Redis,整套联动的服务比较多。这里更推荐用Helm方式部署,在联网机器上把OnlyOffice的Helm Chart和依赖的所有镜像都提前拉下来,然后离线install。OnlyOffice性能调优时要注意JVM内存参数,默认配置在开发环境够用,但如果你要测试大文档并发转换,建议单独把DocumentServer的内存调大。
4.3 大模型服务在离线K8s上的部署要点
最近的离线部署需求里,很大一部分是跑大模型推理服务,比如DeepSeek的8B模型。这类服务的核心问题是镜像大、显存要求高、推理框架版本敏感。部署在K8s上,有几点和普通服务不一样。
第一,镜像双层准备。大模型服务的镜像一般分两层概念:推理框架镜像和模型权重。推理框架镜像可以用docker pull拉取后离线导入,模型权重建议不要打进镜像里,而是用持久化存储挂载进去。原因很简单,模型文件几个GB到几十GB不等,打镜像会导致每次分发都非常慢,而用存储挂载可以一次分发、多处使用。
第二,GPU调度。如果你在开发测试环境里要验证GPU推理,节点上需要提前装好NVIDIA驱动、nvidia-container-toolkit,并且K8s集群要部署对应的Device Plugin才能识别GPU资源。离线环境装GPU的坑更多,驱动依赖的kernel-devel版本必须和当前内核完全一致,稍微不对就编译不了,需要在物料准备阶段就严格锁定环境。
第三,推理服务的健康检查。大模型启动往往要拉取和加载模型,这个过程可能持续几十秒到几分钟,如果默认的livenessProbe设置太激进,Pod会被反复重启。实际配置时我会把initialDelaySeconds设到120秒以上,先用readinessProbe确认服务真正就绪。
5. 常见问题与排查技巧实录
5.1 镜像拉取失败类问题
这是离线部署里最高频的一类问题。症状很简单,Pod的状态一直是ImagePullBackOff,describe一下能看到拉取失败的具体报错。
排查路径基本是固定的:先看镜像路径是不是合理,是不是包含了私有仓库地址;再确认节点上containerd能不能正常连通私有仓库。离线环境里,DNS配置经常被忽略。如果你的Harbor用的主机名访问,而节点上的/etc/resolv.conf没有配置对应的DNS服务器,解析就会失败。我一般会建议直接用IP地址访问Harbor,省掉DNS这层麻烦。
还有一种情况是镜像仓库HTTPS证书不受信任。containerd对HTTPS证书的校验比较严格,如果Harbor用的是自签名证书,需要在所有节点上把CA证书拷贝到/etc/docker/certs.d或配置containerd跳过证书校验。前者更安全,后者更省事,开发测试环境我会选择后者,少折腾:
[plugins."io.containerd.grpc.v1.cri".registry.configs."registry.offline.local".tls] insecure_skip_verify = true5.2 集群初始化失败与节点NotReady排查
kubeadm init失败,第一步永远是看日志,不要反复试。常见失败原因有这么几类:镜像拉不下来,apiserver容器启动失败,etcd卷权限不对,端口被占用或防火墙没关。
看etcd检查:
docker ps -a | grep etcd docker logs <etcd-container-id>如果是端口被占用,要么释放端口,要么改配置换端口。防火墙的问题,开发测试环境我通常直接关闭firewalld,省得排查这种网络不通的问题。
节点NotReady,最常见的就是CNI网络没起来。检查方式:
kubectl get pods -n kube-flannel kubectl logs -n kube-flannel -l app=flannel如果是Flannel一直CrashLoopBackOff,大概率是镜像没导入全,或者RBAC权限配置有问题。另外一个隐蔽原因是节点的主机名带大写字母或下划线,Flannel会直接不理会这种节点,建议主机名一律小写、用中划线连接。
节点时间不同步的问题,前面也提到了。在离线环境里没有公网NTP服务,我一般会在一个能访问公网的跳板机上搭NTP服务器,然后让所有节点指向它。
5.3 离线环境资源不足与调优
开发测试环境的机器配置通常不会太高,常见的瓶颈是内存。K8s集群本身的组件加上业务服务,内存很容易吃紧。有几个优化手段:
第一,Master节点不给业务调度。加一个污点,让业务Pod默认不调度到Master节点上:
kubectl taint nodes k8s-master01 node-role.kubernetes.io/master=true:NoSchedule第二,etcd的存储放在单独的盘上,避免和系统日志抢IO。开发测试环境虽然没有生产那么高的QPS,但etcd坏了整个集群的元数据都读不出来,这个底线的投入还是值得的。
第三,kubelet的垃圾回收参数。磁盘空间紧张时,K8s在imageGC和containerGC之间会左右摇摆,比如反复拉镜像导致磁盘写满。建议显式配置:
# /var/lib/kubelet/config.yaml imageGCHighThresholdPercent: 85 imageGCLowThresholdPercent: 705.4 常用问题排查速查表
我把实际操作中最高频的几个问题整理成表格,方便你们照着排查:
| 症状 | 可能原因 | 排查命令 | 处理方法 |
|---|---|---|---|
| Pod一直ContainerCreating | 镜像拉取失败或存储卷挂载失败 | describe pod;kubectl logs | 确认镜像路径,检查存储类型 |
| 节点NotReady | CNI未就绪或kubelet异常 | journalctl -u kubelet -f; 查看CNI Pod状态 | 重新部署CNI,检查镜像导入 |
| Flannel CrashLoopBackOff | 镜像版本不对或配置错误 | kubectl logs -n kube-flannel | 确认Flannel镜像版本,检查配置 |
| Kubeadm init卡在拉镜像 | 私有仓库镜像不存在 | ctr -n k8s.io images list | 导入缺失镜像,核对版本 |
| APIServer反复重启 | etcd连接失败或证书过期 | docker logs kube-apiserver | 检查etcd健康,重置集群配置 |
| 跨节点Pod网络不通 | 安全组或iptables规则 | ping Pod IP;traceroute | 检查节点防火墙和安全组 |
| 访问Service超时 | kube-proxy未正常工作 | kubectl get pods -n kube-system | 确认kube-proxy镜像,检查IPVS模式 |
这几条是离线部署中最高频的问题,自己动手搭过一遍,基本就全见过了。下次碰到同样的报错,直接照表排查,效率翻倍。
5.5 几个容易忽略但致命的细节
最后分享几个我多次踩坑后留下的操作习惯。第一,所有配置文件、镜像清单、版本号,必须集中存档,不要散落在各个服务器上。我通常是维护一份versions.md,把Kubernetes版本、containerd版本、CNI版本、所有镜像的tag、私有仓库地址全部记下来,每次部署前对照这份文档检查一遍。
第二,离线物料包一定要做完整性校验。传文件传坏了在离线环境里特别难发现,表现为安装过程中出现莫名其妙的问题,比如解压失败、二进制损坏。我会给每个tar包生成SHA256校验文件,传到目标机器后先校验一遍再开始操作。
第三,所有节点的系统时间必须统一。这个前面提过,但值得再强调一次,尤其是在有Windows和Linux混合环境时,系统默认时间格式和时区如果不一致,后面排查超时问题会让你怀疑人生。
这套离线部署方法,我已经在不同的项目里验证过多次,从两三个节点的开发测试集群,到几十个节点的业务预生产环境,都跑得比较稳定。如果你正好要搞离线部署,希望这篇能帮你少走点弯路。