Kubernetes离线部署实战:内网环境搭建开发测试集群的完整指南
2026/9/16 4:16:20 网站建设 项目流程

我们做项目交付或者内部研发的时候,经常会碰到一个很头疼的场景:客户的机房是内网隔离的,没有外网,更不可能让你随便连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 = true

5.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: 70

5.4 常用问题排查速查表

我把实际操作中最高频的几个问题整理成表格,方便你们照着排查:

症状可能原因排查命令处理方法
Pod一直ContainerCreating镜像拉取失败或存储卷挂载失败describe pod;kubectl logs确认镜像路径,检查存储类型
节点NotReadyCNI未就绪或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混合环境时,系统默认时间格式和时区如果不一致,后面排查超时问题会让你怀疑人生。

这套离线部署方法,我已经在不同的项目里验证过多次,从两三个节点的开发测试集群,到几十个节点的业务预生产环境,都跑得比较稳定。如果你正好要搞离线部署,希望这篇能帮你少走点弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询