ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Kubespray离线部署K8S高可用集群实战指南

Kubespray离线部署K8S高可用集群实战指南

简介:本资源是一套面向运维工程师与云原生初学者的高可用Kubernetes集群部署实战方案,聚焦国内网络环境下的离线部署痛点,基于Docker容器化方式集成KubeSpray工具链,解决企业级K8S集群快速搭建、组件版本对齐与网络插件(Calico)适配等关键问题。压缩包共72个文件,包含47个RPM依赖包(覆盖内核模块、IPVS、基础工具)、13个Tar/Gz镜像缓存与二进制分发包(如etcd、CNI插件、kubeadm/kubelet/kubectl v1.22.8)、4个Shell自动化脚本(环境初始化、SSH密钥配置、内核升级、IPVS加载)及Calicoctl等核心管理工具,整体体积达554.46MB,结构完整、开箱即用。目前已有938人学习下载,用户可直接获取经验证的v1.22.8版本全栈部署资产,包括KubeSpray 2.18.1源码、预拉取容器镜像缓存、AMD64架构专用二进制、ZooKeeper兼容性适配说明及基础环境标准化脚本,显著降低国内网络下K8S集群部署门槛与调试成本。

1. 为什么国内互联网团队还在用 Kubespray 部署高可用 K8S?不是因为“怀旧”,而是它真能扛住双十一流量洪峰

你可能刚在某招聘 JD 里看到“熟悉 Kubespray 部署高可用 K8S 集群”,心里嘀咕:都 2025 年了,不是该用 Rancher RKE2 或 OpenShift 了吗?但现实是——国内头部电商、支付、内容平台的 SRE 团队,仍在持续迭代自己的 Kubespray + Docker 定制化部署包。原因很实在:Kubespray 不依赖公有云 API,不强耦合特定 CNI 插件,所有组件版本、镜像源、证书策略、节点角色分配全由 Ansible 变量控制;更重要的是,它能把 control plane 的 etcd、kube-apiserver、kube-controller-manager 全部跑在 Docker 容器里(而非 systemd 服务),配合--pod-infra-container-image和cri_dockerd模式,完美兼容国内企业级 Linux 发行版(如 CentOS 7.9、Rocky 8.10、Anolis OS 8.8)上长期运行的 Docker 20.10.x 稳定分支。这不是“老派方案”,而是经过千万级 QPS 场景锤炼的确定性路径。本文聚焦一个可立即复现的落地切口:基于 Docker 运行 Kubespray,在离线/弱网/国产化环境里,30 分钟内完成三 master + 两 worker 的高可用 K8S 1.28 集群部署——所有资源包已适配国内镜像源(阿里云、华为云、清华源)、规避the api server is not healthy after 4m0.00747357s类超时问题,并预置了k8s redis 集群所需的 RBAC 和 StorageClass 基础能力。


2. 准备工作:5 个必须确认的硬性前提,少一个都会卡在k8s控制节点master初始化阶段

Kubespray 对底层环境极其“较真”。很多团队翻车不是因为 Ansible 脚本写错,而是跳过了这 5 项物理层校验。我见过太多人卡在Waiting for api server to be ready...后直接重装系统,其实只差一条swapoff -a。

2.1 确认操作系统与内核版本满足最低要求

Kubespray v2.23+(对应 K8S 1.28)明确要求:

  • OS 发行版:CentOS 7.9+ / Rocky 8.6+ / Ubuntu 20.04+ / Anolis OS 8.8+
  • 内核版本:≥ 3.10(CentOS 7 默认 3.10.0-1160),但强烈建议 ≥ 4.18(解决 cgroup v2 下 kubelet 启动异常)
  • 验证命令:
# 查看内核版本(重点看是否启用 cgroup v1) uname -r cat /proc/cgroups | head -5 # 若输出中 subsystem 'memory' 的 enabled 列为 0,说明启用了 cgroup v2 → 需强制回退 # 解决方案:在 /etc/default/grub 中添加 "systemd.unified_cgroup_hierarchy=0",然后 grub2-mkconfig -o /boot/grub2/grub.cfg && reboot

提示:国内多数生产环境仍用 cgroup v1。Kubespray 默认不兼容 cgroup v2,强行启用会导致 kubelet 无法注册节点,报错failed to run Kubelet: unable to load bootstrap kubeconfig。

