离线环境手动搭建K8S集群:从证书到Calico全流程详解

离线手动搭建K8S集群,听起来像是自找麻烦。网上一搜全是 kubeadm 一条命令拉起来的教程,生产环境里也确实没人手动拆零件装。但我自己把完整流程走了一遍之后,最大的感受是——这不只是"麻烦",而是一堂极其高效的K8S内部原理课。所有在文档里模糊不清的概念,比如 apiserver 到底怎么跟 etcd 说话、kubelet 又是怎么找到 apiserver 的、Pod 的网段和集群的网段到底谁定谁,全都在手动搭的过程中被迫搞明白了。

如果你是个刚入门 K8S 不久、不想只会"复制粘贴 kubeadm init"的人,或者你接下来的环境天生就没有外网、连 docker pull 都费劲,那这篇文章是写给你的。它会完整地讲解:在没有互联网、没有 kudeadm 的情况下,怎么一步步把 etcd、kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kube-proxy 用最原始的手动方式装起来,并且最终能跑起来一个业务 Pod。

1. 为什么偏偏要"离线+手动":两个约束条件背后的真实收益

先看离线的意义。离线场景绝对不是教科书里的假设,现实中很常见——机房是隔离区,生产网不允许访问外网,或者企业内部有个内部软件源但容器镜像还是要人工拷进去。这种情况下,用 kubeadm 的一大障碍是它默认要从网上拉镜像,虽然可以通过配置文件指定镜像仓库,但一旦你连内网仓库都没有,整个流程就卡死在第一步。而手动搭建时,所有组件都是静态二进制(从官网 tar 包解压出来的),你只需要拷一次二进制和镜像文件,后面全靠 systemd 拉起,网络依赖极小。

再说手动。kubeadm 最大的问题恰恰在于它把一切细节都封装得太好了——它自动生成证书、自动生成 kubeconfig、自动初始化集群、自动把 Pod 网络插件装好。你用它是爽了,但出了问题之后排查起来很痛苦:你根本不知道 apiserver 的证书和 kubelet 的证书是分开签发的,更不知道 role 绑定错了会导致 scheduler 起不来。手动搭建等于把 kubeadm 帮你做决定的部分翻出来重新问自己一遍,每个组件之间怎么打交道、信任关系怎么建立、网段怎么不冲突,答案就全在过程中了。

我的建议是,如果你完全没接触过 K8S,不要上来就手动搭,至少先用 kubeadm 跑通一个集群,对"正常长什么样"有个印象。反过来,如果你已经能熟练使用 kubectl 操作 Pod、Deployment,但对控制面内部还是一团浆糊,那就在一个干净的测试环境里手动搭一次,非常值。手动搭建不是一个"生产可用的替代方案",而是一个"搞懂 K8S 的捷径"。

2. 开始之前的物料准备:证书、二进制、镜像一个都不能少

动手之前,物料准备是第一步,也是离线和手动场景下最容易翻车的地方。你需要准备四样东西:K8S 二进制、etcd 二进制、CNI 插件二进制、网络插件镜像(如果想用容器方式部署 CNI)。四样东西少一样,后面就会卡住。

2.1 版本选型和二进制获取

我这次在模拟项目X的环境里用的版本是 Kubernetes 1.26.6,etcd 3.5.9,CNI 插件用的是 calico v3.27。这几个版本互相兼容,是我在离线环境实测下来最稳的组合。二进制不推荐从源码编译,除非你是装在某奇怪的 CPU 架构上,否则直接用官方 release 页面里的服务器二进制 tar 包就行。kubernetes 的 tar 包里包含 apiserver、controller-manager、scheduler、kubelet、kube-proxy、kubectl 等全部二进制,解压之后进 bin 目录就能看到。

当然,离线环境意味着你不能在现场下载。我通常的做法是:在能联网的机器上一次性把需要的所有包下载好,打完 tar 之后用移动硬盘或内网传输工具拷进去。文件清单列一下,方便你对号入座:

  • kubernetes-server-linux-amd64.tar.gz(约 1GB)
  • etcd-v3.5.9-linux-amd64.tar.gz(约 30MB)
  • calico 的 calicoctl 二进制和 calico.yaml 镜像清单
  • cni-plugins-linux-amd64-v1.3.0.tgz(约 80MB)
  • calico 所需的几个镜像:calico/cni、calico/node、calico/kube-controllers,通过 docker save 保存为 tar 文件

2.2 离线环境下 tar 包分发和镜像搬运的两种思路

