☰
二进制部署高可用K8s集群:架构、证书与一键脚本实战
2026/10/8 4:02:00 网站建设 项目流程

简介:一套基于二进制方式部署高可用Kubernetes集群的自动化脚本,面向希望深入理解k8s组件原理的运维人员、开发者和云原生学习者。脚本依据阿良的二进制部署文档整理,将原本繁琐的手工操作压缩为可重复执行的一键流程,适合在测试环境或生产环境快速搭建高可用控制面。资源包共13个文件,约398.89MB,包含4个部署Shell脚本、etcd、kubernetes-server与docker的二进制压缩包、calico和coredns的YAML配置、cfssl证书工具链以及使用说明文档,文件类型覆盖从证书签发、etcd集群初始化到工作节点加入的完整链路。目前已有1581人学习下载。通过实际运行这套脚本,读者不仅能快速获得一个多主节点高可用集群,还能结合说明文档中的步骤理解HA架构中etcd、apiserver、scheduler和controller-manager的协作关系,为后续排查问题与二次定制打下基础。

1. 二进制部署高可用 k8s:为什么绕开 kubeadm 反而更可控

用 kubeadm 搭集群确实快,但生产环境里一旦出现 apiserver 反复重启、etcd 成员频繁踢出、证书过期这类问题,你会发现所有排错路径都指向一个黑匣子:kubeadm 生成的静态 Pod 和叠加层。二进制部署是把 kubelet、kube-apiserver、etcd 这些组件一个个直接拉起来,每个进程对应一个 systemd unit,日志直接看 journalctl,依赖关系一目了然。这套方案尤其适合离线交付、政企私有化、对组件版本和启动参数有强管控诉求的团队。我一般会在两类场景下选它:一是客户现场不能联网拉镜像,二是集群规模不大但必须保证 Master 组件高可用。本篇会把这个方向拆成架构、证书、脚本、踩坑和验证五块,最后落到一键部署脚本的设计逻辑上。

2. 高可用架构先立住:堆叠 etcd、三 Master 与流量入口怎么选

2.1 组件拓扑:哪些进程在跑、谁和谁说话

高可用 k8s 集群的经典拓扑是三个 Master 节点,每个 Master 上跑 kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kube-proxy,外加一个 etcd 成员。三个 etcd 成员共同组成一个集群,数据在三个节点间同步,这个叫堆叠 etcd 架构。它省机器、省内网带宽,是中小规模集群最常见的生产方案,也符合本篇标题里“二进制高可用”的默认语境。

组件之间的通信链路是固定的:kube-apiserver 主动连接本机的 etcd 成员,kube-controller-manager 和 kube-scheduler 只连接 apiserver,kubelet 只连接 apiserver,kube-proxy 也是只连接 apiserver。这里有个容易混淆的点:controller-manager 和 scheduler 在高可用架构下通过 leader election 机制选主,同一时刻只有一个实例在干活,但它们连接的都是本机的 apiserver。如果 apiserver 挂了,本机的 controller-manager 和 scheduler 会跟着失联,转而通过负载均衡 VIP 去连另一个 Master 的 apiserver。所以集群能撑住两台 Master 宕机的前提是:至少有一台 Master 的 apiserver 还活着,并且所有组件都能通过同一个 VIP 访问它。

2.2 etcd 怎么摆:堆叠还是独立,奇数节点是底线

堆叠 etcd 意味着三台 Master 上各跑一个 etcd 服务,数据互相复制。独立 etcd 集群则是另找三台机器专门跑 etcd,Master 只跑 k8s 组件。独立部署的隔离性好、性能干扰小,但多了三台机器的成本和运维面。对大多数几十个节点以内的集群,堆叠 etcd 完全够用,而且二进制部署时 systemd 管理 etcd 和 apiserver 的逻辑一致,脚本写起来也顺。