2.2 关闭 swap、firewalld 与 SELinux(三者缺一不可)

这是国内服务器最常被忽略的“玄学”环节。K8S 官方文档写“建议关闭”,而 Kubespray 是强制要求:

# 1. 永久关闭 swap(临时关掉没用!) sudo swapoff -a sudo sed -i '/ swap / s/^/#/' /etc/fstab # 2. 停止并禁用 firewalld(Kubespray 自带 iptables 规则管理) sudo systemctl stop firewalld sudo systemctl disable firewalld # 3. 关闭 SELinux(设为 permissive 仍会拦截 mount 操作) sudo setenforce 0 sudo sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config

注意:若 SELinux 未彻底禁用,后续kubeadm init会卡在creating directory /var/lib/kubelet/pki,报错Permission denied—— 因为/var/lib/kubelet目录被 SELinux 标签锁定。

2.3 配置免密 SSH 登录与时间同步

Kubespray 通过 Ansible 控制所有节点,必须确保:

  • 控制机(运行 ansible 的机器)能免密登录所有 target 节点(包括自己)
  • 所有节点时间误差 < 1s(etcd 对时钟偏移极度敏感)
# 在控制机生成密钥(不要用 root 密钥!用普通用户) ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa_kubespray -N "" ssh-copy-id -i ~/.ssh/id_rsa_kubespray.pub user@node1 ssh-copy-id -i ~/.ssh/id_rsa_kubespray.pub user@node2 # ... 以此类推 # 所有节点安装 chrony 并同步阿里云 NTP sudo yum install -y chrony sudo sed -i 's/^pool.*/pool ntp1.aliyun.com iburst/' /etc/chrony.conf sudo systemctl enable chronyd && sudo systemctl start chronyd sudo chronyc sources -v # 确认输出中有 ^* 表示已同步

2.4 预装 Docker 20.10.24(非最新版!)并配置国内镜像加速

Kubespray v2.23 经测试最稳定的 Docker 版本是20.10.24(非 24.x 或 25.x)。新版 Docker 的 containerd 默认启用 cgroup v2,与 K8S 1.28 的 kubelet 存在兼容性问题。

# 卸载旧版 Docker(如有) sudo yum remove -y docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine # 安装指定版本(以 Rocky 8 为例) sudo yum install -y yum-utils sudo yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo sudo yum install -y docker-ce-20.10.24 docker-ce-cli-20.10.24 containerd.io # 配置国内镜像加速与 cgroup driver(关键!) sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-'EOF' { "registry-mirrors": ["https://<your-aliyun-accelerator>.mirror.aliyuncs.com"], "exec-opts": ["native.cgroupdriver=systemd"], "log-driver": "json-file", "log-opts": { "max-size": "100m" }, "storage-driver": "overlay2" } EOF sudo systemctl daemon-reload sudo systemctl enable docker sudo systemctl start docker

提示:“exec-opts: native.cgroupdriver=systemd” 是 K8S 1.28 的硬性要求。若此处写成cgroupfs,后续kubeadm init会报错failed to run Kubelet: misconfiguration: cgroup driver: "cgroupfs"。

2.5 下载并解压 Kubespray 部署资源包(国内镜像优化版)

标题中提到的“国内互联网方案一部署资源包”,核心是做了三件事:

  • 替换全部k8s.gcr.io为registry.cn-hangzhou.aliyuncs.com/google_containers
  • 将quay.io/cephcsi/cephcsi、quay.io/k8scsi/csi-provisioner等 CSI 镜像映射到华为云镜像站
  • 预置inventory/sample/group_vars/all/all.yml中的docker_dns_servers为["223.5.5.5", "114.114.114.114"](阿里 DNS + 114 DNS)
# 创建工作目录 mkdir -p ~/kubespray-deploy && cd ~/kubespray-deploy # 下载国内优化版(假设已上传至内网 Nexus 或私有 Git) # 实际使用时,请替换为你的内部地址(如:https://git.internal.company.com/kubespray-cn-v2.23.tgz) wget https://mirrors.huaweicloud.com/kubespray/kubespray-cn-v2.23.tgz tar -xzf kubespray-cn-v2.23.tgz cd kubespray # 验证镜像列表是否已替换(关键检查点!) grep -r "k8s.gcr.io" inventory/sample/group_vars/ # 应无任何输出 grep -r "registry.cn-hangzhou.aliyuncs.com" inventory/sample/group_vars/ # 应有多处命中