镜像的搬运有两种思路:一种是"推"——把镜像推到内网镜像仓库,让节点从内网拉;另一种是"拉"——用 docker save 导出 tar 包,拷到目标机器后 docker load 再导入。离线环境没有内网仓库的时候,只能用第二种。但要注意一点:docker load 只是把镜像放进了 Docker 的本地存储里,并不会自动打上 tag。如果你 save 的时候没有带 repository 和 tag,load 之后镜像会变成一堆没有 tag 的镜像,后面容器启动直接报找不到镜像。正确做法是 save 的时候指定完整名字,load 完立刻 docker tag 补上 tag。

我在实际操作中还发现一个坑:calico.yaml 里的镜像地址默认是 docker.io/calico/cni:v3.27.0,离线节点上即使 load 了镜像,如果 K8S 用 containerd 容器运行时(而不是 dockershim),crictl 和 containerd 本身并不会自动识别 docker 的本地存储。所以如果节点用的是 containerd,那得把镜像通过 ctr 导入到 containerd 的镜像存储里。不同运行时对应不同的导入工具,这是个很容易忽略的差异点。这次我们用 containerd 做演示,后面对应的导入命令会写清楚。

另外,这些镜像你不用全都拷到所有节点。分类放好:etcd 镜像只发给 etcd 节点,控制面组件的镜像(如果有容器化部署的)只发给 master 节点,calico 的镜像则发给所有节点,因为每个节点都会跑 calico 的 agent。规划好分发的粒度,能省下不少拷贝的时间和磁盘空间。

2.3 节点规划与网段设计

离线手动集群的常见节点规划是三台机器:一台 master,两台 worker。master 节点同时充当 etcd 节点,也就是所谓"堆叠式 etcd",最省机器。生产环境里 etcd 单独三台更好,但在练习环境里复用节点没问题。

网段设计是这次搭建中我认为最需要提前想清楚的部分,一共涉及三个网段:

  • 宿主机通信网段:就是三台机器所在的物理/虚拟网络,比如 192.168.1.0/24
  • Pod 网段(Cluster CIDR):K8S 给每个 Pod 分配的网段,例如 10.244.0.0/16
  • Service 网段(Service CIDR):K8S 给 Service 分配的虚拟 IP 网段,例如 10.96.0.0/12

这三个网段必须互不重叠,否则后面路由一乱你根本没法排查。我见过有人把 Pod 网段设成和宿主机网段192.168.1.0/24 相同的 192.168.0.0/24,结果 Calico 启动后疯狂报 BGP 路由冲突,折腾了两天才发现是网段规划问题。设计网段的第一原则:宁可空间浪费,也别互相踩踏。

3. 证书体系设计:手动搭建时最绕不过去的一关

K8S 内部的几乎所有通信都是 TLS。手动搭建时证书必须自己签发,这一步也是很多人第一次手动搭建时倒下的地方。不要慌,证书体系整体看透了一遍之后会发现,它没那么复杂,只是数量多了一点。我用的是最经典的 openssl 自签发方案,没有用 cfssl——不是 cfssl 不好,而是 openssl 大多数人更熟悉,出了问题更好排查。

3.1 证书要签哪些、签给谁、有什么用

按通信关系划分,整个集群需要这几类证书:

  • etcd 的证书:etcd 集群节点之间通信用的 peer 证书,以及客户端访问 etcd 时验证用的 server 证书。除此之外,etcd 还需要一个 client 证书,供 apiserver 作为客户端访问 etcd 时使用。
  • apiserver 的证书:这是最复杂的一张。kubelet、kubectl、controller-manager、scheduler 都要访问 apiserver,所以 apiserver 要给外部签发一张服务端证书,并且把所有的访问入口地址写进 SAN(Subject Alternative Name)。不仅仅是本机 IP,还包括 Service 网段里那个虚拟的 kubernetes.default IP,通常是 10.96.0.1。如果漏了 SAN 里的某个地址,后面 kubectl 访问就会报证书验证失败。
  • 组件间的 client 证书:kubelet 访问 apiserver 时,要用 kubelet 自己的证书,而不是共用 apiserver 的证书。controller-manager 和 scheduler 也一样,每个组件都需要自己的 client 证书,且带正确的 CN(Common Name)和 O(Organization),因为 K8S 的鉴权机制会用这两个字段识别调用者身份。
  • kube-proxy 的证书:它访问 apiserver 也需要证书,单独签一张。

整体算一下,需要签发的证书至少八九张。建议你建一个清晰的文件目录来放这些证书,我的习惯是 /etc/kubernetes/pki 下面再按组件分子目录,比如 /etc/kubernetes/pki/etcd、/etc/kubernetes/pki/apiserver。这样后面查问题的时候,一眼就能找到对应证书在哪。