etcd 集群必须保持奇数成员。三成员集群允许挂一个,五成员允许挂两个。这背后是 Raft 的多数派逻辑:写入必须超过半数节点确认。三台机器部署时,etcd 的 initial-cluster 参数必须写全三个成员的地址,任何一台机器的 etcd 配置里漏了某个 peerURL,集群就永远无法形成法定人数。实际部署中我见过有人在单节点测试环境里把 initial-cluster 只写了一台,然后直接复制到三台机器上,结果 etcd 一直报无法连接对端。这个配置必须在每个节点上生成,不能用同一份配置硬套。

部署方式机器数量容错能力适用场景
堆叠 etcd3 台 Master允许 1 台宕机中小规模、省机器
独立 etcd3 台 etcd + 3 台 Master允许 1 台宕机大规模、组件隔离要求高
单 etcd1 台无仅测试环境

2.3 流量入口:haproxy 加 keepalived 的方案对比

所有 kubelet、controller-manager、scheduler 访问 apiserver,走的是一个虚拟 IP。这个 VIP 由 keepalived 在三台 Master 上浮动,哪台机器上的 keepalived 优先级最高、且健康检查通过,VIP 就绑定在哪台机器上。haproxy 在每台 Master 上都跑一个实例,监听本机的 6443 端口,把请求转发到三台 Master 的 apiserver 真实端口。

为什么要这么设计?因为 kubelet 默认只配置一个 apiserver 地址,如果这个地址是某台 Master 的物理 IP,那台 Master 宕机后,该节点上的 kubelet 就彻底失联。用 VIP + 本地 haproxy 后,kubelet 永远访问同一个地址,haproxy 负责把流量分发到存活的 apiserver。keepalived 做的是 VIP 层面的故障转移,haproxy 做的是连接层面的负载均衡和故障剔除,两者配合才叫高可用入口。

nginx 做负载均衡也可以,但 haproxy 对 TCP 四层健康检查的支持更直接,配置也简单。选型时主要看团队熟哪个。关键是健康检查的探活路径要精确,这个坑我在第五章会详细说。

3. 从裸机到可装:证书、kubeconfig 与目录规划一次性搞定

3.1 主机与网络规划:IP、VIP、证书 CN 怎么定

动手写脚本之前,先把规划表定下来。三台 Master 的 IP、一个 VIP、三台 Master 的主机名、Pod 网段、Service 网段,这些是写死在证书和配置里的。证书里的 IP SAN 必须包含所有 apiserver 的访问地址:三台 Master 的物理 IP、VIP、以及将来可能用到的域名。漏掉任何一个 IP,就意味着将来从那个地址访问 apiserver 时证书校验失败,这就是二进制部署最常见的翻车原因。

我一般会先把规划写成一个变量文件,脚本里所有配置生成都从这个文件读取。变量文件如下:

# config.env,所有节点共用,脚本执行前手动确认 MASTER_IPS=("192.168.10.11" "192.168.10.12" "192.168.10.13") MASTER_NAMES=("k8s-m1" "k8s-m2" "k8s-m3") VIP=192.168.10.100 # 网段规划,不能与物理网络冲突 SERVICE_CIDR="10.96.0.0/12" POD_CIDR="10.244.0.0/16" CLUSTER_DNS="10.96.0.10"

这段变量的逻辑:MASTER_IPS 是证书里必须包含的物理地址,VIP 也必须在列。SERVICE_CIDR 和 POD_CIDR 决定了 Service 和 Pod 的 IP 范围,证书不用管它们,但 apiserver 的启动参数里要写。这里有个容易被忽略的点:POD_CIDR 和 SERVICE_CIDR 不能跟物理网络重叠,否则路由冲突时排查起来非常痛苦。

3.2 下载二进制与目录布局

