简介:面向希望在国内网络环境下基于Docker和Kubespray快速部署高可用K8S集群的运维与容器工程师,这份资源包整理离线部署所需的核心组件与脚本,解决依赖下载困难、镜像获取不稳定等问题。这套gz压缩包内共72个文件,约554.46MB,包含47个rpm基础环境与内核依赖包、13个tar格式的etcd/kubeadm/kubectl等二进制及镜像包、4个shell脚本用于环境初始化、SSH密钥配置、内核升级与基础工具安装,另有Calico网络插件及Kubespray 2.18.1源码包,适合在内网或离线环境直接使用。已有938人学习下载,尤其适用于具备一定Docker和Linux基础的中高级运维人员快速搭建生产可用的高可用集群。资源内文件按基础环境、工具链、镜像包等模块组织,可结合Kubespray部署流程理解Kubeadm、Etcd、Calico等组件的实际作用,减少反复检索和试错成本。
1. 国内跑通 Kubespray 高可用 K8S:先把镜像和 Ansible 缓存的问题解决掉
用 Kubespray 在裸机或云服务器上部署高可用 K8S,本身不是什么新鲜事,它的 Ansible 剧本和 Kubeadm 生成逻辑在众多生产环境里都验证过。但国内实操卡住的从来不是 Kubespray 本身的逻辑,而是三个绕不开的现实问题:Docker Hub 镜像拉不动、GitHub 源码下载超时、Ansible 从公共源装依赖装到一半断掉。这三个问题不解决,仓库 Clone 下来、Inventory 写对,最后照样卡在kubeadm init拉镜像那一步,而且报错信息极其简单,就是Failed to pull image,看不到任何修复线索。
这套资源包把国内互联网环境下的部署路径重新走了一遍,核心思路是:在 Docker 基础上先准备好可用的容器运行时,再用 Kubespray 的 Ansible 剧本完成高可用 K8S 集群的部署,每个环节的依赖包、离线镜像、Ansible Role 缓存全部提前下载好,不需要依赖海外网络。适合的读者是:已经被 kubeadm 手动部署折腾过、想用成熟工具链快速搭起一套带负载均衡入口的生产级 K8S 集群的运维和开发,也适合刚接触 K8S、需要从零构建一个可用集群并想看清每一步发生了什么的学习者。这篇文章会把资源包里的每一项内容拆开讲清楚,包括目录结构怎么用、Inventory 怎么改、避坑记录,以及部署完成后怎么验证高可用是否真的成立。
2. 部署资源包拆解:目录结构、镜像清单与离线依赖
2.1 资源包里到底有哪些东西
拿到部署资源包后,第一件事不是执行安装脚本,而是打开目录结构看明白每个文件的作用。常见做法是资源包按kubespray_src/、offline_imgs/、roles_cache/、inventory_conf/四块组织,每块解决的问题不同。
kubespray_src/是 Kubespray 的源码,对应某个 release 版本。如果资源包没有明确标注版本,先查看git describe结果或者README.md里的版本号。不同版本的 Kubespray 对 Kubernetes 版本的默认值不同,后续改 Inventory 时要与它对齐。offline_imgs/存放所有部署过程中需要用到的 Docker 镜像,通常按组件分目录:kube_apiserver、kube_controller_manager、kube_scheduler、kube_proxy、etcd、pause、coredns、calico、kubespray自带的一些辅助镜像。每个目录下有xxx.tar格式的镜像归档文件,还有一份images_list.txt记录镜像的完整名称和 Tag。roles_cache/是 Ansible Role 的本地缓存,Kubespray 的 Role 依赖都放在这里,避免在线安装时访问 Ansible Galaxy 超时。inventory_conf/里面有多个 Inventory 模板,比如inventory.ini、inventory_ha.ini、inventory_single.ini,以及all.yml、k8s_cluster.yml、etcd.yml等配置文件的示例。
整个资源包的设计目的很明确:让ansible-playbook全程离线执行。所以除了部署目标机器之间需要网络互通外,控制节点本身不需要访问海外源。这也是这套方案与官方文档在线部署方式最大的差异点。
2.2 控制节点准备:Docker 与 Python 依赖
在开始部署之前,控制节点需要准备好两件事:Docker 运行时和 Python 依赖。控制节点即执行 Ansible 命令的那台机器,通常建议是独立于集群节点之外的跳板机。
Docker 安装这里不展开。只提示一点:确认docker version客户端与服务端版本一致,且systemctl status docker状态是active (running)。很多人的 K8S 部署失败不是集群配置错了,而是 Docker 本身没起来,后面所有容器运行时操作全是Cannot connect to the Docker daemon。这个报错常见于刚装完 Docker 还没启动服务,或者containerd与 Docker 的 socket 冲突。资源包里的镜像导入脚本默认使用 Docker CLI 操作,所以必须先确认 Docker 可用。
Python 依赖主要给 Ansible 用。Kubespray 要求 Ansible 版本与源码要求匹配,一般情况是 Ansible 7.x 对应 Kubespray 2.22 以上的版本。资源包里的requirements.txt会列出所有依赖,安装命令为:
pip install -r requirements.txt --no-index --find-links=./pip_wheels提示:
pip_wheels目录如果资源包没有提供,可以改用系统 Python 自带的 pip 配合国内 PyPI 镜像安装,但版本务必与requirements.txt锁定的版本一致,特别是ansible-core、jinja2和cryptography这三个包,版本不对会在执行 Playbook 时出现方法签名错误,报错信息很隐蔽。
安装完成后验证一下 Ansible 版本:
ansible --version正常输出会显示ansible [core 2.14.x]这样的版本信息。如果提示No module named ansible,检查当前终端使用的 Python 环境是否与 pip 安装时一致,尤其是同时装有 Python2 和 Python3 的系统中,常见做法是在虚拟环境里安装依赖。
2.3 离线镜像导入与加载的完整流程
offline_imgs/里的镜像归档文件,需要在控制节点上先导入到本地 Docker,这样后面某个docker load顺序才不会乱。镜像导入脚本通常长这样:
#!/bin/bash OFFLINE_IMGS_DIR="./offline_imgs" for tar_file in ${OFFLINE_IMGS_DIR}/*.tar; do echo "Loading image archive: ${tar_file}" docker load -i "${tar_file}" if [ $? -ne 0 ]; then echo "Failed to load ${tar_file}" exit 1 fi done echo "All images loaded successfully."这段脚本的逻辑很简单:把所有.tar格式的镜像归档文件逐个导入 Docker。逻辑说明:循环遍历offline_imgs目录下的每个文件,docker load -i从归档文件中恢复镜像到本地镜像库,每加载一个文件后立即检查退出码,失败则终止脚本,避免后续 Playbook 执行到一半发现镜像缺失。
参数说明:$?是上一条命令的退出状态码,0表示成功,非零值表示执行出错。exit 1直接终止脚本,防止带着缺失的镜像继续走流程。执行完后验证镜像是否完整:
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"对照offline_imgs/images_list.txt里的镜像清单,逐条核对 Repository 和 Tag 是否存在。这里最常见的坑是脚本使用了*.tar通配符,但部分资源包的镜像归档文件是.tar.gz压缩格式,调整脚本即可,再跑一遍。镜像全部导入完成后,Kubespray 在部署流程中通过kubeadm拉取镜像的步骤就会直接走本地 Docker 缓存,不再访问外部仓库。
3. 修改 Inventory 与 HA 设计:从单节点思维到生产级高可用
3.1 Inventory 文件结构解析
Kubespray 部署的核心入口是 Inventory 文件,它定义了集群由哪些节点组成、各组件的部署位置以及网络插件的选择。资源包里的inventory_ha.ini已经给出了一个三节点的 HA 拓扑样例,需要根据实际机器修改 IP 和角色。
典型的 HA 拓扑如下所示:
[all] master01 ansible_host=192.168.1.21 ip=192.168.1.21 etcd_member_name=etcd1 master02 ansible_host=192.168.1.22 ip=192.168.1.22 etcd_member_name=etcd2 master03 ansible_host=192.168.1.23 ip=192.168.1.23 etcd_member_name=etcd3 node01 ansible_host=192.168.1.24 ip=192.168.1.24 [kube_control_plane] master01 master02 master03 [etcd] master01 master02 master03 [kube_node] node01 [calico_rr] [k8s_cluster:children] kube_control_plane kube_node calico_rr结构拆开来看:[all]组定义了所有参与集群的机器,ansible_host是 Ansible SSH 连接用的地址,ip是配置网络插件时使用的内网 IP,etcd_member_name是 etcd 节点的唯一标识。[kube_control_plane]组指定三个 Master 节点,[etcd]组指定 etcd 部署位置,[kube_node]组是工作节点,[calico_rr]是 Calico Route Reflector 专用组,单集群规模不需要额外节点,保留空组即可。
这里有一条关键规则:高可用集群要求etcd组与kube_control_plane组的节点数都为奇数,目的是保证 etcd 选主时能形成多数派,防止分区场景下出现脑裂。三个 Master 节点同时承载 etcd 是最常见的部署方式,即堆叠式 etcd。如果 Master 和 etcd 分离部署,需要额外的机器,资源包默认不采用这种拓扑。
3.2 HA 入口的选择:Kubespray 的 VIP 机制与外部 LB 的对比
高可用 K8S 集群的核心问题是:APIServer 有三个实例,客户端(kubelet、kube-proxy、kubectl)到底该连哪个 IP。Kubespray 支持两种方案:内置的 keepalived 虚拟 IP 和外部负载均衡器地址。两者的选择直接决定了后续网络流量入口的可靠性。
资源包里的默认方案是走 keepalived + VIP。这种方式在配置中体现为以下几项:
# all.yml loadbalancer_apiserver: address: 192.168.1.20 port: 6443 keepalived_interface: eth0参数说明:address是虚拟 IP,三个 Master 节点会通过 keepalived 协议竞选这个 IP 的持有权,注意它不能被集群内任何物理机器占用。keepalived_interface绑定的网卡名,取决于你的机器,默认eth0,云主机可能是ens3或ens192,不一致会导致 VIP 不生效。port值与 kube-apiserver 的 secure-port 保持一致,默认 6443。
外部 LB 方案则直接指向云平台的 SLB 或自建的 HAProxy + Keepalived:
loadbalancer_apiserver: address: 10.0.0.100 port: 6443两种方式的取舍:VIP 方案不依赖额外的网络设备,但存在一个选主失败的风险——如果所有 Master 节点的网卡名不一致,keepalived 根本不会绑定接口;外部 LB 方案更贴近云原生架构的演进方向,集群与负载均衡器解耦,但需要额外维护一套 LB 环境。对于第一次用 Kubespray 的人,我建议优先用 VIP 方案,因为资源包已经把它配置好了,不需要额外搭建。
3.3 kube_control_plane 与 etcd 的组配置细节
all.yml中还有一个容易被忽略的配置项etcd_deployment_type。它有两个取值:docker和host。如果系统里已经装好了 Docker,那么docker模式会让 etcd 以容器方式运行,管理起来方便,日志统一由 Docker 接管;host模式则要求机器上有 etcd 二进制文件,适合没有容器运行时的场景。资源包的定位是“基于 Docker 部署”,所以默认使用docker模式。
同时需要注意kube_control_plane组里的节点一旦定义,控制平面的三个组件(apiserver、controller-manager、scheduler)会以 static pod 的方式运行在 kubelet 下,由此引出一个隐含条件:kubelet 必须先启动,控制平面组件才能运行。Kubespray 的 Playbook 执行顺序已经处理了这个问题,但人为手动重启时要注意顺序,不要单独重启某一台 Master 上的 kubelet,那样会造成集群控制面组件短时不可用,但不会导致数据丢失。
部署集群时另一个需要提前确定的参数是kube_network_plugin。资源包默认使用 Calico,原因很简单:它同时支持网络策略和 BGP 路由分发,生产环境覆盖率高。修改这个参数只需在all.yml或k8s_cluster.yml中调整:
kube_network_plugin: calicoCalico 的底层封装是 VXLAN,因为跨网段场景下 BGP 需要额外的路由器和 AS 配置,VXLAN 模式无需机房网络设备配合,开箱即用。
4. 执行 Ansible Playbook:离线模式下的部署流程与配置细节
4.1 预检连通性:从 Ansible ping 到端口可访问
在真正开始部署之前,先要让控制节点打通与所有目标机器的信任关系和网络连通性。Kubespray 的执行账号需要免密 sudo 权限,否则后面大量模块会因权限不足而报错。
先做一次连通性验证:
ansible -i inventory_ha.ini all -m ping常见的输出是每台机器返回"pong"。如果出现unreachable,检查 SSH 端口、密钥和防火墙规则三处。注意这里验证的是控制节点到目标机器的 SSH,与集群内部的网络互通是两码事。
接下来验证目标机器之间的端口互通。Kubespray 部署过程中需要在节点之间传输证书、镜像和配置文件,端口范围包括 6443(apiserver)、2379-2380(etcd 客户端和 peer 通信)、10250(kubelet 指标接口)等。在控制节点上执行:
ansible -i inventory_ha.ini all -m shell -a "test -e /etc/docker && echo ok || echo missing"这行命令的作用是检查目标机器上 Docker 是否安装。Kubespray 本身支持container_manager参数选择containerd或docker,资源包标题明确强调“基于 docker”,所以目标机器上必须预装 Docker。如果未安装,后面执行到容器运行时配置时,Playbook 会尝试拉取 Docker 相关包,而这在国内网络环境下很可能超时,直接违背本方案的离线初衷。
4.2 通过环境变量控制离线安装模式
Kubespray 的离线部署依赖两个关键的环境变量或配置文件参数:bootstrap_os和download_run_once。资源包里的group_vars/all/all.yml已经写好了,但建议在命令行执行时再次明确指定,防止某些变量被覆盖。
典型的执行命令如下:
ansible-playbook \ -i inventory_ha.ini \ -u root \ -b \ --become-user=root \ -e "download_run_once=true" \ -e "download_localhost=true" \ -e "docker_offline_enabled=true" \ cluster.yml参数说明:-u root指定 SSH 登录用户,如果你的账号不是 root 而是具有 sudo 权限的普通用户,改成-u ansible并确保 sudo 免密;-b表示 become,即提权执行;--become-user=root是提权后的目标用户;download_run_once=true让 Kubespray 只在一个节点上下载容器镜像,然后通过docker save和docker load分发到其他节点,避免每个节点重复拉取;download_localhost=true表示下载动作在控制节点执行,配合资源包的offline_imgs使用,实际不会走网络;docker_offline_enabled=true是资源包自定义的变量,用于告诉 Playbook 走离线镜像路径。
提示:
cluster.yml是 Kubespray 的主 Playbook,它会依次执行 etcd 部署、容器运行时配置、kubelet 安装、控制平面初始化和网络插件安装。执行时间视集群规模而定,三个 Master 加一个 Node 的拓扑大约需要 15 到 30 分钟。
4.3 部署过程的卡点监控与日志定位
Ansible Playbook 执行过程中,终端输出的是每个 Task 的执行结果,分为ok、changed、failed。当某个 Task 长时间不返回时,不要急着 Ctrl+C,先判断它在做什么。Kubespray 部署中最耗时的几个 Task 是:Wait for kubelet to be ready、Wait for apiserver to be available和镜像加载过程。
如果 Task 一直卡在kubelet相关阶段,常见做法是打开另一终端登录对应节点,检查 kubelet 服务状态:
systemctl status kubelet --no-pager journalctl -u kubelet --no-pager -n 50日志中出现Unable to update cni config说明 Calico 还没就绪,需要等待网络插件生效;出现Container runtime network not ready时检查 Calico Pod 是否在运行。
如果最终部署失败,Ansible 会打印出失败的 Task 名称。此时不要重新从头跑整个cluster.yml,因为 Kubespray 支持断点续跑,重跑会自动跳过已经完成的 Task。但有一个例外:如果失败发生在kubeadm init阶段之后,重跑可能导致 Kubelet 状态混乱,更稳妥的做法是先执行reset.yml清理节点,再重新部署。
ansible-playbook -i inventory_ha.ini reset.yml这条命令会重置所有节点上的 K8S 组件,包括删除/etc/kubernetes、/var/lib/kubelet以及 Docker 容器。注意它也会删除 Calico 的网络规则。
4.4 部署完成后的 Kubeconfig 与初始检查
部署完成后,控制节点上并不会自动生成 Kubeconfig,因为 Kubespray 不要求控制节点加入集群。需要从任意一个 Master 节点拷贝 Kubeconfig:
scp root@192.168.1.21:/etc/kubernetes/admin.conf ./kubeconfig kubectl --kubeconfig=./kubeconfig get nodes输出中三个master节点和一个node01的状态都应为Ready。如果某个节点长时间NotReady,有一种情况是网络插件没有成功安装,查看该节点的容器组状态:
kubectl --kubeconfig=./kubeconfig get pods -n kube-system -o wide这里会看到calico-node-xxx的 Pod 状态,镜像拉取失败、配置错误都会导致CrashLoopBackOff。国内环境最常见的原因是镜像版本不对或节点 CPU 架构不一致,比如 x86 架构和 ARM 架构混用导致的镜像兼容问题。
5. 避坑手册:Kubespray + Docker + 国内网络场景下的五大翻车现场
5.1 docker load 成功但镜像是<none>:<none>
现象:镜像归档文件通过docker load -i导入后,docker images发现 Repository 和 Tag 都是<none>,后续 Playbook 报错Unable to find image。
原因:归档文件中确实包含完整镜像,但镜像元数据中的 Tag 信息未正确继承,常见于 Docker 版本过旧或归档文件来自docker save后被压缩工具处理过。
解决:不依赖docker load自动恢复 Tag,手动给镜像添加标签。在offline_imgs/下一般有images_list.txt,每一行格式为repository:tag,按清单重新打标签:
docker images --format "{{.ID}} {{.Repository}}:{{.Tag}}" | grep '<none>' docker tag <image_id> registry.k8s.io/kube-apiserver:v1.28.55.2 SSH 无法连接:permission denied 与Host key verification failed
现象:执行ansible -i inventory.ini all -m ping时,所有节点返回permission denied,或单节点出现Host key verification failed。
原因:目标机器的 SSH 配置允许密钥登录,但 Ansible 使用的用户不对;或者控制机的~/.ssh/known_hosts中有冲突记录;或者目标机器的sshd_config设置了PasswordAuthentication no,而资源包默认使用密码方式。
解决:修改inventory_ha.ini中添加一行指定 SSH 用户:
[all:vars] ansible_user=root ansible_ssh_private_key_file=/root/.ssh/id_rsa同时确认目标机器的/root/.ssh/authorized_keys包含控制机的公钥。若仍失败,启用ssh-copy-id重新分发公钥,然后清理控制机的 known_hosts:
ssh-keygen -R 192.168.1.21这条命令移除 known_hosts 中的旧记录,重新 SSH 连接时会自动接受新指纹。
5.3 部署卡在TASK [kubernetes/kubeadm : Join to cluster]
现象:Ansible 输出停在若干节点的Join to cluster任务,长时间无变化,最后以失败结束,日志提示节点无法连接到 apiserver 的 VIP 地址。
原因:VIP 地址在目标机器上无法访问。排查思路是先检查 VIP 是否被任一 Master 节点持有:
ip addr show | grep 192.168.1.20如果所有 Master 都查不到 VIP,说明 keepalived 没有启动,或配置文件选择的网卡名不对。打开/etc/keepalived/keepalived.conf检查interface参数与实际网卡名是否匹配。
解决:在 Inventory 的[all:vars]中明确指定loadbalancer_apiserver.keepalived_interface为实际网卡名。
5.4 Kubelet 无法启动:Container runtime is down
现象:Master 节点上执行systemctl status kubelet显示 active,但通过kubectl get nodes查看节点状态是NotReady,查看日志发现大量
PLEG is not healthy或
Container runtime network not ready原因:常见的两类,一个是 Docker 与 containerd 版本不匹配导致运行时通信异常,另一个是 Calico 依赖的CNI配置未生成完毕。
解决:检查节点上 Docker 的 storage driver 是否为overlay2:
docker info --format '{{.Driver}}'如果是aufs,Kubelet 与 Docker 的配合会有兼容问题,在/etc/docker/daemon.json中追加
{ "storage-driver": "overlay2" }然后重启 Docker。这一步解决之后再检查 CNI 目录:
ls /etc/cni/net.d/空目录意味着 Kubelet 不知道网络插件是谁,需要等待 Calico 组件完成写入,此时在 Master 节点上重启 kubelet 即可。
5.5 重启宿主机后集群无法自动恢复
现象:所有节点断电重启,Docker 正常启动,但kubectl get nodes显示一堆NotReady。
原因:Docker 的服务是开机自启的,但 Kubelet 和 keepalived 可能没有启用 systemd 开机自启。Kubespray 在正常情况下会设置 kubelet 自启,但某些自定义镜像包安装过程中可能覆盖了 systemd unit 文件。
解决:在三台 Master 和 Node 节点上依次执行:
systemctl enable kubelet keepalived systemctl restart kubelet keepalived恢复顺序也有讲究:先启动 etcd(通过 Docker 容器),再启动 kubelet,最后检查 VIP 是否重新落在一个 Master 上。很多时候集群不能自动恢复,是因为 Docker 先起来了,但 keepalived 没有启动导致 VIP 不在任何节点上。
6. 最后验证高可用:故障注入、滚动升级与生产习惯养成
部署完成不等于高可用生效,Kubespray 的 Playbook 执行成功只表示集群组件安装到位。验证高可用需要做一次故障注入:把持有 VIP 的 Master 节点上的 Kubelet 和 Keepalived 全部停掉,观察集群是否在 30 秒内自动切换到另一台 Master。
kubectl --kubeconfig=./kubeconfig get nodes -o wide记录当前持有 VIP 的节点,然后 SSH 登录那台机器:
systemctl stop kubelet systemctl stop keepalived回到控制节点,执行:
for i in $(seq 1 10); do ip addr show | grep 192.168.1.20 && break sleep 3 done正常情况下,VIP 会在几秒内漂移到另一台 Master 上。再用 Kubeconfig 调用 APIServer:
kubectl --kubeconfig=./kubeconfig get pods -A如果 Pod 列表正常返回,说明控制平面的高可用生效。
接着验证 etcd 的健康状态,在三台 Master 中的任意一台执行:
docker exec -e ETCDCTL_API=3 -it etcd-$(hostname) etcdctl \ --endpoints=https://127.0.0.1:2379 \ --cacert=/etc/ssl/etcd/ssl/ca.pem \ --cert=/etc/ssl/etcd/ssl/member-$(hostname).pem \ --key=/etc/ssl/etcd/ssl/member-$(hostname)-key.pem \ endpoint health三台节点都应返回healthy。这里最容易被误判的情况是:etcd 容器状态显示 running,但某个节点之间的 peer 通信已经中断,症状是endpoint health输出中某一台unhealthy。原因一般是对应节点的 2380 端口被防火墙阻断,或者etcd_member_name配置成了重复值。修复方法是在 Inventory 中补齐节点名称的全局唯一性,重跑cluster.yml的 etcd 相关 Role。
验证完成后,需要养成一个习惯:每次部署或变更集群前,先检查ansible --version、docker version和kubectl version三者的版本兼容性。Kubespray 对不同版本的组合有明确的 matrix 支持表,跳过这一步只会在部署到一半时暴露出莫名其妙的问题。从那以后我每次调整集群前都会强制走一遍版本核对流程,把资源包里的镜像清单重新拉出来过一遍,确认镜像 Tag 与 Kubernetes 版本对应,再跑 Playbook。这套流程帮我排查掉的翻车现场比我记得的还要多,其中超过一半的问题不是配置写错,而是环境版本不一致。每个集群环境都不同,但这些版本基线核对的动作是通用的,希望帮到你。
本文还有配套的精品资源,点击获取