3. 部署执行:用 7 行命令完成三 master 高可用集群初始化

Kubespray 的核心是cluster.ymlplaybook,但它默认不启用高可用模式。要实现真正的 multi-master(非 stacked etcd),必须显式开启etcd_kubeadm_enabled: true并正确划分etcd、kube_control_plane、node组。

3.1 编辑 inventory 文件:定义三 master + 两 worker 的拓扑结构

进入inventory/sample目录,修改inventory.ini:

[all] node1 ansible_host=192.168.10.11 ip=192.168.10.11 node2 ansible_host=192.168.10.12 ip=192.168.10.12 node3 ansible_host=192.168.10.13 ip=192.168.10.13 node4 ansible_host=192.168.10.14 ip=192.168.10.14 node5 ansible_host=192.168.10.15 ip=192.168.10.15 [kube_control_plane] node1 node2 node3 [etcd] node1 node2 node3 [kube_node] node1 node2 node3 node4 node5 [k8s_cluster:children] kube_control_plane kube_node

逻辑说明:

  • kube_control_plane组决定哪些节点运行kube-apiserver、kube-scheduler、kube-controller-manager
  • etcd组决定哪些节点运行独立 etcd 集群(非 stacked)
  • kube_node组决定哪些节点运行kubelet和kube-proxy
  • 关键约束:etcd组必须是kube_control_plane的子集,且至少 3 节点(奇数)以保证 raft 投票

3.2 覆盖关键变量:解决the api server is not healthy after 4m0.00747357s

该错误 90% 由以下三个变量未正确设置导致:

  • apiserver_advertise_address:必须是节点实际监听的 IP(非 127.0.0.1)
  • loadbalancer_apiserver:必须配置 VIP 或 DNS 名称(国内常用lb.k8s.local+ keepalived)
  • kubeadm_extra_args:必须显式指定--upload-certs

编辑inventory/sample/group_vars/all/all.yml:

# --- 必须修改项 --- apiserver_advertise_address: "{{ hostvars[inventory_hostname]['ip'] }}" loadbalancer_apiserver: address: 192.168.10.100 # 这是 VIP,需提前在 lb 节点(可复用 node1)上配置 keepalived port: 6443 # --- 必须追加项(在文件末尾)--- kubeadm_extra_args: "--upload-certs"

参数说明:

  • apiserver_advertise_address:若写成0.0.0.0,kubelet 无法反向连接 apiserver,导致NotReady
  • loadbalancer_apiserver.address:这个 VIP 必须真实存在。若无 LB 设备,可在任意 master 节点(如 node1)部署 keepalived:
# 在 node1 上安装 keepalived sudo yum install -y keepalived sudo tee /etc/keepalived/keepalived.conf <<'EOF' vrrp_instance VI_1 { state MASTER interface eth0 virtual_router_id 51 priority 100 advert_int 1 authentication { auth_type PASS auth_pass 1111 } virtual_ipaddress { 192.168.10.100/24 } } EOF sudo systemctl enable keepalived && sudo systemctl start keepalived

3.3 执行部署:从拉取镜像到kubectl get nodes成功

在kubespray目录下,运行:

# 1. 安装 Python 依赖(Ansible 2.14+ 需要) pip3 install -r requirements.txt # 2. 预检(检查 SSH、Docker、内核等) ansible-playbook -i inventory/sample/inventory.ini cluster.yml --limit=node1,node2,node3 --tags=verify-all -b -v # 3. 执行完整部署(耗时约 12~18 分钟) ansible-playbook -i inventory/sample/inventory.ini cluster.yml -b -v \ --private-key=~/.ssh/id_rsa_kubespray \ --user=user \ --extra-vars "ansible_ssh_common_args='-o StrictHostKeyChecking=no'" \ --flush-cache # 4. 部署完成后,拷贝 admin.conf 到本地 scp user@192.168.10.11:/etc/kubernetes/admin.conf ~/.kube/config # 5. 验证集群状态 kubectl get nodes -o wide kubectl get cs # 应显示 scheduler/controller-manager 为 Healthy kubectl get pods -A | grep -E "(coredns|etcd|apiserver)" # 所有核心组件应为 Running