二进制部署的第一步是把 kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kube-proxy、etcd 六个组件下载好。不需要下载 kubectl 之外的额外客户端工具,全部解压后按平台放到 /usr/local/bin 下。下载时注意对应版本的一致性,etcd 和 k8s 组件各自有版本号,没有强绑定,但 kubelet 和 apiserver 之间不能跨大版本,这个要记住。离线环境里,提前把二进制包拷到每台机器上,脚本负责解压和安装。

目录布局我一般固定如下:

# 在每个 Master 节点执行,创建标准目录 mkdir -p /etc/kubernetes/{pki,manifests,config} mkdir -p /var/lib/etcd mkdir -p /var/log/kubernetes mkdir -p /opt/k8s/bin

逻辑说明:/etc/kubernetes/pki 放所有证书,/etc/kubernetes/config 放 kubeconfig 和组件配置文件,/var/lib/etcd 是 etcd 的数据目录,/opt/k8s/bin 放自定义运维脚本。目录一旦定死,后续所有 systemd unit 的路径引用都基于这套规则,不要每台机器一个样,否则排查时全靠猜。

3.3 CA 证书与配置文件生成:openssl 到底在签什么

证书体系是整个二进制部署里最容易出错、也最不能出错的部分。集群内部要三套 CA:etcd CA、kubernetes CA、front-proxy CA。etcd CA 签 etcd 的 peer 证书和 client 证书,kubernetes CA 签 apiserver、kubelet、controller-manager、scheduler、admin 的证书,front-proxy CA 签聚合层证书。

openssl 生成 CA 和签发证书的过程里,最核心的是 SAN(Subject Alternative Name)。apiserver 的证书 SAN 必须包含上面变量里所有的 IP 和主机名,缺一个都不行。生成 apiserver 证书的典型写法:

# 生成 apiserver 私钥和证书请求 openssl genrsa -out apiserver.key 2048 # 创建 openssl 配置文件,包含 SAN 信息 cat > apiserver.cnf <<EOF [req] req_extensions = v3_req distinguished_name = req_distinguished_name [req_distinguished_name] [v3_req] basicConstraints = CA:FALSE keyUsage = keyEncipherment, dataEncipherment extendedKeyUsage = serverAuth subjectAltName = @alt_names [alt_names] IP.1 = 192.168.10.11 IP.2 = 192.168.10.12 IP.3 = 192.168.10.13 IP.4 = 192.168.10.100 DNS.1 = k8s-m1 DNS.2 = k8s-m2 DNS.3 = k8s-m3 DNS.4 = kubernetes.default.svc.cluster.local EOF # 用集群 CA 签发 apiserver 证书 openssl x509 -req -in apiserver.csr \ -CA ca.pem -CAkey ca.key -CAcreateserial \ -out apiserver.pem -days 3650 \ -extensions v3_req -extfile apiserver.cnf

参数说明:-days 3650 是证书有效期,内网环境十年一换问题不大,但要注意组织内部的安全策略,有些客户会强制要求一年有效期。-extensions v3_req 和 -extfile 是让签出来的证书带上 SAN,如果不带这两个参数,apiserver 启动后会因为证书里没有目标 IP 而拒绝 TLS 连接,报错信息是 x509: cannot validate certificate for IP address。

kubeconfig 生成时要注意 server 地址写 VIP。admin.kubeconfig、controller-manager.kubeconfig、scheduler.kubeconfig 都要指向 https://192.168.10.100:6443,而不是任何一台 Master 的物理 IP。否则组件连接 apiserver 时绕过了 haproxy,VIP 故障转移就失去了意义。

4. 一键部署脚本的核心设计:幂等、分阶段与 systemd 拉起

4.1 脚本主流程:分阶段执行与日志落盘

一键部署脚本的难点不在每条命令本身,而在编排。我会把整个流程拆成六个阶段:环境检查、证书生成、配置文件生成、etcd 启动、k8s 组件启动、高可用组件启动。每个阶段封装成独立函数,主函数按顺序调用,任何一个阶段失败就中止。脚本开头必须加上 set -euo pipefail,否则某个命令静默失败会导致后续步骤基于错误状态继续执行,这是脚本类工具最大的隐患。