3.2 制作 CA 证书和核心签发脚本

先创建 CA。CA 本质上是自签的根证书,其余证书都由它签发。创建 CA 的命令很简单,但重要的是 CA 私钥文件的权限要设成 600,因为一旦私钥泄露,整个集群的 TLS 信任体系就崩塌了。

创建 CA 之后,我给整个签发过程写了一个脚本,批量生成所有证书。核心逻辑是:先为每个组件生成私钥,再生成 CSR 证书请求文件,最后用同一个 CA 签发。有几个参数我现在都记得,因为它们太关键了:

apiserver 证书的 SAN 必须是以下内容的集合:

  • 本机 IP(master 节点 IP,比如 192.168.1.10)
  • 三个 master 的 IP(如果做了 HA,但我们的堆叠式单 master 只有一台)
  • Service 网段第一个 IP:10.96.0.1
  • 额外的几个 ClusterIP:比如 Kubernetes svc 的 IP
  • 域名:kubernetes.default.svc.cluster.local、kubernetes.default.svc、kubernetes.default、kubernetes

如果只填了本机 IP,kubectl 在本机能用,但 kubelet 从 worker 节点访问 apiserver 时就会报证书错误,因为 K8S 校验 apiserver 证书时,必须看到访问时用的 IP 或域名在 SAN 列表里。

etcd 证书方面,peer 证书的 SAN 要包含三台机器的 IP(即使目前只有一台,也建议预留后续扩展的 IP),因为 etcd 的 peer 之间是用 IP 直连的。server 证书则主要是给客户端访问用的,IP 也写本机和 localhost 就够了。其实 etcd 的证书配置空间很灵活,把自己控制范围内的节点 IP 全填进去,是逻辑最安全的选择。

3.3 生成 kubeconfig 文件而不是直接用证书连接

很多人以为证书搞完,手里的牌就齐了——只对了一半。实际操作中,kubelet、controller-manager、scheduler 这些组件并不是直接拿证书去访问 apiserver 的,而是通过 kubeconfig 文件。kubeconfig 是一个 YAML 文件,里面封装了 apiserver 的地址、CA 证书路径、客户端的证书和私钥路径。换句话说,它把所有连接凭证打包在一个文件里,让组件不用在命令行里显式写一堆证书路径。

这个文件要分成四份:admin.kubeconfig(给 kubectl 用的)、controller-manager.kubeconfig、scheduler.kubeconfig、kubelet.kubeconfig(worker 节点上 kubelet 用)。生成 kubeconfig 并不需要手写 YAML,直接用 kubectl config 系列命令就能生成,用 --kubeconfig 指定输出文件路径即可。注意不同的文件里,user 的 name 要和证书的 CN 一致。比如 kubelet 证书 CN 是 system:node:<节点名>,那 kubelet.kubeconfig 里的 user 也要写成这个,因为 apiserver 会根据 CN 来判断你是谁,然后根据 RBAC 决定你有没有权限。而且 K8S 内置的 RBAC 规则里,system:node 前缀的证书才有节点相关权限,弄错了调度器会报 Unauthorized。

CSR 的操作建议是:把整个 K8S 组件证书的生成脚本固定成一个可复现的 shell 脚本放进仓库。我这边踩过的一次大坑是,apiserver 证书过期后全部重新签发时,etcd 的 client 证书没有一起更新,导致 apiserver 连不上 etcd,集群直接"脑死亡"。这提醒我,证书之间的依赖关系要提前梳理清楚,最好在脚本注释里写清楚每张证书被谁消费。

4. 从 etcd 到 apiserver:启动控制面组件的顺序和参数设计

证书弄完,开始真正往集群里塞二进制了。K8S 控制面的启动顺序是有讲究的:先是 etcd,然后 apiserver,再 controller-manager 和 scheduler。这个顺序源于依赖关系:apiserver 依赖 etcd 作为存储后端;controller-manager 和 scheduler 依赖 apiserver 提供的 API 来干活。顺序乱了你会看到一些很奇怪的报错,比如 controller-manager 启动后狂报"failed to list *v1.Endpoints"——大概率就是 apiserver 还没起。

4.1 用 systemd 管理 etcd:参数里的两处关键设计

二进制解压到 /usr/local/bin 后,我在 /opt/etcd 下建了 data 目录,然后写了一个 systemd unit 文件来管理 etcd 服务。unit 文件本身不难写,关键在 ExecStart 里的启动参数。手动搭建中,etcd 的启动参数有两点值得认真对待:

第一,initial-cluster 参数。哪怕是单节点,也要写成 initial-cluster=etcd1=http://192.168.1.10:2380 这种带名字的格式,不能省略名字只写 IP。如果省略,etcd 在启动时会默认用 hostname 作为自身节点名,而集群配置里的名字和实际名字对不上就会启动失败。

第二,证书相关参数要完整。手动方式下必须显式设置 client-cert-auth=true、peer-client-cert-auth=true,然后分别指向对应的证书和私钥。没有设置的后果是,apiserver 连接 etcd 时会因为证书验证不过而连不上——即使数据目录里看起来正常,实际 etcd 一直在拒绝非法的 TLS 连接。

我不推荐用 docker 跑 etcd,除非你已经很熟悉 containerd 和 ctr 的用法。二进制方式写 systemd 反而更简单,升级、排查也直观。而且 etcd 的数据目录建议放在独立的挂载盘上,哪怕是模拟环境,这个习惯也要养成——etcd 是 K8S 的"黑匣子"(全集群状态都在里面),磁盘挂了基本等于集群数据全没。

启动完之后验证一下:etcdctl endpoint health 能看到 healthy 状态就是成功了。这个命令需要指定证书参数,建议把 etcdctl 的访问别名写进环境变量,省得每次敲一大串证书路径。

4.2 kube-apiserver:启动参数最多的一个组件

apiserver 的启动参数是我见过所有组件里最多的。不需要一条条全记,但有几类必须理解:

  • --etcd-servers:指向 etcd 的地址和端口,必须带 https 前缀并搭配证书参数
  • --service-cluster-ip-range、--service-node-port-range:决定了 Service 网段和 NodePort 的端口范围。service-node-port-range 默认是 30000-32767,手动时如果你希望用特定端口范围,必须提前定好
  • --client-ca-file、--tls-cert-file、--tls-private-key-file:apiserver 自身的 TLS 配置
  • --authorization-mode=Node,RBAC:开启节点认证和 RBAC 授权。如果你把这个参数漏了,后面 kubectl 可能可以访问,但 kubelet 也会以很高权限操作集群,非常危险
  • --enable-bootstrap-token-auth / --token-auth-file:手动场景下不需要 bootstrap token,全部走证书认证,所以不配也行

apiserver 的 systemd unit 里我习惯把千参数拆成多行,每条参数一个反斜杠连接,这样 readable 程度高很多。启动后立刻在 master 上执行 kubectl get componentstatuses 检查各个控制面组件状态——如果你看到 etcd 显示 unhealthy,先别慌,大概率是 apiserver 访问 etcd 的证书没配对,仔细核对 --etcd-certfile、--etcd-keyfile、--etcd-cafile 三项参数即可。

4.3 controller-manager 和 scheduler:权限前置与感知顺序

controller-manager 和 scheduler 是控制面的"后台大脑"。controller-manager 负责监控各种资源状态,比如 Deployment 副本数对不对、Node 有没有挂,然后自动修正;scheduler 负责给新 Pod 选节点。这两个组件的启动参数相对简单,重点依然是证书和 kubeconfig。

controller-manager 这里有个小细节我值得提:--cluster-cidr 参数要和 Pod 网段一致。如果这个参数没设或设错,controller-manager 里负责给 Service 分配 IP 的 controller 会出问题,而且 cloud-provider 相关的路由功能可能异常。手动搭建时我吃过这个亏,设成了默认值,结果发现 Service 老拿不到 IP。

scheduler 更简单,只要知道 apiserver 地址和 kubeconfig 路径就行。但启动顺序上,scheduler 必须等 apiserver 正常响应之后才能完整工作。你可以在 apiserver 还没起来之前启动 scheduler,但它的"寻找可调度节点"动作永远会失败,因为它看不到任何 Node 对象。所以我建议按顺序一个一个来,别图省事一键全起。

这两个组件起来后,在 master 上执行 kubectl get pods --namespace=kube-system 你会发现除了网络插件之外的空空如也——这是正常的,因为 kubelet 还没注册 Node,集群还感知不到计算节点。

5. kubelet+kube-proxy:让 worker 节点真正"入伙"

控制面起好,master 自己还不能调度 Pod——K8S 里真正管理容器的角色是 kubelet,它在每个节点上跑,负责跟容器运行时打交道,并把状态汇报给 apiserver。所以接下来要在所有 worker 节点(也包括 master)上安装 kubelet 和 kube-proxy。

