前段时间帮一个研发团队搭开发测试环境,网络是全物理隔离的,所有节点都不能访问外网。说实话,在线环境下我用 kubeadm 部署 Kubernetes 已经很熟了,半小时就能拉起一个集群;但一换到离线场景,很多“理所当然”的操作全部失效——镜像没法直接 pull,系统依赖装不上,连 pause 镜像漏了都能让节点反复 NotReady。这篇文章把我在开发测试环境下做 Kubernetes 离线部署的完整过程整理出来,从物料准备、版本锁定、镜像搬运,到集群初始化、网络插件、开发测试常用组件的落地,再到排障和后面团队实际使用时的经验补充,希望能给同样被困在隔离网络里的研发、运维和测试同学一条能直接照做的路线。
需要先说清楚的是,这里我谈的不是生产级别的离线高可用部署,而是开发测试环境:它对性能、容灾要求没那么高,但对“能快速交付、稳定复现、团队上手成本低”有非常强的诉求。所以文章里很多选型都偏向务实和节省人力,比如私有镜像仓库直接用 registry:2 而不是 Harbor 全家桶,存储用 NFS 而不是去做 Ceph,这些取舍在正式生产环境里可能不成立,但在开发测试场景下,是真的好用又省心。
1. 开发测试环境为什么必须走离线部署这条路
1.1 我在实际项目里遇到的几类网络受限场景
做离线部署之前,先得搞清楚自己到底面对的是哪种“离线”,不同场景下的准备策略差别很大。
物理隔离内网。
这是我遇到最多的情况,常见于安全要求严格的研发内网。机器之间能互相通信,但整个网段没有任何公网出口,连 DNS 解析都只认内网域名。要从外部拿数据,只能通过光闸、U 盘、审批流程这种人工方式拷进去。这种环境下,你基本上要假设“所有需要的文件都必须提前带进去”,后续排障时只能靠内网已有的资源。
云上 VPC 的私有子网。
部分云环境或企业私有云会把开发测试集群放在无法访问公网的子网里,但会给你留一台可以出网的跳板机。这种场景相对宽松,可以通过跳板机搭个临时通道把镜像传输进去,也可以在跳板机上部署一个私有镜像仓库,让集群节点通过内网访问。相比物理隔离,排障空间大了很多。
临时交付和现场复现。
还有一类是展会、客户现场、实验室这种临时环境。机器可能是崭新的,也可能预装了一部分中间件,但公网不稳定或者根本没有。这种场景时间紧、任务重,通常不能让你从零开始折腾,最好提前准备好一套完整的离线交付包,到了现场直接导入运行。
这三种场景的共性是:你不能依赖“在线下载”这条路。但它们的区别会直接影响你选择镜像搬运方案——物理隔离只能用导出的 tar 包人工拷贝;VPC 环境可以用仓库同步;现场交付则要尽量自动化。
1.2 离线部署真正的难点不是“装不上”,而是“不知道缺什么”
在线部署时,报错了搜一下就能下载对应依赖,重试一下也许就过去了;离线环境不一样,很多坑是到了实际安装那一刻才暴露出来的。比如 kubeadm init 提示拉取镜像失败,你才发现镜像包没带全;kubelet 启动失败,排查半天发现系统缺了 conntrack;Pod 网络不通,检查之后发现内核模块 br_netfilter 没加载。这些在联网环境里一条命令就能解决,但在离线环境里,每缺一个东西都意味着一次人肉搬运和漫长的审批等待。
我自己的经验是,离线部署的核心工作其实不在“部署”本身,而在“前期物料盘点”。你得把安装的依赖闭环、镜像的依赖闭环、甚至配置文件的依赖闭环全部打通。下面要讲的整套流程,本质上就是在做这件事。
1.3 我建议的版本组合与物料闭环思路
离线环境里最忌讳的就是“尝鲜”。选一个太新的 Kubernetes 版本,很可能 kubeadm 的配置格式变了、容器运行时的适配还没跟上,出了问题很难在隔离环境里快速找到答案。我推荐选一个较 mature 且社区资料丰富的版本,比如 Kubernetes 1.28.x,配套工具也用与之兼容的稳定版本。
我这次使用的版本组合大致如下:
组件名 版本 用途 Kubernetes 1.28.2 集群核心 containerd 1.7.11 容器运行时 kubeadm / kubelet / kubectl v1.28.2 集群管理组件 Calico v3.27.0 Pod 网络插件 metrics-server v0.6.4 资源指标采集 Kubernetes Dashboard v7.1.1 Web 管理界面 nginx-ingress-controller v1.9.5 Ingress 控制器 pause 3.9 每个 Pod 都会用到的 sandbox 镜像
版本组合确定后,马上要建立“物料闭环”的概念:把二进制包、镜像 tar、RPM 包、内核模块、配置模板全部对齐到这些版本,在联网机器上准备成一套完整的 artifact 目录,然后整体带到离线环境。
2. 在联网机器上完成离线物料打包:版本、镜像与系统依赖
2.1 先花一小时做版本对齐,能避免后面几天的折腾
离线部署最痛苦的一件事就是版本不匹配导致的“连锁反应”。
比如你在联网机器上下载了 Kubernetes 1.28.2 的二进制,但 containerd 还是老版本,cri 接口可能不兼容;又比如你把 Calico 镜像换成了最新版,但它要求的 kube-apiserver 特性开关在 1.28 里默认没开。这些兼容性问题在在线环境可以用“升级一下组件”糊弄过去,离线环境里你手上没有新版本,就只能干瞪眼。
所以第一步一定要做版本对齐。你可以在联网机器上先用 kubeadm 生成一份镜像清单:
kubeadm config images list --kubernetes-version=v1.28.2这条命令会输出 kubeadm 初始化集群所需的全部镜像,包括 kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy、etcd、pause 等。下载这些镜像时,注意 kubeadm 默认的 imageRepository 是 registry.k8s.io,如果网络访问不了,可以从国内镜像站或代理下载后再重新打 tag。
除了镜像之外,还需要提前下载好 kubeadm、kubelet、kubectl 三个二进制文件,并且版本要和镜像版本一致。一个小技巧是,下载后用sha256sum校验一下文件完整性,避免拷贝过程中文件损坏。
2.2 系统依赖包与内核参数的离线准备
很多人在离线部署时把所有精力都放在镜像上,结果到了现场发现操作系统层面的包都缺。开发测试环境通常用的是 CentOS 7.9 或 Rocky Linux 9,建议提前准备这些包:
- tar、gzip:解压二进制包用
- socat:kubeadm 初始化时检查它是否存在
- conntrack-tools:kubelet 依赖
- ebtables、ipset、ipvsadm:iptables 和负载均衡相关
- nfs-utils:后面挂 NFS 存储会用到
你在联网的机器上准备这些包时,可以用 yumdownloader 把所有依赖包下载到本地:
yumdownloader --resolve --destdir=/tmp/k8s-rpms tar socat conntrack-tools ebtables ipset ipvsadm nfs-utils到了离线环境后,把这些 RPM 包拷贝进去,用rpm -ivh *.rpm或者yum localinstall安装即可。如果离线机器数量多,建议在其中一台建一个本地 yum 源,其他机器通过内网访问,这样效率高很多。
同样重要的还有内核模块和系统参数。Kubernetes 要求加载 br_netfilter 和 overlay 模块,否则 Pod 网络和容器镜像挂载都会出问题。离线环境下,把模块写进/etc/modules-load.d/k8s.conf,并配置好/etc/sysctl.d/k8s.conf:
net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.ipv4.ip_forward = 1需要提醒的是,有些内核模块在默认内核里可能没有编译进去,比如 overlay。如果modprobe overlay失败,说明内核不支持,最稳妥的办法是换一个包含该模块的内核版本,而不是硬着头皮继续。
2.3 镜像搬运的三种方案,以及怎么选
镜像搬运是离线部署里最重的体力活。我在不同场景下用过三种方案,这里分别说一下它们的适用条件。
方案 A:单机或小规模集群,用 export + import。
如果只是搭建一个 3 节点左右的开发测试集群,可以直接在联网机器上拉取镜像后导出 tar 包,到离线节点上再导入。以 containerd 为例:
# 联网机器上:拉取并导出镜像 docker pull registry.k8s.io/kube-apiserver:v1.28.2 docker save registry.k8s.io/kube-apiserver:v1.28.2 -o kube-apiserver.tar # 离线节点上:导入镜像到 containerd 的 k8s.io 命名空间 ctr -n k8s.io images import kube-apiserver.tar注意,这里一定要指定-n k8s.io,因为 kubelet 和 kubeadm 默认从 containerd 的 k8s.io 命名空间读取镜像。如果导入到了默认命名空间,crictl 是看不到的,kubelet 依然会认为镜像缺失。
方案 B:多节点或后续需要持续交付,搭建私有镜像仓库。
如果集群节点多,或者团队后面要在集群里频繁部署中间件,那最简单的方式是搭一个私有镜像仓库。裸机环境直接跑一个 registry:2 容器就行:
docker run -d --name registry --restart=always \ -p 5000:5000 \ -v /data/registry:/var/lib/registry \ registry:2然后把下载好的镜像重新 tag 成内网仓库地址并 push:
docker tag registry.k8s.io/kube-apiserver:v1.28.2 192.168.1.10:5000/kube-apiserver:v1.28.2 docker push 192.168.1.10:5000/kube-apiserver:v1.28.2这种模式的好处是,后续节点加入集群时不需要再拷贝 tar 包,直接配置好 containerd mirror 就能从仓库拉取,开发测试阶段往里塞中间件镜像也方便。
方案 C:超大镜像或跨机房传输,直接拷贝 tar 到节点再导入。
有些镜像是真的巨大,比如只针对大模型推理的镜像可能有 5GB 甚至更大。如果所有节点都从私有仓库拉取,可能把内网带宽打满。这种情况下我建议先在联网机器导出 tar,然后 rsync 或 scp 到目标节点,再用ctr -n k8s.io images import导入。缺点是占磁盘空间,但胜在稳。
三种方案不是互斥的,实际项目中经常组合使用。控制面组件用私有仓库,大体积中间件镜像用 tar 方式直传节点,都是很常见的做法。
2.4 内部镜像仓库搭建与 containerd 的 mirror 配置
镜像仓库建好之后,接下来要解决“kubelet 怎么知道去私有仓库拉镜像”的问题。
如果你用的是 Docker 运行时,只需要在/etc/docker/daemon.json里加insecure-registries即可。但既然我们选择 containerd,就需要修改它的配置文件。
containerd 的 CRI 插件配置文件一般在/etc/containerd/config.toml,你需要找到[plugins."io.containerd.grpc.v1.cri".registry.mirrors]这一段,按下面的方式配置:
[plugins."io.containerd.grpc.v1.cri".registry.mirrors] [plugins."io.containerd.grpc.v1.cri".registry.mirrors."192.168.1.10:5000"] endpoint = ["http://192.168.1.10:5000"] [plugins."io.containerd.grpc.v1.cri".registry.configs."192.168.1.10:5000".tls] insecure_skip_verify = true如果镜像仓库启用了认证,还需要配置 auth 字段。不过开发测试环境建议先以 http 方式跑起来,后面有安全需求再加证书和认证。
修改完配置后重启 containerd:
systemctl restart containerd测试拉取是否正常:
crictl pull 192.168.1.10:5000/pause:3.9这里要特意提一下 pause 镜像。kubelet 创建每个 Pod 时,都会先创建一个 sandbox 容器,用的就是 pause 镜像。如果你的镜像仓库里没有提前导入 pause 镜像,节点初始化后会出现kubelet is down或者 Pod 一直 ContainerCreating 的问题。很多人第一次离线部署就栽在这个上面。
3. 离线部署 Kubernetes 集群的实操全流程
3.1 containerd 离线安装与初始化
环境准备好之后,第一步是安装容器运行时。我推荐 containerd,原因很简单:Kubernetes 从 1.24 版本开始移除了 Dockershim,如果你还坚持用 Docker,就得多装一个 cri-dockerd 适配层,这在离线环境里又增加了故障点。开发测试环境没必要给自己加戏。
离线安装 containerd 有两种方式。一是直接下载官方编译好的二进制 tar 包,解压后拷贝到/usr/local/bin,然后生成配置;二是把 containerd 的 rpm 包提前下载好,离线机器上 rpm -ivh 安装。我更倾向二进制方式,因为不依赖操作系统的包管理,版本完全可控。
安装完成后,生成默认配置:
containerd config default | tee /etc/containerd/config.toml然后重点检查两个配置:
SystemdCgroup必须为 true,否则 kubelet 和容器运行时在 cgroup 驱动上会不一致,节点状态可能一直 NotReady。sandbox_image要修改成私有仓库里的 pause 镜像地址。
这两个坑在离线环境特别常见,建议配好后先重启 containerd,再用ctr -n k8s.io images list确认 pause 镜像已经在本地。
3.2 用 kubeadm 初始化控制平面节点
安装完 containerd 之后,把 kubeadm、kubelet、kubectl 三个二进制拷到节点上,放到/usr/local/bin目录并加上执行权限。然后用配置文件的方式初始化控制面节点,这样可以明确指定镜像仓库地址和网络网段。
我准备的 kubeadm-config.yaml 大致如下:
apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.1.10 bindPort: 6443 --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.28.2 imageRepository: 192.168.1.10:5000 networking: podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/12 --- apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration cgroupDriver: systemd然后执行初始化:
kubeadm init --config kubeadm-config.yaml --upload-certs如果前面镜像导入完整,这一步通常能顺利通过。初始化成功后会输出一段 join 命令,先保存下来。然后配置 kubectl:
mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config如果你在初始化过程中遇到拉取镜像超时或认证失败,先别急着重试,按第 4 节的排查链路走一遍,往往都是 mirror 配置或者 imageRepository 地址写错导致的。
3.3 工作节点加入与 Calico 网络插件离线配置
控制平面初始化成功后,工作节点的加入流程相对简单。先把 kubeadm、kubelet、containerd 这些组件全部装好,确保镜像仓库里的镜像都能被 pull 到本地,然后执行控制平面输出的 join 命令即可。
如果 token 过期了,可以在主节点重新生成:
kubeadm token create --print-join-command执行 join 之前,建议在工作节点手动拉一次镜像,比如crictl pull 192.168.1.10:5000/kube-proxy:v1.28.2,确认网络和认证都通了再跑 join,不然很容易被一堆无关报错淹没。
网络插件我选了 Calico。离线安装 Calico 的核心,是把它的镜像从 Docker Hub 改为内网仓库地址。具体做法是,提前从 Calico 官方 GitHub 下载对应的 manifest 文件:
curl -O https://raw.githubusercontent.com/projectcalico/calico/v3.27.0/manifests/calico.yaml把这个 yaml 文件里的docker.io/calico/前缀全部替换成192.168.1.10:5000/,然后 apply:
kubectl apply -f calico.yaml验证网络是否正常:
kubectl get pods -n kube-system如果 calico-node 的 Pod 都在 Running,并且kubectl get nodes显示节点状态 Ready,说明网络插件已经生效。这时候可以随便创建一个部署测试一下跨节点 Pod 通信,比如跑两个 nginx 副本分布在两个节点上,互相 curl 一下。
3.4 开发测试常用 Addons 的离线安装思路
集群基础可用之后,接下来要补几个开发测试环境必须的组件。这些组件在在线环境里直接kubectl apply就行,但离线环境需要先把镜像同步到私有仓库或直接导入节点。
第一个是 Kubernetes Dashboard。很多开发测试人员不习惯 kubectl,更喜欢图形界面。Dashboard 的部署文件下载后,同样要把镜像替换成内网仓库地址。为了方便访问,我通常用 NodePort 方式暴露服务,而不是去折腾 Ingress,毕竟开发测试环境对安全域名的要求没那么高。
第二个是 metrics-server。没有它,kubectl top和 Dashboard 的资源图表都没法用。metrics-server 的镜像比较小,替换成私有仓库后直接 apply 即可。如果发现 metrics-server 一直 CrashLoopBackOff,大概率是 kubelet 的证书问题,需要给 metrics-server 的启动参数里加--kubelet-insecure-tls。
第三个是 Ingress Controller。开发测试环境一般有多个 Web 服务要暴露,每个都用 NodePort 太浪费端口且管理混乱,所以 nginx-ingress-controller 还是要装一个。离线部署时,把镜像替换后 apply 就行。
第四个是 StorageClass。开发测试环境跑 GitLab、Redis、MySQL 这些中间件时,PVC 是绕不开的。我这里推荐两种最省事的方案:一是用 NFS 动态供给,在集群里部署一个 NFS Client Provisioner,前提是内网有 NFS 服务器;二是用开源项目提供的 local-path-provisioner,它把节点本地目录映射成 PV,适合不需要跨节点共享数据的场景。
4. 开发测试阶段的常用服务落地与高频排障记录
4.1 在离线 K8s 上部署 GitLab、Redis、OnlyOffice、Superset 这些开发依赖
集群稳定跑起来之后,开发测试团队真正关心的是:GitLab 能不能用?Redis 能不能连?OnlyOffice 能不能在线预览文档?Superset 能不能做数据看板?这些服务在开发测试环境里几乎是刚需。
它们的离线部署思路是相通的:先在联网机器上把所有镜像拉下来,然后通过私有仓库或 tar 包导入集群节点,最后替换 manifest 里的 image 地址。例如一个常见的 GitLab 部署,至少需要准备这些镜像:
- gitlab/gitlab-ce
- gitlab/gitlab-runner
- redis
- postgresql
更省事的方式是用 Helm 来管理这些中间件。你可以在联网机器上用helm pull把 chart 打包下载,然后在离线环境里用helm install安装,同时把 chart 中引用的镜像全部同步到私有仓库。需要注意的是,Helm chart 中很多依赖子 chart,比如 Redis 和 Postgresql,这些子 chart 的镜像也得一起准备。
如果你要部署 OnlyOffice Document Server,有一点必须提前知道:这个镜像非常大,可能有几个 GB,传输时间会比较长。而且它对 CPU 和内存都有要求,建议单独调度到一台配置较高的节点,而不是和其他高负载应用挤在一起。
Superset 这种数据可视化工具也无非是 apache/superset 镜像加一个 redis、一个 postgres。开发测试环境不用追求特别高的性能,简单安装即可。
近几年还有一个趋势就是在开发测试环境里离线部署大模型相关服务,比如把 DeepSeek 8B 这种大模型的推理镜像导入集群做本地测试。这类镜像的体积通常是几个 GB 甚至几十个 GB,而且需要有 GPU 节点的支持。我的建议是:大镜像不要走私有仓库中转,直接导成 tar 后 scp 到节点再用ctr -n k8s.io images import导入;同时确认 GPU 节点已经安装了匹配的驱动和 nvidia-device-plugin,否则调度到 GPU 节点后 Pod 会一直处于 ContainerCreating。
可以说,私有仓库建立之后,后续的中间件离线落地就变成了一件比较机械的事情。最关键的是提前把“镜像清单”梳理清楚,不要到了现场才发现缺一个 tag 差一个版本。
4.2 镜像拉取失败与 ImagePullBackOff 的完整排查链路
ImagePullBackOff 是开发测试环境里出现频率最高的错误之一。离线环境里遇到它,我建议按照下面的链路一步步排查,而不是直接删 Pod 重来。
第一步,看 Pod 事件:
kubectl describe pod <pod-name>事件里通常会写清楚失败原因,比如Failed to pull image、manifest unknown、connection refused、no such host等。这些错误信息已经能定位到大部分问题。
第二步,检查镜像是否存在。如果事件提示manifest unknown,大概率是镜像名称或 tag 打错了。你可以在节点上手动执行:
crictl pull 192.168.1.10:5000/myimage:v1.0.0如果仍然报 manifest unknown,说明仓库里没有这个镜像,回联网机器重新打 tag 并 push。
第三步,检查 containerd 的 mirror 配置。如果手动 pull 时报connection refused或timeout,多半是 containerd 的 mirror 没配对,或者仓库地址不通。执行curl http://192.168.1.10:5000/v2/_catalog可以快速确认仓库是否可达。
第四步,看架构是否匹配。如果你把 amd64 的镜像推到了仓库,但目标节点是 arm64 架构,拉取时可能会报exec format error或者在启动时直接异常。这种问题在开发测试环境很常见,因为团队的笔记本和服务器架构经常不统一。解决方法是重新拉取对应架构的镜像,或者在构建镜像时用docker buildx构建多架构版本。
第五步,检查凭据和认证。如果仓库开了认证,containerd 的 config.toml 里必须有对应的 auth 配置。否则 Pod 会报pull access denied。
4.3 容易被忽略的隐藏坑:时钟同步、DNS、代理残留和防火墙
开发测试环境跑了一段时间之后,经常会出现一些看起来很奇怪的问题,比如节点时不时变成 NotReady,或者 Pod 之间的网络时通时不通。这类问题的根因往往不在 Kubernetes 本身,而是底层环境。
时钟同步是最容易被忽视的一个。Kubernetes 组件之间大量依赖证书和 token 做认证,而证书的有效期校验依赖系统时间。如果节点之间的时间差超过几分钟,就会导致x509: certificate has expired or is not yet valid一类的问题。离线环境没有公网 NTP,所以一定要在集群里找一台机器作为内部时间服务器,其他节点通过 chrony 与它同步。
DNS 也是个大坑。Kubernetes 集群内部的 CoreDNS 负责解析 Service 名称,但它转发集群外部域名时,默认会读取节点的/etc/resolv.conf。如果离线环境的内网 DNS 配置不对,或者 CoreDNS 的上游地址没有指向内网 DNS,那么 Pod 里访问集群外服务就会失败。遇到这种问题,检查一下 CoreDNS 的 ConfigMap,把 forward 指向内网 DNS 服务器。
代理残留这个问题,说实话在开发测试环境特别普遍。很多内网机器之前因为调试需要配置过 HTTP_PROXY 环境变量,或者/etc/systemd/system/containerd.service.d/下存在带 proxy 的配置文件。这些代理配置在连外网时没问题,但访问内网仓库时会先尝试走代理,结果代理又不通,导致镜像拉取一直超时。解决方式很简单:把 containerd 服务里的 proxy 环境变量清理干净,重启服务。
防火墙和 SELinux 如果开着,也可能导致 Calico 的 VXLAN 流量被拦截、Pod 之间无法互通。开发测试环境我通常会直接关掉防火墙并设置 SELinux 为 permissive 模式,如果团队有安全规范必须开启,那就要在防火墙上放行集群 Pod 网段和 VXLAN 端口。
4.4 备份与集群升级:离线环境必须提前想好的两件事
很多团队在开发测试集群跑起来之后就不管了,直到集群出问题才后悔没做备份。离线环境里,一旦集群损坏,修复成本比在线环境高得多,因为很多排查工具和修复包都需要重新导入。所以我强烈建议从一开始就定期备份 etcd。
etcd 是 Kubernetes 的“元数据数据库”,备份它是整个集群备份的核心。最简单的做法是用 etcdctl 直接做快照:
ETCDCTL_API=3 etcdctl --endpoints=https://127.0.0.1:2379 \ --cacert=/etc/kubernetes/pki/etcd/ca.crt \ --cert=/etc/kubernetes/pki/etcd/server.crt \ --key=/etc/kubernetes/pki/etcd/server.key \ snapshot save /backup/etcd-snapshot-$(date +%Y%m%d).db至于集群升级,离线环境的正确态度是“能不升就不升,要升也必须先在隔离的测试环境演练一遍”。开发测试集群一般不涉及生产数据,升级诉求没那么强,但如果确实需要升级小版本,必须提前准备好新版本的 kubeadm、kubelet、kubectl 二进制和所有对应镜像,把新镜像同步到私有仓库,再按照“控制平面节点先升级、工作节点后升级”的顺序操作。跨大版本升级(比如 1.28 升到 1.30)就不要在离线环境里冒进了,除非你有非常充分的理由和足够的时间。
5. 团队真正跑起来之后,我觉得值得补上的几个细节
5.1 开发测试环境的资源规划与命名空间配额
开发测试环境不像生产环境有严格的容量规划,但也不能完全放任。团队一起用同一个集群时,很容易出现“一个人把资源吃满,其他人全部卡死”的情况。
我的建议是:按项目或业务线划分命名空间,并通过 ResourceQuota 限制每个命名空间的资源总量。比如给测试项目组设置 limits 和 requests:
apiVersion: v1 kind: ResourceQuota metadata: name: dev-quota namespace: dev-project spec: hard: requests.cpu: "8" requests.memory: "16Gi" limits.cpu: "16" limits.memory: "32Gi" persistentvolumeclaims: "10"这样即使某个团队的某个应用出了内存泄漏,也不会拖垮整个集群。另外建议给每个命名空间配置 LimitRange,防止有人忘了写 resources 里 limits,导致 Pod 被调度到节点后被 OOM Kill。
5.2 离线环境下的 CI/CD 协同
开发测试集群往往不只是用来部署应用,还会承载一定量的 CI/CD 任务。离线环境下,CI/CD 的难点在于:构建镜像时需要基础镜像,而基础镜像从哪来?这里有两种思路。
一种是把 gitlab-runner 或 jenkins agent 本身部署进 K8s 集群,所有镜像从私有仓库拉取。构建时通过 Kaniko 或者 BuildKit 在集群内完成,基础镜像也是从私有仓库拿。这种方式的好处是链路完整,但前提是你提前把所有可能用到的基础镜像都同步到私有仓库。
另一种是“构建机与集群分离”:开发测试环境里有一台或多台可以访问内网仓库的构建机,它们在本地构建镜像,然后把构建产物 push 到私有仓库,K8s 集群部署时只负责拉取。这种方式更灵活,也更符合很多团队的现状。
不管哪种方式,都建议把“同步基础镜像”这个动作做成一个定时任务或脚本,比如每天或每次版本发布后,自动把从外部镜像源拉取的新版本镜像同步到私有仓库,这样开发测试环境就不会因为缺镜像而阻塞。
5.3 如果我再搭一次,会提前做的几件事
回过头看这次离线部署的整个过程,如果再做一次,我会在动手之前先做三件事。
第一,把所有节点的系统版本和内核版本统一。这次我就遇到一个节点是 CentOS 7.9、另一个是 Rocky Linux 9 的问题,导致某些内核模块的加载方式不一样,在网络插件排障时浪费了不少时间。
第二,写一个全局的镜像同步脚本。不要人工一个个去 docker pull 和 retag,而是把镜像清单维护在一个文件里,脚本自动拉取、导出、推送。这套脚本虽然前期要花一点时间写,但后面每次补充镜像都会变得非常轻松。
第三,先在单节点上完整跑通一遍,再扩展到多节点。离线环境最怕就是一开始就搞多节点,结果报错了也不知道是网络问题还是镜像问题。单节点环境把所有组件跑到 Ready 以后,再添加工作节点,排障范围会小很多。
离线部署 Kubernetes 这件事,本质上是在和“不确定性”作斗争。你把所有可能用到的东西都提前锁死、安排好,后面的部署就是水到渠成的事;但只要有一样东西没准备到,整个交付就可能陷入反复拷数据、反复等审批的泥潭。上面这些经验,都是我在实际部署中一步步踩出来的。希望你看完之后,能少走一些弯路,尤其是那些花了很多时间才发现是系统基础依赖缺失的坑,最好一次也不要踩。