
简介一套基于二进制方式部署高可用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 只连接 apiserverkubelet 只连接 apiserverkube-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 集群则是另找三台机器专门跑 etcdMaster 只跑 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) VIP192.168.10.100 # 网段规划不能与物理网络冲突 SERVICE_CIDR10.96.0.0/12 POD_CIDR10.244.0.0/16 CLUSTER_DNS10.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 到底在签什么证书体系是整个二进制部署里最容易出错、也最不能出错的部分。集群内部要三套 CAetcd CA、kubernetes CA、front-proxy CA。etcd CA 签 etcd 的 peer 证书和 client 证书kubernetes CA 签 apiserver、kubelet、controller-manager、scheduler、admin 的证书front-proxy CA 签聚合层证书。openssl 生成 CA 和签发证书的过程里最核心的是 SANSubject 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 时绕过了 haproxyVIP 故障转移就失去了意义。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 21 | 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] DescriptionKubernetes Kubelet Aftercontainerd.service Requirescontainerd.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-imageregistry.local/pause:3.9 Restartalways RestartSec5 StartLimitInterval0 [Install] WantedBymulti-user.target参数说明--config 指向 kubelet 的 yaml 配置文件里面写 SerializeImagePulls、maxPods 这类运行参数不要全堆在命令行里。--pod-infra-container-image 指定 pause 镜像地址离线环境必须改成内网镜像源且版本要和容器运行时匹配。Restartalways 加上 RestartSec5 是组件挂掉后自动拉起StartLimitInterval0 表示不限制重启次数避免 etcd 数据恢复需要多次重启时 systemd 直接放弃。kube-proxy 的配置相对简单主要是一个 kubeconfig 和 mode 参数。二进制部署时 kube-proxy 默认以 iptables 模式工作如果你的内核和网络环境支持 IPVS可以在启动参数里加 --proxy-modeipvs。两者的差别在大量 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 2option httpchk GET /healthz 这个探活路径是 apiserver 的健康检查端点返回 200 表示 apiserver 正常。很多人会漏掉 /healthz 或者写成 /apiserver 对根路径不会返回 200haproxy 会判定后端全部 downVIP 虽然漂移成功但所有连接都失败。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 --fromstart_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 节点 NotReadypause 镜像版本被忽略现象kubelet 正常启动集群里能看到节点但状态一直是 NotReadyKubeletReady 的 condition 为 FalsePod 调度上去后 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;" />