5.1 containerd 配置和 kubelet 的"错位"细节

先装 containerd 和 CNI 插件二进制。我之前提到过,离线环境下镜像导入是个容易出错的地方。如果你用的是 containerd 做运行时,将 calico 镜像从 tar 导入到 containerd 用 ctr 命令。但 ctr -n=k8s.io 这个参数非常重要——containerd 支持多命名空间,K8S 默认使用 k8s.io 命名空间。如果导入时没指定命名空间,docker 里 load 的镜像在 K8S 里完全找不到。

kubelet 的启动参数里,--node-ip 非常关键。在多网卡机器上,kubelet 如果不指定 node-ip,它可能选错 IP 注册到集群里,导致后续 apiserver 访问这个节点走错网卡,Pod 通信直接异常。建议显式指定。

再说一个我反复踩过的坑:kubelet 的 --cgroup-driver 必须与 containerd 的配置一致。K8S 1.24 以后 dockershim 被移除,大家都用 containerd 了,containerd 配置文件里默认可能没有设置 cgroup driver,会导致 kubelet 启动报错说"failed to run Kubelet: failed to validate kubelet cgroup driver=systemd"。解决办法是修改 containerd 的 config.toml,把 SystemdCgroup 设为 true,然后重启 containerd。这个坑的原因也很简单:K8S 默认希望 cgroup 驱动是 systemd,但 containerd 默认不启用 systemd cgroup,如果两边不一致,资源隔离就会出问题。

kubelet 的 unit 文件里还建议设置 --pod-infra-container-image 指向一个 pause 镜像。手动搭建时这个镜像也要从外部导入。pause 镜像的作用是给每个 Pod 提供"捡跑"的基础容器,负责持有网络命名空间。如果你在离线环境里忘了导这个镜像,节点上 Pod 会一直处于 ContainerCreating,且报错信息是拉取镜像失败——非常误导人,因为它引用的镜像地址看起来一点都不像 pause 容器。

5.2 kube-proxy 和集群网络的第一次打通

kube-proxy 负责实现 Service 的 ClusterIP 和 NodePort 转发规则,它本身不负责 Pod 之间的通信。它的启动参数主要就是 apiserver 地址和 kubeconfig,模式默认 iptables。手动搭建时,kube-proxy 有两种部署方式:二进制以 systemd 托管,或者以 DaemonSet 跑在集群里。这里我选择二进制 systemd 方式,因为这一阶段集群内 Pod 网络还没建立,以 DaemonSet 方式部署镜像反而跑不起来——鸡生蛋蛋生鸡的经典问题。

当所有节点的 kubelet 都起来后,回到 master 上执行 kubectl get nodes,正常情况下能看到三台节点处于 NotReady 状态。NotReady 是正常的,因为每个节点上的 kubelet 会持续向 apiserver 汇报状态,而状态里包含一个 network 条件,它取决于 CNI 插件是否就绪。没有 CNI 插件,节点就永远 NotReady。这一步是"控制平面已通、数据平面未通"的临界点,也是很多手动搭建教程止步的位置。

接下来就该把 calico 请进场了。

6. 用 Calico 打通 Pod 网络:从 NotReady 到业务可用

CNI(Container Network Interface)是一个接口标准,规定了容器运行时如何把网络配置交给 CNI 插件。我们选择 Calico,因为它在离线环境里也有相对完整的部署方案,BGP 模式不需要额外的键值存储(不像 Flannel 的 VXLAN 需要 etcd 存储信息),自带的 kube-controllers 也不依赖外部的 IPAM。当然如果用 Flannel 也可以,只是 Calico 在企业场景覆盖更广,包括 NetworkPolicy 的支持。手动搭建时用 Calico 可以顺带把 NetworkPolicy 的机制学一下。

6.1 导入镜像:离线环境最大的实操陷阱

Calico 部署用 yaml 清单文件,K8S 会按照清单创建 DaemonSet、Deployment、Service 等资源。但这些资源包含的镜像如果不存在,Pod 就会 ImagePullBackOff。离线环境下这个问题更加尖锐,因为节点根本没有外网。我们手边已经有 calico 的镜像 tar 包,但导入的时候要注意:

  • 如果 containerd 的命名空间是 k8s.io,则镜像也必须在 k8s.io 下
  • tar 包里的镜像 tag 要与清单里的 tag 完全一致,一个字符都不能差
  • 清单文件里的镜像地址默认是 docker.io/calico/cni 这种全路径,如果你导入时没有这个全路径 tag,containerd 会当作一个不存在的镜像去拉取