主流程框架如下:

#!/usr/bin/env bash set -euo pipefail LOG_FILE="/var/log/k8s-deploy.log" # 日志函数,所有输出同时落到终端和文件 log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" } # 阶段函数声明,这里只列主要流程,具体实现后文展开 init_env() { log "初始化环境,检查端口、swap、防火墙"; } gen_certs() { log "生成 CA 与各组件证书"; } gen_configs() { log "生成 kubeconfig 与组件配置文件"; } start_etcd() { log "启动 etcd 集群"; } start_k8s() { log "启动 kubelet、apiserver 等核心组件"; } start_lb() { log "启动 haproxy 与 keepalived"; } # 每个阶段执行前检查是否已完成,实现幂等 deploy() { init_env gen_certs gen_configs start_etcd start_k8s start_lb } deploy 2>&1 | tee -a "$LOG_FILE"

幂等设计是脚本能不能反复执行的关键。我的做法是每个阶段函数开头检查产物标志:证书目录下如果已有 ca.pem,就跳过生成;etc/etcd 目录下如果已有 member 数据,就跳过初始化;systemd unit 文件已存在且服务是 active 状态,就跳过启动。这样脚本执行到一半挂了,修复问题后再跑一次,不会重复初始化导致数据损坏。日志落到 /var/log/k8s-deploy.log,每次执行追加,排错时能看到完整的失败现场。

4.2 kubelet 与 kube-proxy 的 systemd unit 写法

二进制部署下,每个组件一个 systemd unit。kubelet 是唯一必须由系统 init 拉起的组件,因为 kubelet 负责拉起其他静态 Pod。kubelet 的 unit 文件里最重要的参数是 --bootstrap-kubeconfig 和 --kubeconfig 的区分:首次启动用 bootstrap 配置向 apiserver 发起 CSR 申请,证书签发后再用正式 kubeconfig。这个机制在二进制部署里容易绕晕,很多人在单节点测试时会图省事直接用 admin.kubeconfig 当 kubelet 的 kubeconfig 用,但高可用集群里节点多、证书管理严格,建议走正规流程。

kubelet 的 unit 文件写法:

[Unit] Description=Kubernetes Kubelet After=containerd.service Requires=containerd.service [Service] ExecStart=/usr/local/bin/kubelet \ --bootstrap-kubeconfig=/etc/kubernetes/config/bootstrap-kubeconfig \ --kubeconfig=/etc/kubernetes/config/kubelet-kubeconfig \ --config=/etc/kubernetes/config/kubelet-config.yaml \ --pod-infra-container-image=registry.local/pause:3.9 Restart=always RestartSec=5 StartLimitInterval=0 [Install] WantedBy=multi-user.target

参数说明:--config 指向 kubelet 的 yaml 配置文件,里面写 SerializeImagePulls、maxPods 这类运行参数,不要全堆在命令行里。--pod-infra-container-image 指定 pause 镜像地址,离线环境必须改成内网镜像源,且版本要和容器运行时匹配。Restart=always 加上 RestartSec=5 是组件挂掉后自动拉起,StartLimitInterval=0 表示不限制重启次数,避免 etcd 数据恢复需要多次重启时 systemd 直接放弃。

kube-proxy 的配置相对简单,主要是一个 kubeconfig 和 mode 参数。二进制部署时 kube-proxy 默认以 iptables 模式工作,如果你的内核和网络环境支持 IPVS,可以在启动参数里加 --proxy-mode=ipvs。两者的差别在大量 Service 的场景下比较明显,IPVS 的负载均衡性能和规则更新速度更好,但 iptables 的兼容性更稳。我一般先在测试环境验证 IPVS 没问题再上生产。

