做私有化交付这几年,“离线部署K8S集群,而且还得用二进制方式”基本上是隔离网络项目里躲不掉的一关。所谓离线,就是目标机房跟互联网物理隔离,或者网络策略收得很紧,你没法在目标环境直接拉依赖;所谓二进制,就是不靠kubeadm这类在线引导工具,而是把kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kube-proxy、etcd这些组件一个个下载好、配置好、装成systemd服务,手工把集群拼起来。
这篇文章就是给准备在完全无外网的环境里交付集群的运维同学看的。你不需要对K8S原理有多深的研究,但最好已经跑过一两次有网络的集群,知道Pod、Service、CNI大概是什么。看完之后你能得到一份从物料准备、证书规划、组件启动到问题排查的完整操作清单,我踩过的坑也会一并写出来。
1. 为什么离线场景要选二进制而不是kubeadm
1.1 离线部署的真实痛点
很多人第一反应是:kubeadm也能离线用啊,先把镜像导出再导入不就行了?理论上是,但实际交付的时候你会发现一堆额外问题。
kubeadm init会要求把控制面组件的镜像全部弄到目标机上,这一步就需要你先从一台有网的机器把镜像包导出来,再想办法传进去。而且kubeadm对镜像仓库地址、版本号、签名校验都很敏感,一旦内网环境里没有对应的镜像仓库,或者离线镜像包缺了某个digest,整个过程就会卡在“拉取镜像”这一步,且报错信息对新人非常不友好。
更麻烦的是,很多隔离网络不只是没外网,连内部软件源、容器镜像仓库都没有标准化建设。你要先帮客户解决“东西从哪进来”的问题,再谈部署。二进制方式的优势就在这里:它把对在线仓库的依赖降到了最低。只要有一台能上网的机器把全部二进制压缩包、配置文件模板准备好,拿到内网之后解压、改配置、起服务就能跑,整个链路清晰可控。
另外,安全审计也是一个现实问题。交付类项目通常要提交物料清单、版本清单、文件校验值,二进制方式下每个组件是什么版本、打了什么补丁、放在哪个目录,都能白纸黑字写清楚,后期的运维交接也少很多扯皮。
1.2 二进制、kubeadm与发行版工具的取舍
我把三种方式放在一起对比过,各有适用场景:
| 维度 | 二进制方式 | kubeadm方式 | 发行版图形化工具 |
|---|---|---|---|
| 对离线仓库依赖 | 很低,基本不依赖 | 较高,需要镜像仓库 | 较高,需要软件源和镜像仓库 |
| 可控性 | 高,每个组件都是独立systemd单元 | 中,被抽象成静态Pod和kubeadm配置 | 低,平台封装了细节 |
| 证书管理 | 完全自己负责 | kubeadm自动签发管理 | 平台托管 |
| 升级维护 | 替换二进制+手动改配置 | kubeadm升级流程相对规范 | 平台一把梭,出问题不好定位 |
| 适合人群 | 想深入理解控制面原理、交付环境复杂 | 常规内网环境、团队有K8S基础 | 追求效率、环境标准化的团队 |
二进制方式最大的代价,就是所有事都要自己兜底。证书过期要自己盯,etcd备份要自己写脚本,节点故障恢复要自己判断。但反过来,正因为每一步都亲手做过,集群出问题的时候你脑子里是有一条完整链路的:证书对不对、配置指向对不对、网络通不通,能很快定位到具体环节。
我个人的建议是:如果是给客户做长期运维的交付项目,优先二进制方式。它麻烦,但可控、透明。如果是自己公司内部快速搭一套开发环境,没必要遭这个罪,选kubeadm省心的多。
1.3 版本选型要提前锁死
离线环境最忌讳的一件事,就是部署到一半发现组件版本不兼容,然后你还没法上网查。
这里说的版本不兼容,不只是Kubernetes和etcd的对应关系,还包括容器运行时、CNI插件、CoreDNS镜像的配合。我的习惯是先把版本锁死,写进一个部署说明文档里,所有节点严格按照这个版本走。
给你一个我近期项目里用过的组合,Kubernetes 1.28.x配上etcd 3.5.x,containerd用1.7.x,cni-plugins用1.4.x,CoreDNS镜像固定到1.10.x,网络插件用的Calico 3.27.x。这套组合在离线环境里实测下来稳定性不错。
要特别注意的是,Kubernetes版本和etcd的兼容范围、Kubernetes和containerd的兼容范围都要提前确认。这些信息在有网的时候就要查好记录,别指望到现场再研究。另外,架构也要提前确认:x86和ARM的二进制包不通用,目标机上全是ARM的话,所有物料都得换成arm64版本,这个错误我见过不止一次。
2. 离线物料准备:从一台能上网的机器开始
2.1 组件清单与下载
离线部署的物料准备,本质上就是在一台有网的机器上,把目标环境需要的一切提前"搬运"下来。我习惯建一个目录,按服务类型分开放:
offline-k8s/ ├── kubernetes-server-linux-amd64.tar.gz ├── etcd-v3.5.10-linux-amd64.tar.gz ├── cni-plugins-linux-amd64-v1.4.0.tgz ├── containerd-1.7.18-linux-amd64.tar.gz ├── images/ │ ├── pause-3.9.tar │ ├── coredns-1.10.1.tar │ └── calico/ └── checksums.txtKubernetes的二进制包是个大杂烩,里面同时包含kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kube-proxy、kubectl,下载一个包就够了。etcd要单独下载,它虽然常和Kubernetes一起出现,但其实是独立项目。
下载完第一件事不是解压,而是算校验和。记录每个文件的SHA256,到内网之后分发完成再校验一遍,防止文件传输过程中损坏。离线环境里坏了文件是件很痛苦的事——你没法重新下载,只能回去拿U盘再进来一趟。
2.2 镜像离线导入:containerd 与 pause/coredns
光有二进制还不行,Kubernetes集群正常运行还需要几个基础镜像。pause镜像是每个Pod都会用到的基础容器,CoreDNS提供集群内DNS解析,网络插件需要自己的镜像。
导出这些镜像的方式取决于源环境用的什么运行时。如果是Docker环境,就docker save;如果是containerd环境,就用ctr或者crictl导出:
ctr -n k8s.io images export pause-3.9.tar registry.k8s.io/pause:3.9 ctr -n k8s.io images export coredns-1.10.1.tar registry.k8s.io/coredns/coredns:v1.10.1到目标机上导入也是对应的逆向操作。这里有个小经验:提前检查Kubernetes版本默认的pause镜像版本和etcd、CoreDNS镜像版本,确保和集群版本配套。如果内部有镜像仓库,就先把这些镜像推到内网仓库,目标机从内网仓库拉取;如果连内网仓库都没有,就挨个节点手动导入。
2.3 证书体系规划
证书是二进制部署里最核心也最容易翻车的地方。kubeadm会自动帮你做证书签发和管理,而二进制方式下所有证书都要自己生成、自己分发、自己规划有效期。
我需要提前规划好这几类证书:
| 证书用途 | 作用 | 大概需要多少张 |
|---|---|---|
| CA根证书 | 签发其他所有证书 | 1套 |
| etcd证书 | 节点间通信和客户端访问 | 每节点1套 |
| apiserver证书 | 对外提供HTTPS服务 | 每个Master 1张 |
| kubelet证书 | 每个节点连接apiserver | 每节点1套 |
| kube-proxy证书 | 代理组件访问apiserver | 1张(可共用) |
| admin证书 | 管理员操作集群 | 1张 |
| ServiceAccount密钥对 | 控制器签发SA Token | 1套 |
生成工具有openssl和cfssl两种。我倾向用openssl,因为它在所有Linux环境里都有,不依赖额外安装。但openssl写配置比较啰嗦,证书SAN字段极其容易出错,所以一定要用配置文件而不是命令行参数来指定。
给apiserver签发证书的时候,SAN至少要包含:所有Master节点的内网IP、集群VIP(如果有)、service ClusterIP(通常是10.96.0.1)、以及你准备用的域名。漏了任何一个地址,将来通过那个地址访问apiserver就会报证书校验失败。这个坑我踩过好几次,排查起来非常耗时间。
2.4 目录与文件分发方案
在目标环境里,目录规划要提前统一。我常用的目录结构是这样的:
| 路径 | 内容 |
|---|---|
| /etc/kubernetes/pki | 证书和密钥 |
| /etc/kubernetes | apiserver等组件的kubeconfig和配置文件 |
| /var/lib/kubelet | kubelet配置和运行数据 |
| /var/lib/etcd | etcd数据目录 |
| /opt/kubernetes | 二进制文件 |
| /opt/etcd | etcd二进制文件 |
| /opt/cni/bin | CNI插件二进制 |
文件分发方式看现场条件。如果内网有跳板机,就在跳板机上起一个临时的HTTP服务,节点用wget去拉;如果只有一台能带进去的干净机器,就用移动介质拷贝。分发完以后一定要校验:所有关键文件的SHA256要一致。
证书和kubeconfig是敏感文件,分发的时候权限记得收好,配置文件统一owner是root,目录权限建议700或者600,不要因为离线环境就放松这个要求。
3. 一步一步把控制面拉起来
3.1 先搭etcd集群
离线部署里我的固定顺序是:先etcd,后apiserver,最后是controller-manager和scheduler。etcd是Kubernetes的存储底座,它不健康,后面全是白搭。
etcd集群建议奇数节点,三台或者五台。为什么一定要奇数?因为etcd用Raft协议做选举,需要超过半数的节点存活才能选出Leader。三台挂一台还能干活,四台挂两台就废了,五台挂两台还能干活。多出来的那台除了增加故障容忍上限,没有别的收益,所以三台通常是性价比最高的选择。
etcd的systemd单元可以这样写:
[Unit] Description=etcd server After=network.target [Service] Type=notify ExecStart=/opt/etcd/etcd \ --name=etcd-01 \ --data-dir=/var/lib/etcd \ --listen-peer-urls=https://10.10.0.11:2380 \ --listen-client-urls=https://10.10.0.11:2379,https://127.0.0.1:2379 \ --advertise-client-urls=https://10.10.0.11:2379 \ --initial-advertise-peer-urls=https://10.10.0.11:2380 \ --initial-cluster=etcd-01=https://10.10.0.11:2380,etcd-02=https://10.10.0.12:2380,etcd-03=https://10.10.0.13:2380 \ --initial-cluster-state=new \ --cert-file=/etc/etcd/pki/server.crt \ --key-file=/etc/etcd/pki/server.key \ --peer-cert-file=/etc/etcd/pki/peer.crt \ --peer-key-file=/etc/etcd/pki/peer.key \ --trusted-ca-file=/etc/etcd/pki/ca.crt \ --peer-trusted-ca-file=/etc/etcd/pki/ca.crt Restart=on-failure RestartSec=10 User=etcd LimitNOFILE=65536 [Install] WantedBy=multi-user.target注意Type=notify,etcd启动完成之后会主动通知systemd,这样systemd能准确知道服务是否真正就绪。数据目录一定要放到独立磁盘或者至少独立分区,etcd是IO密集型组件,跟系统盘抢IO会直接影响集群稳定性。
三台节点都启动后,用etcdctl检查集群状态:
etcdctl --cacert=/etc/etcd/pki/ca.crt \ --cert=/etc/etcd/pki/server.crt \ --key=/etc/etcd/pki/server.key \ --endpoints=https://10.10.0.11:2379,https://10.10.0.12:2379,https://10.10.0.13:2379 \ endpoint health --cluster三条endpoint都返回healthy,才说明etcd集群就绪。这里出现最多的问题就是initial-cluster参数写错或者节点间2380端口不通,这些都要在上apiserver之前排除掉。
3.2 部署kube-apiserver
apiserver是整个集群的入口,所有kubectl命令、所有组件之间的通信都经过它。它也是配置项最多的组件。
[Unit] Description=Kubernetes API Server After=network.target [Service] ExecStart=/opt/kubernetes/kube-apiserver \ --bind-address=0.0.0.0 \ --secure-port=6443 \ --advertise-address=10.10.0.11 \ --etcd-servers=https://10.10.0.11:2379,https://10.10.0.12:2379,https://10.10.0.13:2379 \ --etcd-cafile=/etc/kubernetes/pki/ca.crt \ --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt \ --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key \ --client-ca-file=/etc/kubernetes/pki/ca.crt \ --kubelet-certificate-authority=/etc/kubernetes/pki/ca.crt \ --kubelet-client-certificate=/etc/kubernetes/pki/apiserver-kubelet-client.crt \ --kubelet-client-key=/etc/kubernetes/pki/apiserver-kubelet-client.key \ --service-account-key-file=/etc/kubernetes/pki/sa.pub \ --service-account-signing-key-file=/etc/kubernetes/pki/sa.key \ --service-account-issuer=https://kubernetes.default.svc.cluster.local \ --api-audiences=https://kubernetes.default.svc.cluster.local \ --service-cluster-ip-range=10.96.0.0/16 \ --service-node-port-range=30000-32767 \ --authorization-mode=Node,RBAC \ --tls-cert-file=/etc/kubernetes/pki/apiserver.crt \ --tls-private-key-file=/etc/kubernetes/pki/apiserver.key Restart=on-failure RestartSec=10 User=kubernetes LimitNOFILE=65536 [Install] WantedBy=multi-user.target这里每一行参数都有意义。--etcd-servers要写完整的三台地址,只要有一台apiserver能连上etcd就能工作,但写全了能做故障转移;--service-account-key-file和controller-manager那边的sa.key是一对公私钥,配错的话Pod里的ServiceAccount完全不可用;--service-cluster-ip-range决定了Service的网段,必须和后面controller-manager、kube-proxy的参数保持一致。
apiserver启动之后先别急着往下走,验证一下HTTPS端口:
curl --cacert /etc/kubernetes/pki/ca.crt \ --cert /etc/kubernetes/pki/admin.crt \ --key /etc/kubernetes/pki/admin.key \ https://10.10.0.11:6443/healthz能返回ok才说明apiserver和etcd链路是通的。如果返回500,错误日志基本指向etcd连接失败,重点排查证书SAN和CA路径。
3.3 部署controller-manager与scheduler
这两个组件相对简单,因为它们被动依赖apiserver,不需要对外暴露端口,只需要一个kubeconfig指向apiserver即可。controller-manager负责维护集群的期望状态,比如副本数、节点状态、ServiceAccount;scheduler负责给Pod找合适的节点。
controller-manager的启动配置:
ExecStart=/opt/kubernetes/kube-controller-manager \ --kubeconfig=/etc/kubernetes/controller-manager.kubeconfig \ --authentication-kubeconfig=/etc/kubernetes/controller-manager.kubeconfig \ --authorization-kubeconfig=/etc/kubernetes/controller-manager.kubeconfig \ --service-account-private-key-file=/etc/kubernetes/pki/sa.key \ --root-ca-file=/etc/kubernetes/pki/ca.crt \ --cluster-cidr=10.244.0.0/16 \ --service-cluster-ip-range=10.96.0.0/16 \ --leader-elect=true \ --use-service-account-credentials=true--cluster-cidr是Pod网段,决定了CNI插件要用什么网段来分配Pod IP,必须跟CNI配置一致。--leader-elect=true很重要,多Master环境下controller-manager要靠这个参数选出Leader,只有一个实例干活,其他实例Standby。
scheduler的配置更简单,只有kubeconfig和leader-elect:
ExecStart=/opt/kubernetes/kube-scheduler \ --kubeconfig=/etc/kubernetes/scheduler.kubeconfig \ --leader-elect=true两个组件起来之后,用kubectl检查组件状态。这里要注意一个变化:老教程里习惯用kubectl get cs查看组件健康,但新版本Kubernetes的componentstatuses接口已经废弃了,这个命令显示的信息不可信。正确做法是直接看系统服务状态,或者访问apiserver的/healthz接口。
3.4 高可用入口怎么接
三个Master节点的apiserver都起来之后,需要一个统一的入口把流量分发到三台上。通常用四层负载均衡,可以是硬件负载,也可以是nginx或者haproxy跑在专门的机器上。
负载均衡器要转发6443端口,并且要配置健康检查,检查路径用/healthz。这样哪台apiserver挂了,负载均衡器自动把流量切到健康节点。所有节点的kubelet、kube-proxy,以及管理员自己的kubectl,都通过这个VIP访问apiserver。
在离线环境里,负载均衡器本身可能也需要离线安装,所以它的安装包也要提前准备。另外VIP这个地址要在所有证书的SAN里,包括apiserver证书和后面要生成的kubeconfig里的server地址,这个我前面提过,真的很重要。
4. 工作节点怎么接入集群
4.1 kubelet与容器运行时对接
控制面就绪后,就是往集群里加工作节点。kubelet是每个节点上最核心的代理进程,它负责管容器、向apiserver上报状态、执行Pod探针。
如果后续不打算用TLS Bootstrap的复杂机制,二进制部署最常见的做法是直接给每个节点签一份kubelet客户端证书,然后生成对应的kubeconfig。生成完毕把文件放到节点的/etc/kubernetes目录下,kubelet启动时用这个kubeconfig和apiserver握手。
kubelet新版本强烈建议用配置文件启动,而不是把所有参数堆在命令行里:
apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration clusterDomain: cluster.local clusterDNS: - 10.96.0.10 staticPodPath: /etc/kubernetes/manifests cgroupDriver: systemd containerRuntimeEndpoint: unix:///run/containerd/containerd.sock authentication: x509: clientCAFile: /etc/kubernetes/pki/ca.crtsystemd单元只需要指定配置文件位置:
ExecStart=/opt/kubernetes/kubelet \ --config=/etc/kubernetes/kubelet.yaml \ --kubeconfig=/etc/kubernetes/kubelet.kubeconfig \ --node-ip=10.10.0.21这里有一个高频翻车点:cgroupDriver必须和容器运行时保持一致。containerd默认用systemd作为cgroup驱动,kubelet这边也必须配systemd。如果一边是systemd一边是cgroupfs,kubelet会直接报错拒绝启动,报错信息里会明确告诉你两边的值不匹配。改完配置记得重启两个服务,光重启kubelet不够,containerd也要重启生效。
新版本Kubernetes默认不再支持直接对接Docker,所以容器运行时用containerd是主流选择。containerd本身装起来不复杂,解压、起服务、生成默认配置改几个参数就行。
4.2 kube-proxy与内核转发
kube-proxy负责实现Service的负载均衡规则。它监听apiserver里的Service和Endpoint变化,然后在本机写入iptables或者ipvs规则。
kube-proxy同样通过kubeconfig连接apiserver,启动参数里最关键的是--cluster-cidr,要和controller-manager里的Pod网段保持一致。用ipvs模式需要提前加载内核模块:
modprobe ip_vs modprobe ip_vs_rr modprobe ip_vs_wrr modprobe ip_vs_sh modprobe nf_conntrack_ipv4老版本内核里nf_conntrack_ipv4叫这个名字,新内核版本改成了nf_conntrack,这个细节在离线环境里容易踩坑,因为装不了新的模块包。建议把ipvs相关模块写入/etc/modules-load.d/,重启节点之后自动加载。如果模块缺失,kube-proxy会起不来,但报错不直观,日志里只说ipvs初始化失败,很多人会绕一大圈才定位到是内核模块的问题。
4.3 网络插件的离线安装
节点能注册进集群只是第一步,想让Pod互通、访问Service,必须装CNI网络插件。我常用Calico,也可以选Flannel,看现场网络条件。
离线装Calico的完整套路是这样的:提前在有网机器上把Calico的镜像导出,到内网后导入containerd,或者推到内网镜像仓库。然后应用Calico的manifest文件,把里面的image字段全部改成内网镜像地址。
节点上的CNI插件二进制要放到/opt/cni/bin,配置文件放到/etc/cni/net.d。这里最常见的坑是:CNI配置文件的优先级问题。系统会按字典序读取/etc/cni/net.d下的配置文件,如果里面残留了旧插件的配置,会导致新插件不生效。我习惯在部署前把这个目录清空。
确认网络插件就绪之后,Pod调度到节点上才不会一直ContainerCreating。判断CNI是否正常的快捷方式是看节点状态:kubectl get node,如果还是NotReady,大概率是CNI问题或者镜像导入问题。
4.4 节点加入后的验证动作
每个节点加入集群后,我都会做一遍基本验证:
- kubectl get nodes确认节点状态为Ready
- 在该节点上手动拉取pause镜像确认网络能通
- 创建一个测试Pod并确认调度到这个节点、状态变成Running
- 检查kubelet日志里没有持续的Error和Warning
这里特别强调等待时间。节点从加入集群到Ready,中间要经历证书校验、容器运行时连通性检查、CNI初始化,这个过程一两分钟内都是正常的。不要一看到NotReady就慌,先看日志再动手。
5. 常见问题与排查实录
5.1 高频问题速查表
离线二进制部署翻车点相对集中,整理一个我自己项目里遇到过的速查表:
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| apiserver起不来,报x509错误 | 连接etcd时CA路径不对,或etcd证书SAN缺IP | openssl s_client检查证书,核对文件和路径 |
| kubectl get nodes显示NotReady | CNI插件未装好,或pause镜像缺失 | 检查cni二进制和镜像仓库,crictl images查看 |
| kubelet报cgroup驱动不匹配 | containerd和kubelet两侧配置不一致 | 统一改成systemd,重启两个组件 |
| Pod一直Pending | 节点资源不足、有污点、或调度器故障 | kubectl describe pod看事件,kubectl describe node |
| kube-proxy CrashLoopBackOff | ipvs内核模块缺失 | modprobe对应模块,重新启动 |
| Pod能创建但无法访问Service | kube-proxy规则没生效或集群网段不一致 | 检查iptables-save,核对cluster-cidr |
| etcd频繁选举Leader | 节点间时钟不同步 | 用chronyc或者ntpdate校准时钟 |
| 访问apiserver报证书无效 | SAN字段漏了访问地址 | 重新签发证书,把IP和域名加全 |
| ServiceAccount不可用 | sa.key和sa.pub不是一对 | 重新生成并同步给apiserver和controller-manager |
| kubectl get cs显示Unhealthy | 新版本componentstatuses废弃 | 别再依赖这个命令,看服务状态 |
5.2 排查思路
排错的核心是看日志,但看日志也要讲究顺序。我的固定流程是:先看服务状态,systemctl status;再看最近的错误日志,journalctl -u服务名 -n 200 --no-pager;最后才动配置。
节点层面的问题看kubelet日志,控制面问题看apiserver日志,网络问题看kube-proxy日志和CNI Pod日志。日志里出现connection refused,优先怀疑端口和防火墙;出现x509或者certificate,优先怀疑证书和CA;出现timeout,优先怀疑网络链路和MTU。
离线环境里还有一个隐藏杀手:节点之间的时间不同步。etcd对时间漂移极其敏感,时间偏差超过一定阈值会导致选举频繁失败、租约过期。装完系统第一件事就应该是配好时间同步,内网没有NTP服务器的话,至少保证所有节点时间偏差在几百毫秒以内。
另外,我强烈建议在一开始就把所有部署动作写成可重复执行的脚本或者Ansible playbook。离线项目最大的特点是环境不统一,今天这套流程可能明天就要换个环境重新来一遍。有了脚本,下次部署就是改IP、改证书、执行,效率和正确率完全不是一个量级。
5.3 一些实战经验
最后分享几个我做离线项目攒下来的体会。
第一,物料准备阶段宁可多带不可少带。除了部署用的二进制和镜像,运维期要用的兜底工具也要带进去:etcdctl、crictl、各种网络排查工具、离线安装的包管理器。现场没有网,你没法临时装任何东西。
第二,证书有效期要规划好,并且提前写入监控。二进制部署不像kubeadm会自动轮转证书,过期了集群就断。我在项目里会额外写一个证书过期时间检查脚本,提前三个月告警。
第三,版本记录要做到"每台机器一份"。集群里多个节点的配置细节可能有差异,比如IP不同、hostname不同。把这些信息记录成表格放在交付文档里,将来排错能快速定位是哪台机器、什么状态。这个习惯救过我很多次。
离线二进制部署K8S这件事,本质上就是一场"把所有不确定性提前在联网机器上消化掉"的战斗。只要物料齐、证书对、版本锁死,剩下的就是按部就班地把服务一个个拉起来。真到了现场,反而比在线的部署更省心——因为你知道,问题只会出在你手里这些文件和配置里,不会突然冒出一个外网超时或者版本漂移。