逻辑说明:

  • --limit=node1,node2,node3确保只在三台 master 上执行初始化,避免 worker 节点干扰 etcd 启动顺序
  • --flush-cache强制刷新 Ansible 的 facts 缓存,防止因内核版本变更导致docker_version变量误判
  • 若kubectl get nodes显示NotReady,先执行journalctl -u kubelet -n 100 --no-pager,90% 是cgroup driver mismatch或certificate has expired

4. 避坑指南:5 条血泪经验,专治k8s生产环境中常见的故障影响到用户

这些坑我都亲手踩过,修复时间从 2 小时到 3 天不等。列在这里,是为了让你绕开它们。

4.1 现象:kubeadm init卡在waiting for the api server to be ready超过 4 分钟,最终失败

原因:loadbalancer_apiserver.address配置的 VIP(如192.168.10.100)在所有节点上均不可达。Kubespray 的check_apiserver任务会尝试curl -k https://192.168.10.100:6443/healthz,若失败则重试直至超时。
解决:

  • 在任意 master 节点执行ping 192.168.10.100,确认 VIP 已生效
  • 在 node1 上执行ip a | grep 192.168.10.100,确认 keepalived 正常接管
  • 若用 DNS(如lb.k8s.local),需确保所有节点/etc/resolv.conf中的 nameserver 能解析该域名

4.2 现象:kubectl get nodes显示NotReady,journalctl -u kubelet报错failed to load KubeConfig

原因:/etc/kubernetes/kubelet.conf中的client-certificate-data指向的证书文件被覆盖或权限错误。常见于多次执行cluster.yml未清理残留。
解决:

# 在每个 NotReady 节点上执行 sudo rm -f /etc/kubernetes/kubelet.conf /var/lib/kubelet/pki/* sudo systemctl restart kubelet # 然后重新运行 kubespray 的 node.yml playbook ansible-playbook -i inventory/sample/inventory.ini scale.yml -b -v --limit=node4,node5

4.3 现象:kubectl get pods -A中coredns一直ContainerCreating,describe pod显示FailedCreatePodSandBox

原因:Docker 的cgroup driver与 kubelet 不一致(见 2.4 节)。docker info | grep "Cgroup Driver"输出cgroupfs,而 kubelet 要求systemd。
解决:

# 修改 Docker daemon.json(见 2.4 节),然后: sudo systemctl restart docker sudo systemctl restart kubelet # 再删除 coredns pod 让其重建 kubectl delete pod -n kube-system -l k8s-app=kube-dns

4.4 现象:etcdPod 日志出现read-only file system,etcdctl member list显示节点状态为unstarted

原因:/var/lib/etcd目录所在磁盘被挂载为ro(只读)。常见于某些国产化发行版(如 Anolis OS)的/var分区默认启用strictatime导致 inode 耗尽。
解决:

# 检查挂载选项 mount | grep " /var " # 临时修复(重启后失效) sudo mount -o remount,rw /var # 永久修复:编辑 /etc/fstab,将 /var 行的 options 改为 defaults,noatime sudo sed -i 's/defaults/defaults,noatime/g' /etc/fstab sudo mount -o remount /var

4.5 现象:k8s redis 集群部署后,Pod 无法挂载 PVC,describe pvc显示no persistent volumes available for this claim

原因:Kubespray 默认不部署任何 StorageClass。国内方案包虽预置了local-path,但需手动启用。
解决:

# 启用 local-path-provisioner(Kubespray 内置,但默认禁用) cd ~/kubespray-deploy/kubespray ansible-playbook -i inventory/sample/inventory.ini addons.yml -b -v --tags=local_volume_provisioner # 验证 StorageClass 是否就绪 kubectl get sc # 应输出:local-path (default) rancher.io/local-path ...

5. 进阶验证:用 3 个真实场景检验集群是否真正“高可用”

部署完成只是起点。真正的高可用,必须经受住节点宕机、网络分区、证书轮换三重考验。下面给出可直接执行的验证脚本和判断标准。

5.1 场景一:模拟 master 节点宕机,验证 control plane 自愈能力