4.3 高可用组件的配置生成:keepalived 与 haproxy

高可用集群的入口组件配置,是三台 Master 各自生成、内容略有不同的。haproxy 的配置在三台机器上基本一致,只有注释里的节点名不同。keepalived 的优先级和网卡名每台机器不同,优先级高的那台会成为 VIP 的初始持有者。

haproxy 配置里最关键的是健康检查的路径和端口:

global log /dev/log local0 maxconn 4096 defaults mode tcp timeout connect 5s timeout client 50s timeout server 50s frontend kube-apiserver bind *:6443 default_backend apiserver-backend backend apiserver-backend balance roundrobin option httpchk GET /healthz server k8s-m1 192.168.10.11:6443 check inter 3s fall 3 rise 2 server k8s-m2 192.168.10.12:6443 check inter 3s fall 3 rise 2 server k8s-m3 192.168.10.13:6443 check inter 3s fall 3 rise 2

option httpchk GET /healthz 这个探活路径是 apiserver 的健康检查端点,返回 200 表示 apiserver 正常。很多人会漏掉 /healthz 或者写成 /,apiserver 对根路径不会返回 200,haproxy 会判定后端全部 down,VIP 虽然漂移成功但所有连接都失败。check inter 3s fall 3 rise 2 表示每 3 秒探测一次,连续 3 次失败摘除节点,连续 2 次成功恢复节点。这个参数在 apiserver 启动慢的机器上要调大 fall 的次数,否则滚动重启时流量会被过早摘除。

keepalived 配置里,虚拟 IP 的网卡要选对。很多云环境里机器的默认网卡名不是 eth0,而是 ens33 之类,写错网卡名的话 VIP 根本绑不上去,但 keepalived 不报错,只是 VIP 一直处于 down 状态。用 ip addr 命令先确认网卡名,再写进配置。

4.4 一键执行时的交互边界:哪些该自动、哪些必须停

一键部署脚本不代表全程无脑执行。网络规划、版本选择、网卡名、镜像地址这些属于“环境事实”,脚本无法自动发现,必须提前确认。我的做法是脚本开头做环境预检:检查三台机器能互相 ping 通、检查端口 2379 和 6443 未被占用、检查 swap 已关闭、检查内核参数已设置。预检不通过就直接退出,并提示具体哪一项没满足。

同时,脚本必须支持分阶段执行。部署到 etcd 阶段失败了,修复后只需要重跑 start_etcd 之后的阶段,而不是从头再来。我会在主函数里加一个参数解析,支持 ./deploy.sh --from=start_etcd 这样的控制方式。这个设计在长时间部署、客户现场网络不稳的情况下特别有用,重跑整个脚本的成本远大于指定阶段重跑。

还有一点必须说明:脚本默认在第一个 Master 节点上生成全部证书和 kubeconfig,然后通过 SSH 分发给另外两台。这意味着第一个节点的 SSH 免密登录必须提前配好,脚本里不会去处理密钥拷贝。如果客户现场禁止 SSH 免密,就得改成把证书手动拷到其他机器,脚本只做本机执行。

5. 二进制部署避坑清单:五条真实踩坑记录与排查命令

5.1 kubelet 启动失败:证书 CN 与 Token 对不上

现象:kubelet 启动后一直报 x509: certificate is valid for, not 这样的错误,或者 CSR 申请被拒绝。

原因:kubelet 通过 bootstrap token 向 apiserver 发起 CSR 时,token 绑定的角色组是 system:bootstrappers,默认只能申请一个特定前缀的证书 CN。如果你在 bootstrap-kubeconfig 里写的 user 名称不是 system:node:节点名,或者 CSR 的 CN 不符合 system:node: 前缀规则,apiserver 的 CSR approving controller 会直接拒绝。

