
做私有化交付这几年“离线部署K8S集群而且还得用二进制方式”基本上是隔离网络项目里躲不掉的一关。所谓离线就是目标机房跟互联网物理隔离或者网络策略收得很紧你没法在目标环境直接拉依赖所谓二进制就是不靠kubeadm这类在线引导工具而是把kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kube-proxy、etcd这些组件一个个下载好、配置好、装成systemd服务手工把集群拼起来。这篇文章就是给准备在完全无外网的环境里交付集群的运维同学看的。你不需要对K8S原理有多深的研究但最好已经跑过一两次有网络的集群知道Pod、Service、CNI大概是什么。看完之后你能得到一份从物料准备、证书规划、组件启动到问题排查的完整操作清单我踩过的坑也会一并写出来。1. 为什么离线场景要选二进制而不是kubeadm1.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.xcontainerd用1.7.xcni-plugins用1.4.xCoreDNS镜像固定到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证书代理组件访问apiserver1张可共用admin证书管理员操作集群1张ServiceAccount密钥对控制器签发SA Token1套生成工具有openssl和cfssl两种。我倾向用openssl因为它在所有Linux环境里都有不依赖额外安装。但openssl写配置比较啰嗦证书SAN字段极其容易出错所以一定要用配置文件而不是命令行参数来指定。给apiserver签发证书的时候SAN至少要包含所有Master节点的内网IP、集群VIP如果有、service ClusterIP通常是10.96.0.1、以及你准备用的域名。漏了任何一个地址将来通过那个地址访问apiserver就会报证书校验失败。这个坑我踩过好几次排查起来非常耗时间。2.4 目录与文件分发方案在目标环境里目录规划要提前统一。我常用的目录结构是这样的路径内容/etc/kubernetes/pki证书和密钥/etc/kubernetesapiserver等组件的kubeconfig和配置文件/var/lib/kubeletkubelet配置和运行数据/var/lib/etcdetcd数据目录/opt/kubernetes二进制文件/opt/etcdetcd二进制文件/opt/cni/binCNI插件二进制文件分发方式看现场条件。如果内网有跳板机就在跳板机上起一个临时的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] Descriptionetcd server Afternetwork.target [Service] Typenotify ExecStart/opt/etcd/etcd \ --nameetcd-01 \ --data-dir/var/lib/etcd \ --listen-peer-urlshttps://10.10.0.11:2380 \ --listen-client-urlshttps://10.10.0.11:2379,https://127.0.0.1:2379 \ --advertise-client-urlshttps://10.10.0.11:2379 \ --initial-advertise-peer-urlshttps://10.10.0.11:2380 \ --initial-clusteretcd-01https://10.10.0.11:2380,etcd-02https://10.10.0.12:2380,etcd-03https://10.10.0.13:2380 \ --initial-cluster-statenew \ --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 Restarton-failure RestartSec10 Useretcd LimitNOFILE65536 [Install] WantedBymulti-user.target注意Typenotifyetcd启动完成之后会主动通知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 \ --endpointshttps://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-apiserverapiserver是整个集群的入口所有kubectl命令、所有组件之间的通信都经过它。它也是配置项最多的组件。[Unit] DescriptionKubernetes API Server Afternetwork.target [Service] ExecStart/opt/kubernetes/kube-apiserver \ --bind-address0.0.0.0 \ --secure-port6443 \ --advertise-address10.10.0.11 \ --etcd-servershttps://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-issuerhttps://kubernetes.default.svc.cluster.local \ --api-audienceshttps://kubernetes.default.svc.cluster.local \ --service-cluster-ip-range10.96.0.0/16 \ --service-node-port-range30000-32767 \ --authorization-modeNode,RBAC \ --tls-cert-file/etc/kubernetes/pki/apiserver.crt \ --tls-private-key-file/etc/kubernetes/pki/apiserver.key Restarton-failure RestartSec10 Userkubernetes LimitNOFILE65536 [Install] WantedBymulti-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负责维护集群的期望状态比如副本数、节点状态、ServiceAccountscheduler负责给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-cidr10.244.0.0/16 \ --service-cluster-ip-range10.96.0.0/16 \ --leader-electtrue \ --use-service-account-credentialstrue--cluster-cidr是Pod网段决定了CNI插件要用什么网段来分配Pod IP必须跟CNI配置一致。--leader-electtrue很重要多Master环境下controller-manager要靠这个参数选出Leader只有一个实例干活其他实例Standby。scheduler的配置更简单只有kubeconfig和leader-electExecStart/opt/kubernetes/kube-scheduler \ --kubeconfig/etc/kubernetes/scheduler.kubeconfig \ --leader-electtrue两个组件起来之后用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-ip10.10.0.21这里有一个高频翻车点cgroupDriver必须和容器运行时保持一致。containerd默认用systemd作为cgroup驱动kubelet这边也必须配systemd。如果一边是systemd一边是cgroupfskubelet会直接报错拒绝启动报错信息里会明确告诉你两边的值不匹配。改完配置记得重启两个服务光重启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缺IPopenssl s_client检查证书核对文件和路径kubectl get nodes显示NotReadyCNI插件未装好或pause镜像缺失检查cni二进制和镜像仓库crictl images查看kubelet报cgroup驱动不匹配containerd和kubelet两侧配置不一致统一改成systemd重启两个组件Pod一直Pending节点资源不足、有污点、或调度器故障kubectl describe pod看事件kubectl describe nodekube-proxy CrashLoopBackOffipvs内核模块缺失modprobe对应模块重新启动Pod能创建但无法访问Servicekube-proxy规则没生效或集群网段不一致检查iptables-save核对cluster-cidretcd频繁选举Leader节点间时钟不同步用chronyc或者ntpdate校准时钟访问apiserver报证书无效SAN字段漏了访问地址重新签发证书把IP和域名加全ServiceAccount不可用sa.key和sa.pub不是一对重新生成并同步给apiserver和controller-managerkubectl 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这件事本质上就是一场把所有不确定性提前在联网机器上消化掉的战斗。只要物料齐、证书对、版本锁死剩下的就是按部就班地把服务一个个拉起来。真到了现场反而比在线的部署更省心——因为你知道问题只会出在你手里这些文件和配置里不会突然冒出一个外网超时或者版本漂移。