正确的导入方式是:docker save 时保留完整路径,比如 docker.io/calico/cni:v3.27.0,docker load 或 ctr 导入后镜像 tag 自动就是 docker.io/calico/cni:v3.27.0。如果你之前 save 的是简化版 calico/cni:v3.27.0,那 load 后还得多一步 docker tag,把它改成 docker.io/calico/cni:v3.27.0。这不算技术难题,但很少有人当场能反应过来——报错信息总是长长一串,而原因却在镜像 tag 少了个前缀。

如果节点上这种 Pod 一直 ContainerCreating,建议先 crictl ps -a 看看具体的 sandbox 容器状态,八成是 pause 镜像缺失;再 crictl images 确认 calico 镜像是否存在。这套命令可以帮你快速定位是运行时问题还是网络问题。

6.2 部署 Calico 清单时的两个必要修改

拿到 calico.yaml 之后,直接 kubectl apply -f 大概率不能工作。必须修改两个地方:

第一,Pod 网段。calico.yaml 默认的 IPPool 网段是 192.168.0.0/16,这和我们的宿主机网段冲突,必须改成之前规划的 10.244.0.0/16。这一步如果你忘了,calico 启动后会自动创建 IPPool,你会看到节点上的 Pod 拿到 192.168.x.x 的地址,这在你的宿主机网络里完全不通。

第二,在可能的情况下,把 CALICO_IPV4POOL_CIDR 环境变量在清单里也改为 10.244.0.0/16,保证单点配置不会覆盖掉。

我建议在部署之前先检查一遍 calico.yaml 里所有相关的配置项,包括 cni_network_config 里的 MTU 设置。如果宿主机网卡 MTU 是 1500,而容器网络设置了一个明显偏大的 MTU,Pod 之间的性能会莫名变差。不是每个环境都需要调,但知道这个选项在哪,真出问题时你就能马上排查。

6.3 Node 就绪后,部署一个 Nginx 验证全链路

Calico 的 DaemonSet 起来后,等所有 calico-node Pod 都 Running,回到 master 执行 kubectl get nodes,应该看到三台节点都 Ready 了。此时集群的网络数据面已经打通:跨节点的 Pod 可以直接互相访问,Service 的规则也由 kube-proxy 生效了。

为了验证全链路,我习惯在集群里部署一个 Nginx 实例并暴露一个 NodePort 服务。这个验证只有三个步骤:

  1. 创建 Deployment 和 Service
  2. 查看 Service 分配的 ClusterIP
  3. 从集群外部访问任一节点 IP + NodePort 端口

如果访问失败,先检查 Service 的 Endpoints 是否正常填充。如果 Endpoints 为空,说明 kube-proxy 或者网络插件没把 Pod 的 IP 关联好,这个问题的复现思路很固定:从 NodePort 一层层往 Pod IP 追,先确认节点 iptables 规则存在,再确认 Pod IP 能 ping 通。手动搭建的每个组件都是我们亲手拉起来的,所以排查链路反而比 kubeadm 更清晰——你知道每个组件在哪个机器上跑,也知道它的配置文件在哪,剩下的就是按部就班看日志。

到这一步,一个离线环境下的手动 K8S 集群就完成了。它能做的事情和 kubeadm 搭出来的几乎一样,只是整个构建过程的透明度完全不同。

7. 手动搭建完毕后的行动清单:证书续期、备份与版本升级

集群能跑起来前程似锦,但手动搭建的维护责任比 kubeadm 重得多。你在搭建时绕开的工具,在维护时也要绕过。这里给一份我总结的行动清单,都是实践中真正会碰到的事。

7.1 kubelet 证书轮换:手动集群最容易踩的定时炸弹

K8S 组件之间通信的证书都有有效期。手动搭建时,如果你的自签证书有效期设成了默认的一年,那一年后你就要面临证书过期的灾难。关键点不只是 apiserver 证书,kubelet 的证书更容易被忽视——kubelet 默认会自动申请短期证书(一年),但如果集群没有配置证书轮换,到期后 kubelet 就无法再访问 apiserver,节点会变成一个"失联节点",非常难排查。

手动集群建议在搭建时就考虑好证书续期方案。最简做法是写一个定时任务脚本,定期检查集群里所有证书的过期时间,提前一个月发告警。另一种做法是直接利用 K8S 自带的 kubelet 证书轮换机制:配置 --rotate-certificates=true 和 --rotate-server-certificates=true。但注意,开启这个功能的前提是集群配置了证书签名服务(CSR API),而这又要求 apiserver 正确配置了 --client-ca-file,并且有相应的 RBAC 规则。你可以在文件 /etc/kubernetes/kubelet.conf 里查看 kubelet 持续连接的凭据,理解这个机制的闭环。

