上周帮一个硬件研发团队在纯内网机房里搭Kubernetes,客户机不能联网,也没有私有云,连最简单的pause镜像都拉不下来。折腾到第三天,kubelet还在报SandBox镜像拉取超时,团队里一个小伙子说:“要不这周先不管K8s了,大家继续用docker compose?”我理解他的想法,但都做到这一步了,放弃亏大了。最终我们把整套v1.28.2集群在完全离线的情况下跑起来了,包括Calico网络、Ingress、本地存储,开发测试环境日常够用。这篇文章就是把那几天的过程沉淀下来,给后面要碰“开发测试环境-Kubernetes离线部署”的同学一份能直接照着抄的路线图。
开发测试环境的离线部署,和生产环境的离线部署完全是两回事。生产环境讲究高可用、多副本、严格的安全策略,开发测试环境追求的是快速可用、随时重建、资源别太浪费。所以我不会按照SHA256校验每个包、搞三套etcd那套重流程来讲。我更多关注的是怎么一次性备好物料、怎么把镜像装进去、怎么在完全没外网的情况下解决所有依赖,以及踩坑之后怎么快速定位问题。这篇文章适合三类人:准备在纯内网或机房隔离环境搭建K8s的开发、测试、运维同学;想搞明白kubeadm离线部署到底卡在哪个环节的人;还有那些手头只有一台性能还行的机器,想跑起一套最小可用集群的折腾型选手。
1. 开发测试环境的离线痛点,和你想的不太一样
先说一个最常见的误判:很多人以为离线部署K8s的难点,是把那十几个二进制包搬进内网。事实是二进制根本不是问题,rpm、deb包、tar包拷贝进去解压就完事了,K8s没有一个地方强制要求在线校验。真正把你卡死的是容器镜像。
K8s控制平面里每一个组件都是以Pod形式跑的,kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy、etcd、coredns、pause,全部是镜像。也就是说,就算你二进制准备好了,kubelet一启动要拉pause镜像,拉不到就NodeNotReady,coredns拉不到就一直Pending,整个集群看起来就死了。
开发测试环境为什么更要命?因为开发测试环境的网络往往是公司内网、研发实验室、或者某些信创项目里的隔离机房,这些地方没有外网出口,甚至连DNS解析内网域名都很费劲。生产环境好歹有网管帮忙开白名单,开发测试环境通常是自己硬扛。还有一个更现实的问题:开发测试环境生命周期短,集群说拆就拆、说重建就重建,你不能每次重建都让两个兄弟手动拉两天镜像。所以离线部署方案必须做到“物料一次备好,重建可重复”。
另外开发测试环境还有一个特点:机器配置参差不齐。有可能是一台16G内存的老服务器,也可能就是开发工位旁边的一台Mini主机。这对网络插件、监控组件选型有很大影响。后面我会提到,开发测试环境尽量不要上太重的东西,比如把kube-prometheus全家桶塞进去,可能集群资源已经花了一半。
2. 三条路线选哪条:kubeadm、离线发行版、纯二进制
离线装K8s,常见的不外乎三条路。我分别说说它们各自适合什么情况。
2.1 kubeadm加自建镜像仓库:最可控,也最通用
kubeadm是官方推荐的安装工具,离线部署时核心思路是:在有网环境把镜像拉到本地,导出成tar包,拷贝到离线机器,然后再配合本地私有镜像仓库或者直接导入到containerd里。你不需要懂太多底层知识就能装起来,但也不是完全没有学习成本。
这种方式最大好处是透明。每个组件是什么版本、镜像存在哪个仓库、kubelet配置在哪,你都一清二楚。出了问题排查起来路径非常清晰。开发测试环境如果想长期维护、反复重建,我强烈推荐这条路线。
2.2 sealos、KubeKey类离线发行版:图快可以,别对黑盒掉以轻心
sealos这类工具确实很方便,它把K8s集群整个打成集群镜像,一条命令就完成部署。KubeKey也是类似思路,在KubeSphere平台上很常见。它们内部已经把自己需要的所有镜像打成离线包,你只要导入就行。
但我不建议一上来就无脑用这类工具,原因有两个:第一,它们把太多细节封在内部,一旦集群出问题,你很难知道是哪个组件、哪个镜像导致的;第二,离线包版本跟它们工具版本是绑定的,你要升级K8s,可能得重新下一套对应版本的工具。对开发测试环境来说,如果你只是想快速起一套环境验证代码,那用sealos没问题。如果你想借着部署机会搞懂K8s离线部署的全貌,还是kubeadm比较扎实。
2.3 纯二进制手工部署:想彻底搞懂可以试一次
纯二进制部署就是自己去GitHub下载kube-apiserver、kubelet等二进制,然后用systemd管理进程,用etcd做存储,中间要自己签发证书、写各种配置文件。这条路能走通之后,你对K8s的理解会上一个台阶。但日常开发测试环境真没必要这么搞,太累,而且容易出错。
我的结论是:默认选kubeadm加自建镜像仓库这套组合。后面所有内容都基于这条路展开。
3. 离线物料备货:镜像、安装包、正好缺的那个工具
离线部署最怕的不是安装过程中报错,是报错之后发现某个包没带,而你又没法下载。所以第一步就是把清单理清楚。我把这套流程需要的物料分成三类。
3.1 Kubernetes镜像清单:去有网机器上提前拉好
K8s控制平面需要哪些镜像,可以用kubeadm直接列出来。我在一台有网的Linux机器上装好kubeadm后,执行:
kubeadm config images list --kubernetes-version=v1.28.2输出大致是这个样子:
k8s.gcr.io/kube-apiserver:v1.28.2 k8s.gcr.io/kube-controller-manager:v1.28.2 k8s.gcr.io/kube-scheduler:v1.28.2 k8s.gcr.io/kube-proxy:v1.28.2 k8s.gcr.io/pause:3.9 k8s.gcr.io/etcd:3.5.9-0 k8s.gcr.io/coredns/coredns:v1.10.1注意,不同K8s版本对应的pause、etcd、coredns版本都不太一样,千万别想当然。拉镜像之前,先确定集群版本,然后用这个命令把清单拿到手。这里提醒一句,k8s.gcr.io现在已经迁移到registry.k8s.io,如果你在海外或有代理的机器上拉取,注意源地址的变动;在纯内网里,反正后面都会导入本地私有仓库,源地址反而无所谓了。
拉取并导出成tar包的做法:
kubeadm config images pull --image-repository=registry.k8s.io docker save $(kubeadm config images list --image-repository=registry.k8s.io | tr '\n' ' ') -o k8s-images.tar如果你用的是containerd而不是docker,那就在有网机器上先装好containerd,然后:
ctr images pull registry.k8s.io/pause:3.9 ctr -n k8s.io images export k8s-images.tar $(ctr -n k8s.io images list -q)这里有个小细节:ctr images export导出的是default命名空间下的镜像,而kubelet实际用的是k8s.io命名空间。导入的时候要显式指定:
ctr -n k8s.io images import k8s-images.tar别嫌这个命名空间啰嗦,它背后的原因后面专门讲。
3.2 系统依赖与工作节点物料
镜像之外,每台节点机器上还需要准备下列东西:
- 一个容器运行时。目前主流是containerd,K8s 1.24之后已经移除了dockershim,用docker作为运行时反而需要一个中间层cri-dockerd,开发测试环境没必要自找麻烦。直接上containerd。
- kubelet、kubeadm、kubectl三个二进制包。可以通过apt或yum本地源安装,也可以直接下载deb、rpm包拷贝进去用dpkg或rpm安装。
- CNI插件二进制。如果后面装Calico,它自带的manifest会包含插件,不用单独准备;但如果你想调网络策略,建议把官方CNI插件包也备一份。
- 内核模块和系统参数依赖,这个离线环境一般不需要额外下载包,改sysctl和modprobe就行。
这里我特别强调一下架构问题。有网机器和离线机器的CPU架构务必保持一致,x86_64就都是x86_64,arm64就都是arm64。镜像本身区分架构,你在x86机器上下载arm64的镜像导进去,kubelet跑起来也是直接崩溃。曾经见过有人在一个离线arm环境里导入了amd64镜像,折腾了一天最后发现是架构不匹配。
3.3 辅助工具:没有它们你会痛苦两倍
不要忽略一些看起来很小、但没有就很难受的工具。至少准备这几个:
crictl:K8s社区推荐的CRI调试工具,配置文件在/etc/crictl.yaml,默认连接containerd的socket路径是unix:///var/run/containerd/containerd.sock。没有crictl,你排查镜像拉取问题会非常费劲。nerdctl:如果习惯docker命令行风格,可以在离线机器上装一个nerdctl,它是containerd的docker兼容CLI,排查的时候心智负担低很多。jq、yq:解析JSON和YAML,调试时经常用。helm:后面装Ingress Controller、存储插件都会用到。离线环境里helm本身就是一个二进制,拷进去就能用,然后把chart和镜像导好就行。
4. 私有镜像仓库:整个离线方案的枢纽
开发测试环境如果在三台以上节点工作,我建议一定要搭一个私有镜像仓库,而不是把tar包在每台机器上重复导入。私有仓库的作用是让所有节点从一个源头拉镜像,既省磁盘空间,也避免各节点镜像版本漂移。镜像仓库软件选Harbor还是Docker Registry,我建议开发测试环境用轻量级的Docker Registry 2.x就够了。Harbor功能全,但依赖数据库、Redis这些组件,对离线环境来说是在给自己增加工作量。
4.1 搭建Registry
在有网环境或内网的一台机器上,准备一个目录放镜像数据,然后启动容器:
mkdir -p /data/registry docker run -d \ --name registry \ --restart=always \ -p 5000:5000 \ -v /data/registry:/var/lib/registry \ registry:2.8.3然后把之前导出的tar包推上去。在能访问这个Registry的机器上操作,假设Registry地址是192.168.1.10:5000:
docker load -i k8s-images.tar docker tag registry.k8s.io/pause:3.9 192.168.1.10:5000/pause:3.9 docker push 192.168.1.10:5000/pause:3.9如果镜像数量多,手写docker tag太累,可以写个for循环:
for img in $(docker images --format "{{.Repository}}:{{.Tag}}" | grep registry.k8s.io); do new_img="192.168.1.10:5000/${img#registry.k8s.io/}" docker tag "$img" "$new_img" docker push "$new_img" done4.2 让containerd信任私有仓库
这里很多人会踩坑。如果你内网里的Registry用的是HTTP协议,而不是HTTPS,那kubelet默认不会从HTTP仓库拉镜像。kubelet底层的containerd需要单独配置,告诉它允许跳过TLS校验。
containerd的配置文件在/etc/containerd/config.toml,先执行:
containerd config default > /etc/containerd/config.toml然后修改/etc/containerd/config.toml,我建议加一段类似这样的配置(不同版本字段略有差异,请以你的containerd版本为准):
[plugins."io.containerd.grpc.v1.cri".registry] [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或者新版containerd支持/etc/containerd/certs.d目录方式,不过项目内网里最多的是老版本配置,用config.toml这种写法兼容性更好。改完以后重启containerd:
systemctl restart containerd然后用crictl验证是否通:
crictl pull 192.168.1.10:5000/pause:3.9只要能拉下来,这步就稳了。另外,哪怕你只是想在一台机器上单节点跑,不搞多节点,我也建议把pause镜像手动预导入到每个节点的containerd里,减少初始化时一次网络请求的时间。
5. kubeadm离线初始化与节点入队全流程
物料备齐之后,真正的安装流程开始。整个流程分三块:环境准备、master初始化、worker加入。
5.1 每台机器上的基础环境准备
K8s对系统有一些基本要求,离线环境也不例外。我总结成一张清单:
| 项目 | 要求 | 说明 |
|---|---|---|
| 操作系统 | CentOS 7.9+/Ubuntu 20.04+ | 内核3.10以上基本可用,建议4.x以上 |
| 关闭swap | 必须关闭 | kubelet默认不允许启用swap |
| 内核模块 | br_netfilter, ip_vs, nf_conntrack | 网络转发和负载均衡需要 |
| 系统参数 | net.bridge.bridge-nf-call-iptables=1 | 保证iptables规则对网桥流量生效 |
| 容器运行时 | containerd已安装并能启动 | socket在/var/run/containerd/containerd.sock |
关闭swap:
swapoff -a sed -ri 's/.*swap.*/#&/' /etc/fstab加载内核模块:
modprobe br_netfilter modprobe ip_vs modprobe ip_vs_rr modprobe ip_vs_wrr modprobe ip_vs_sh modprobe nf_conntrack设置系统参数,写入/etc/sysctl.d/k8s.conf:
cat > /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 sysctl --system5.2 配置containerd的SystemdCgroup
先说一下为什么这个很重要。K8s在管理Pod资源时,需要cgroup跟systemd配合。containerd运行时不配上SystemdCgroup=true,kubelet会报运行时网络未就绪或者是节点状态始终NotReady。
修改/etc/containerd/config.toml:
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] SystemdCgroup = true然后重启containerd。这个配置我在三个不同环境里都验证过,只要忘记配置,节点状态大概率会出问题。
5.3 master节点初始化
在master节点上,写一个kubeadm配置。假设私有仓库地址为192.168.1.10:5000,master IP为192.168.1.11,Pod网段用Calico默认的192.168.0.0/16:
# kubeadm-config.yaml apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.28.2 imageRepository: 192.168.1.10:5000 networking: podSubnet: 192.168.0.0/16 serviceSubnet: 10.96.0.0/12 --- apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.1.11 bindPort: 6443 nodeRegistration: criSocket: unix:///var/run/containerd/containerd.sock执行:
kubeadm init --config kubeadm-config.yaml这一步如果失败,先不要慌,看输出是在哪个阶段失败。最常见的几种情况:镜像拉取失败、apiserver容器启动失败、证书校验失败。
初始化成功后,输出里会有一段kubeadm join命令,保存好,里面带着token和ca证书hash。同时按提示复制kubeconfig:
mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config验证master状态:
kubectl get nodes这个时候master大概率是NotReady,这是正常的,因为CNI网络插件还没装。
5.4 worker节点加入
worker节点上只需要装好containerd、kubelet、kubeadm,然后把私有仓库信任配置弄好,再执行与上面相同的镜像预导入或者确认能从仓库拉取。然后运行master输出里的join命令:
kubeadm join 192.168.1.11:6443 --token xxxxx \ --discovery-token-ca-cert-hash sha256:xxxxx注意一点:开发测试环境经常遇到token过期。token默认24小时过期,如果太久之后要加节点,可以在master上执行:
kubeadm token create --print-join-command重新生成一条join命令,很方便。
5.5 单节点开发测试环境的去污点操作
如果你只有一台机器,master也承担pod调度,那就把master节点的污点去掉:
kubectl taint nodes --all node-role.kubernetes.io/control-plane-去掉之后,所有节点都能调度业务Pod。开发测试环境这么干没问题,生产环境千万不要这么干,这是常识,但我还是多嘴一句。
6. 开发测试环境的CNI、Ingress与存储配套
K8s集群本身跑起来之后,其实还是个半成品。没有CNI,Pod之间没法正常通信;没有Ingress,你想用域名访问服务会很痛苦;没有存储,任何有状态应用都跑不了。这三样在离线环境里一样要提前备好镜像。
6.1 Calico网络插件:三个镜像搞定
开发测试环境我推荐Calico。Calico功能全,性能也不错,而且manifest里面就三个主要镜像:calico/cni、calico/node、calico/kube-controllers。
在有网机器上下载对应版本镜像后导出导入,或者推送到私有仓库。然后从Calico官方仓库把离线版manifest拿下来,通常是一个calico.yaml。这个文件里写死的镜像路径是docker.io/calico/cni:v3.27.0这类,如果你用的是私有仓库,可以用sed统一替换:
sed -i 's#docker.io/calico#192.168.1.10:5000/calico#g' calico.yaml然后应用:
kubectl apply -f calico.yaml等两分钟,查看coredns和calico的Pod状态:
kubectl get pods -n kube-system -o wide如果都不是Pending或CrashLoopBackOff,说明网络通了。
这里有一个坑:前面kubeadm配置里的podSubnet必须和Calico的IPPool在同一个网段。我上面的配置用的就是Calico默认的192.168.0.0/16,如果你要自定义网段,记得同步改Calico的CALICO_IPV4POOL_CIDR环境变量。
6.2 Ingress Controller:离线内网访问服务的门面
开发测试环境里,一个ingress-nginx足够了。它有两个镜像,controller和webhook-certgen,都从registry.k8s.io/ingress-nginx拉取。离线部署其实核心就是先把两个镜像搞定,然后helm安装或者应用manifest。
如果内网没有域名解析,可以把Ingress Controller服务类型改成NodePort,把80和443映射到宿主机端口。YAML里对应spec.type: NodePort,然后找到映射端口:
kubectl get svc -n ingress-nginx ingress-nginx-controller之后在开发机或测试机上修改/etc/hosts,把域名解析到任意一台节点的IP,就能用域名访问了。
6.3 本地存储:local-path-provisioner是开发测试环境救星
有状态应用在开发测试环境里怎么处理数据卷?上Ceph、GlusterFS太重了,NFS又得额外找服务器。我推荐用rancher的local-path-provisioner。它做的事情其实就是在节点本地目录里动态创建PV,对开发测试环境简直是量身定做。镜像只有rancher/local-path-provisioner:v0.0.26一个,部署之后它会创建一个默认的storageClass。
kubectl get storageclass如果显示local-path是默认,那你创建PVC时不用指定storageClass,直接申请即可。所有Pod数据最终落到节点主机目录,重启Pod后只要PVC还在,数据就在,开发测试足够了。缺点大家也都知道:Pod调度到别的节点,数据不在同一台机器上,所以只适合开发测试环境。
7. 坑位复盘:我在离线环境里反复踩过的几件事
最后我把这几次部署中遇到的典型问题列出来,每个问题都带排查链路,不是直接给结论,因为下一次你遇到的问题大概率包装方式不一样。但排查思路是可以复用的。
7.1 节点一直是NotReady的排查链路
遇到NotReady,第一步不要看kubectl,先看kubelet日志:
journalctl -u kubelet -f --no-pager日志里如果出现container runtime network not ready,说明CNI没有ready,这不是网络插件本身的问题,而是容器运行时和CNI对接没完成。如果出现failed to get sandbox image "...",说明pause镜像没有正确导入到k8s.io命名空间,或者sandbox_image配置指向了非本仓库的地址。
排查完kubelet日志后,再看系统组件Pod状态:
kubectl get pods -n kube-system -o wide如果是Pending,用kubectl describe pod看事件。大概率是镜像拉取失败或者节点没有足够资源。这里我建议把describe输出里的Events信息完整看一遍,很多时候问题直接写在里面。
7.2 coredns一直在CrashLoopBackOff
这个现象很常见。有一种情况和CNI无关,纯粹是coredns的配置里forward . /etc/resolv.conf无法访问上游DNS。开发测试内网往往没有内网DNS,或者/etc/resolv.conf指向了一个不通的地址。
排查思路:先看日志:
kubectl logs -n kube-system -l k8s-app=kube-dns --tail=50如果是dns: server misbehaving之类,可以修改coredns的ConfigMap,把forward目标改为公司内网可用DNS,比如forward . 223.5.5.5。如果没有外网DNS可用,就指向网关或另一台能解析内网域名的机器。
7.3 kube-proxy选择IPVS模式但没有ip_vs模块
如果你修改了kube-proxy模式为ipvs,但节点上没有加载ip_vs相关内核模块,kube-proxy Pod会一直重启,网络规则不生效,Service访问异常。我之前在CentOS 7.9上遇到过,解决方案就是前面环境准备阶段提前modprobe那几个模块。如果你已经跑起来才发现,可以用modprobe加载后直接删掉kube-proxy Pod让它重启,一般能恢复。
7.4 离线环境时间不同步导致证书校验失败
这是一个隐藏得很深的坑。开发测试环境机器如果长时间没有外网,时间可能会慢慢漂移。如果master和worker之间时间差超过几分钟,kubeconfig里的证书校验就会失败,join时会出现x509: certificate has expired or is not yet valid。当时我排查了快两个小时,看证书有效期完全没问题,最后发现是系统时间慢了8分钟。
所以离线环境一定要建立一个内网NTP时间同步方案,哪怕只是让一台固定的主机定时date同步源,也比完全放任不管好。如果你只是临时测试,也可以手动对时,先保证集群能建起来。
7.5 开发测试环境的重启问题
开发测试环境经常整个机房断电,然后重启所有机器。你会遇到一个现象:机器都起来了,K8s集群状态不正常,很多Pod一直Pending。这时候不用慌,先看etcd和docker/containerd是否正常启动,然后再一张张检查节点状态。
还有一个更隐蔽的问题:如果Pod数据存储在local-path,但Pod被调度到了另一台机器,PVC绑定不上会一直Pending。这就是开发测试环境里local-path的特性,遇到这种问题就把对应Deployment的节点选择器固定到数据所在的那台机器上,或者直接删掉PVC用新的,反正是开发测试数据,丢了也没多大影响。
8. 跑通之后的维护命令库与几个小建议
集群跑通之后,日常维护主要靠几条命令。我顺手整理一个速查表放在这里,离线环境下用着方便:
| 场景 | 命令 |
|---|---|
| 查看节点状态 | kubectl get nodes -o wide |
| 查看系统Pod状态 | kubectl get pods -n kube-system -o wide |
| 查看Pod调度失败原因 | kubectl describe pod <pod-name> -n <namespace> |
| 查看容器运行时可识别镜像 | crictl images |
| 手动导入镜像到k8s命名空间 | ctr -n k8s.io images import xxx.tar |
| 查看kubelet实时日志 | journalctl -u kubelet -f |
| 查看ingress端口映射 | kubectl get svc -n ingress-nginx |
| 创建新token加入节点 | kubeadm token create --print-join-command |
| 查看各namespace事件 | kubectl get events --all-namespaces --sort-by='.lastTimestamp' |
最后再分享两个实际经验。
第一,离线部署前一定要把整个流程在虚拟机里离线演练一遍。不要以为文档看懂了就能直接上,我第一次也是这么想的,结果到现场发现Mac上拉的镜像和服务器架构不匹配,硬是又花了一个下午重新拉。现在我的习惯是:把有网机器当作“物料制备机”,先在本地虚拟机里完整跑一遍离线安装流程,确认没有任何遗漏之后再带到现场。
第二,离线环境里的K8s最好不要把版本追得太新。K8s社区迭代很快,每个版本都可能引入新的配置字段或移除旧字段。开发测试环境本来就图稳定,选一个经过大量实践验证的稳定版本,比如1.28.x,比选一个刚发布的最新版本要稳妥得多。后续升级的事情等真正有需求再说。
这个离线部署的流程走通之后,后面再搭新的开发测试环境就很简单了。镜像全部躺在私有仓库里,安装包放在内网文件服务器上,照着这个流程半小时就能起一套新集群。如果后面需要我把Harbor仓库、多集群管理、或开发测试环境的流水线对接这些内容展开,欢迎评论区和私信继续交流。