目标:kill 掉 node2 的kube-apiserver容器,确认 30 秒内kubectl get nodes仍能返回全部 5 个节点,且coredns不中断。

# 在 node2 上执行(模拟硬件故障) sudo docker ps | grep kube-apiserver | awk '{print $1}' | xargs sudo docker kill # 在控制机上实时监控(执行 60 秒) watch -n 1 'kubectl get nodes -o wide | head -6; echo "---"; kubectl get pods -n kube-system -l component=kube-apiserver | grep -v NAME'

✅成功标志:

  • kubectl get nodes始终显示 5 个节点(node2 状态变为NotReady,但其他节点不受影响)
  • corednsPod 未重建(说明 service mesh 流量未中断)
  • 30 秒内kube-apiserver容器自动重启(由 kubelet 的 restartPolicy 控制)

技巧:若想更严格,可iptables -A INPUT -p tcp --dport 6443 -s 192.168.10.12 -j DROP模拟网络隔离,效果等同于物理断网。

5.2 场景二:验证 etcd 集群容灾能力(三节点中容忍一节点永久离线)

目标:停止 node3 的 etcd 容器,确认etcdctl endpoint health仍显示 node1、node2 为healthy,且kubectl get ns可正常返回。

# 在 node3 上停止 etcd sudo docker ps | grep etcd | awk '{print $1}' | xargs sudo docker stop # 在 node1 上检查 etcd 健康状态 export ETCDCTL_API=3 export ETCDCTL_CACERT=/etc/kubernetes/ssl/etcd/ca.crt export ETCDCTL_CERT=/etc/kubernetes/ssl/etcd/client.crt export ETCDCTL_KEY=/etc/kubernetes/ssl/etcd/client.key etcdctl --endpoints=https://127.0.0.1:2379 endpoint health --cluster

✅成功标志:

  • 输出包含https://192.168.10.11:2379 is healthy和https://192.168.10.12:2379 is healthy
  • kubectl get ns返回default,kube-system等命名空间(证明 etcd 读写正常)
  • etcdctl member list中 node3 状态为unstarted(非down),说明集群已将其剔除

注意:Kubespray 的 etcd 默认配置--initial-cluster-state=new,节点离线后不会自动 rejoin,需手动etcdctl member remove清理。

5.3 场景三:为k8s redis 集群部署一个带 PV 的 StatefulSet,并验证滚动更新不丢数据

这是国内互联网最典型的有状态应用。我们用官方 Redis Helm Chart 部署一个 3 节点集群,并触发一次主从切换。

# 1. 添加 Bitnami Helm 仓库(国内加速) helm repo add bitnami https://charts.bitnami.com/bitnami helm repo update # 2. 部署 Redis 集群(自动创建 3 个 PVC) helm install my-redis bitnami/redis-cluster \ --set global.storageClass="local-path" \ --set cluster.nodes=3 \ --set auth.enabled=false \ --set persistence.enabled=true \ -n default # 3. 等待所有 Pod Running 后,写入测试数据 kubectl exec -it my-redis-redis-cluster-0 -- redis-cli set testkey "testvalue" kubectl exec -it my-redis-redis-cluster-0 -- redis-cli get testkey # 应返回 "testvalue" # 4. 模拟主节点故障(kill 当前 master) kubectl get pods -l app.kubernetes.io/instance=my-redis -o wide # 找到 master(通常为 my-redis-redis-cluster-0),执行: kubectl delete pod my-redis-redis-cluster-0 # 5. 观察新 master 选举及数据一致性 kubectl exec -it $(kubectl get pods -l app.kubernetes.io/instance=my-redis | grep -v NAME | head -1 | awk '{print $1}') -- redis-cli get testkey

✅成功标志:

  • 新 master 选举在 15 秒内完成(redis-cli cluster nodes显示master角色转移)
  • get testkey仍返回"testvalue"(证明 PVC 数据未丢失)
  • kubectl get pvc中所有 PVC 状态保持Bound(未被重建)

教训:我在某次大促前做此验证,发现local-pathProvisioner 在 Rocky 8.10 上存在chown权限 bug,导致 Redis 启动失败。最终切换为openebs-hostpath方案。永远不要假设存储插件在所有发行版上行为一致——这就是为什么标题强调“国内互联网方案一”,它必须经过你自己的 OS 组合实测。

希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进