我个人的经验是:手动搭建完集群,第一件事就是把证书检查脚本写好,并设成节假日前一天自动跑。这听起来有点夸张,但我见过不止一次因为证书过期,整个集群控制的墓碑上写着"节点 NotReady、apiserver 拒绝一切请求",而日志却只显示 tls: handshake failure 的灾难现场。

7.2 etcd 备份和恢复演练

K8S 的所有状态都保存在 etcd 里。手动搭建时,etcd 的备份、恢复都是一条条命令自己敲的,绕不开工具封装。但反而是手动搭的集群,备份做起来最直接:etcdctl snapshot save 一条命令,然后定时拷走快照文件就行。

真正值得说的是恢复演练。我建议在节点上准备一个恢复脚本,脚本的逻辑很简单:停掉 kubelet 和 etcd,用快照文件恢复 etcd 数据目录,再启动 etcd 和 kubelet。整个演练就在沙箱环境反复执行几遍,别等到真出事再第一次做恢复——那样大概率会出现"快照文件在但恢复不了"的情况,因为 etcd 数据目录里的 wal 文件和快照的版本对不上,或者目录权限不对。手动搭建给了我们练习的机会,别浪费。

恢复之后最重要的一件事是验证集群状态:kubectl get nodes 和 kubectl get pods --all-namespaces。如果 Pod 状态显示异常,先别慌,很多情况下是网络策略或者镜像拉取的问题,需要配合日志逐层排查。

7.3 离线升级路径:手动搭建是可以平滑升级的

用 kubeadm 的最大心理优势是升级有配套工具。手动搭建的集群升级,本质上就是替换二进制:下载新版本的 tar 包,把旧二进制备份,用新二进制替换,重启对应 systemd 服务。听起来简单,但里面有严谨的顺序要求:先升级 etcd(如果有新版本),再升级 apiserver(它会短暂停止服务),再升级 controller-manager 和 scheduler,最后逐个节点升级 kubelet 和 kube-proxy。

升级前要重点检查的是 API 兼容性:apiserver 的新版本是否还接受旧版本对象。如果跨大版本升级,比如从 1.26 升到 1.27,需要先看一下弃用特性清单,确保清单里没有你正在使用的字段,否则升级后资源可能被强制转换或者直接拒绝。

升级过程中的版本差异也会影响之前的配置。如果 kubelet 的新版本不认旧版本 CNI 插件的配置格式,你在升级时就要同步更新 CNI 插件的版本。这也是为什么我建议把整个集群的组件版本、镜像版本、配置文件全记录在一个文档里,升级时对照着改,就不会遗漏。

还有一个细节:离线环境升级时的 yaml 清单。kubeadm 可以自动生成新版本对应的清单,手动搭建必须自己准备好新版本下的配置文件。如果是升级控制面组件,可以直接复用原来的 systemd 参数,其中 --feature-gates 等参数有变化时单独处理。升级完验证一条业务链路过一遍,确认无碍再切流量。

手动搭建集群的"售后"工作,说穿了就是三件事:证书别过期、etcd 别挂、升级有预案。能做齐这三件,手动集群的稳定度完全不输 kubeadm 出来的集群。

我在实际搭建过程中还有一个很深的体会:当你在离线环境里被迫手动搭了一整套集群之后,再回头用 kubeadm,你会看得懂它每一步在干什么,而不是盲目相信一行命令。很多文档里没有细讲的概念,比如为什么 kubelet 需要访问 apiserver 而 apiserver 又要访问 etcd、为什么 Pod 网段要独立设计,都会变成你排障时的肌肉记忆。如果你有条件,找三台闲置的虚拟机,把上面的步骤照着走一遍,绝对比你读十遍理论文章收获要大。最后再说一个实用建议:动手之前装好 bash 命令历史和时间戳记录,每敲一条命令就回看一次,手动搭建过程中最大的敌人不是技术难度,而是你记不清到哪一步了。

相关新闻

SpringBoot+Vue3+MyBatis前后端分离人事管理系统开发实战

SpringBoot+Vue3+MyBatis前后端分离人事管理系统开发实战

平时做中小企业的管理系统&#xff0c;最常用也最稳妥的一套组合就是 SpringBoot 加 Vue3 加 MyBatis。这套人事管理系统源码&#xff0c;前端用 Vue3 配上 Element Plus 做界面&#xff0c;后端是 SpringBoot 提供接口&#xff0c;MyBatis 负责数据库操作&#xff0c;MySQL 存…