解决:检查 bootstrap token 的 secret 在 apiserver 里是否可见,检查 CSR 对象的 Common Name 和 Organization 是否符合预期。命令是 kubectl get csr 和 kubectl describe csr 加上 kubectl certificate approve 手动批准。如果证书已经批准但 kubelet 还是起不来,查看 kubelet 日志里的实际证书 CN,和节点的 metadata.name 是否一致。

5.2 etcd 集群起不来:peerURLs 地址写死问题

现象:etcd 三个节点各自启动后,journalctl 里报 etcdserver: request timed out,或者 is starting a new election 一直循环。

原因:etcd 的 initial-cluster 参数在第一次启动时用于形成集群,之后 etcd 会把集群信息持久化到数据目录。如果你第一次启动时 initial-cluster 里的地址写错了、或者用了 localhost,集群信息里就固化了一个错误地址,之后怎么改配置都不会生效。

解决:第一次启动前务必核对 initial-cluster 和 initial-advertise-peer-urls 里的 IP 和端口。如果已经启动错误,需要停掉 etcd,清空 /var/lib/etcd 数据目录,重新初始化。这里我吃过一次亏:在测试环境把 peerURL 写成了 localhost 想省事,结果三台机器的 etcd 都指向自己的本地端口,根本形不成集群。清空数据目录重新来一次,算是二进制部署里的唯一后悔药,生产环境一定要先备份 etcd 数据再动手。

5.3 apiserver 负载均衡后健康检查失效:探活路径要精确

现象:haproxy 配置没动,但 apiserver 后端节点被标记为 down,集群入口完全不可用。

原因:健康检查路径写错。apiserver 的端口上,根路径 / 不会返回 HTTP 200,返回的是 404 或者重定向,haproxy 判定节点不健康。另外,TLS 握手在某些版本下会让 HTTP 健康检查直接超时,需要在 haproxy 配置里关闭探测时的 TLS 校验。

解决:把 option httpchk GET /healthz 写成精确路径,并且加上 no-ssl 关键字(除非你配置了 haproxy 到 apiserver 之间的 TLS 透传)。最直接的排查方法是 curl -k https://物理IP:6443/healthz 手动验证,返回 ok 才能判定健康检查路径正确。这个坑在 apiserver 开启匿名认证时不容易发现,因为根路径有时候会返回 200 的空的 JSON。

5.4 节点 NotReady:pause 镜像版本被忽略

现象:kubelet 正常启动,集群里能看到节点,但状态一直是 NotReady,KubeletReady 的 condition 为 False,Pod 调度上去后 ContainerCreating 卡住。

原因:kubelet 启动时如果 --pod-infra-container-image 没有指定 pause 镜像,会默认从公共镜像仓库拉取 pause。离线或内网环境拉不下来,sandbox 容器创建失败,节点永远无法就绪。另外,pause 镜像的版本和容器运行时之间也存在兼容性,某个 containerd 版本可能不认太老或太新的 pause。

解决:提前在内网镜像仓库准备好 pause 镜像,在 kubelet 的 systemd unit 里显式指定 --pod-infra-container-image。用 crictl images 确认 pause 镜像已经存在,然后 systemctl restart kubelet。检查节点状态命令是 kubectl get node -o wide 和 kubectl describe node 看 Conditions 里的具体报错。

5.5 systemd 拉起失败:WorkingDirectory 与权限

现象:systemctl start kubelet 报错,提示 Failed at step EXEC,或者 kubelet 启动后马上退出,journalctl 里是 permission denied。

原因:systemd unit 文件里没有指定 WorkingDirectory,或者指定的目录不存在。kubelet 在启动过程中会在当前目录创建文件,如果当前目录没有写权限,组件会直接退出。另一个常见问题是用 root 用户启动所有组件时,证书目录的属主不对,导致 etcd 无法读取证书文件。

解决:在每个组件 unit 里写 WorkingDirectory=/var/lib/组件名,提前 mkdir -p 并 chown 给对应属主。etcd 的 style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;" />

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询