2026/10/20 20:08:26 阅读更多 →
BGE嵌入模型在RAG知识库中的选型与实战调优

BGE嵌入模型在RAG知识库中的选型与实战调优

做知识库问答的人应该都有过这种经历&#xff1a;文档解析、切块、召回管线都调得差不多了&#xff0c;上线一测却发现用户问什么模型都答得稀碎。查到最后&#xff0c;问题往往不在大模型身上&#xff0c;而是出在嵌入模型&#xff08;Embedding Model&#xff09;选型上。文本…

2026/10/20 20:08:26 阅读更多 →
多变量时序预测实战:CNN-LSTM-Transformer融合模型完整方案

多变量时序预测实战:CNN-LSTM-Transformer融合模型完整方案

前面做了不少单变量的时序预测项目&#xff0c;这次直接挑战一个更有代表性的场景&#xff1a;多变量输入、多步预测&#xff0c;模型也换成了CNN-LSTM-Transformer融合结构。整个过程从数据处理到模型训练&#xff0c;再到结果评估&#xff0c;踩了不少坑&#xff0c;但也把整…

2026/10/20 20:08:26 阅读更多 →

最新新闻

LangChain与LangGraph:从链式编排到图结构工作流的实战迁移指南

LangChain与LangGraph:从链式编排到图结构工作流的实战迁移指南

1. 从「链」到「图」到底在说什么第一次接触 LangChain 的人&#xff0c;脑子里冒出来的问题往往不是“这玩意儿怎么用”&#xff0c;而是“它到底是个啥”。官方文档翻了三页&#xff0c;看到 Chain、Agent、Tool、Memory、Retriever 一堆名词砸过来&#xff0c;很容易直接劝退…

2026/10/20 20:54:23 阅读更多 →
2026年AI论文工具测评:继续教育学员的导师严选与搭配方案

2026年AI论文工具测评:继续教育学员的导师严选与搭配方案

每年到三四月份&#xff0c;继续教育学院的学员群里就开始互相安利各种AI论文工具。有人晒出半小时生成的综述初稿&#xff0c;有人炫耀润色后的重复率降了十几个点&#xff0c;也有人因为用了某个来路不明的工具&#xff0c;差点把写了一半的论文搭进去。作为平时帮学员把关论…

2026/10/20 20:54:23 阅读更多 →
YOLO11果树害虫识别系统实战:从数据集到论文答辩全流程

YOLO11果树害虫识别系统实战:从数据集到论文答辩全流程

每年毕业季&#xff0c;选择深度学习目标检测方向的人有一大批&#xff0c;果树害虫识别更是常青选题。打开学术库看到的相关论文很多&#xff0c;但真正轮到自己动手时&#xff0c;从克隆一个YOLO11开源项目到交出一套包含源码和论文的完整系统&#xff0c;中间隔着数据、训练…

2026/10/20 20:54:23 阅读更多 →
STM32F407标准库6路串口DMA中断配置实战

STM32F407标准库6路串口DMA中断配置实战

简介&#xff1a;这份资源面向使用STM32F407进行多串口通信开发的嵌入式工程师与学生&#xff0c;基于标准外设库实现6路USART的完整配置&#xff0c;并配套DMA发送与中断接收机制&#xff0c;可解决多设备并发通信、模块化数据转发等场景下的串口资源调度问题。压缩包共115个文…

2026/10/20 20:54:23 阅读更多 →
电商大数据规模到底有多大?从业务场景到技术选型的完整拆解

电商大数据规模到底有多大?从业务场景到技术选型的完整拆解

电商行业的数据规模到底有多大&#xff0c;这个问题几乎每个做数据相关工作的朋友都问过我。不管是刚入行的数据分析师想了解行业天花板&#xff0c;还是做技术选型时拿不准该用什么量级的方案&#xff0c;又或者是产品经理在规划数据看板时想搞清楚“我们这点数据算大还是算小…

2026/10/20 20:54:23 阅读更多 →
AI爬虫技能实战:从反爬识别到合规数据采集

AI爬虫技能实战:从反爬识别到合规数据采集

做数据采集的人&#xff0c;几乎都逃不过“反爬”这两个字。就在上个月&#xff0c;我给自己那个只会聊天的AI助手——代号“小龙虾”——加了一套爬虫技能&#xff0c;让它能自己看网页、提取数据、清洗入库。整个过程踩了不少坑&#xff0c;但也整理出了一套相对完整的思路。…

2026/10/20 20:53:19 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →