☰
虚拟机环境下的Kubernetes集群自动化部署脚本设计与实现
2026/10/8 20:39:53 网站建设 项目流程

最近在折腾一套能在虚拟机里快速搭建Kubernetes集群的自动化脚本。说白了就是想把之前手动敲命令搭k8s的流程,全部固化成一堆可重复执行的shell脚本。这个版本目前还处于“待验证”状态,我在本地VMware环境里跑过几轮,基本流程能走通,但离“拿去生产环境直接抄作业”还差得远。这篇东西就是把我这个版本的设计思路、脚本结构、执行细节、踩过的坑以及还没验证的风险点,一次性写清楚。

如果你也在做多节点k8s环境,比如为了练CKA、测业务高可用、跑大数据组件,或者单纯想把k8s安装过程彻底搞明白,这套脚本的思路可以直接参考。接下来我按照“为什么这么设计”和“具体怎么实现”两条线来讲。

1. 为什么选择虚拟机加自动化脚本这套组合

1.1 学习与测试场景下的核心痛点

很多人最初学k8s,都是在自己的电脑上折腾。要么是单机版minikube,要么是kind,这两种工具适合快速跑demo,但和真正的多节点集群还是隔了一层。比如你在生产环境会遇到节点NotReady、Pod跨节点通信异常、网络插件冲突、证书过期等问题,单机环境很难复现。

要模拟真实集群,最贴近的方式就是搞多台虚拟机。三台虚拟机,一台master两台worker,资源消耗不算大,16G内存的电脑完全能扛住。但问题是,每次从零搭建一个集群,手动敲命令至少需要40分钟到1小时,而且中间容易敲错。尤其是kubeadm init参数、join token、网络插件配置,稍有疏忽就得重来。最痛苦的还不是第一次搭,而是搭完测试完,过几天又需要推倒重建。这时候人肉重复劳动的代价就被无限放大了。

自动化脚本要解决的就是这个问题。把装环境、初始化、加节点、装网络插件这些重复操作全部脚本化,每次需要新集群时,跑一遍脚本,出去喝杯水回来集群就绪。我自己实测下来,从干净虚拟机到集群可用,脚本全流程大概12到15分钟,比手动操作快一大截。

1.2 虚拟机、云服务器、物理机的方案对比

在动手写脚本之前,我先把几种常见的集群搭建环境放一起做过对比,因为方案选型直接决定了后面脚本怎么写。

方案成本隔离性可重复性适用场景
物理机高低差生产环境、性能测试
云服务器中中较好生产、远程团队协作
虚拟机(NAT/仅主机)低高好学习、本地开发、CI测试

选择虚拟机有几个天然优势。第一,快照功能非常实用,搭建前打一个快照,脚本跑挂了恢复快照就能重新来,完全不消耗真实资源;第二,网络环境可控,虚拟机的网络模式可以自由切换,便于测试不同网络插件;第三,不依赖外部网络,哪怕没有公网,本地镜像缓存好之后照样能搭集群。

这里我选的是VMware Workstation,配合NAT网络模式。NAT的好处是虚拟机之间可以互相通信,同时通过宿主机访问外网拉镜像。测试下来,三台虚拟机用NAT模式,内部网络通信没问题,节点之间互Ping延迟也很低,完全满足集群要求。如果你用的是VirtualBox,思路也一样,只是网络配置的地方略有差异。

1.3 自动化的边界在哪里

写自动化脚本之前还有一件事很重要,就是先想清楚哪些该自动化、哪些不该自动化。不是所有步骤都适合写进脚本。

比如虚拟机本身的创建,我建议手动操作。因为每台机器的磁盘分区、内存分配、CPU核数都不一样,硬写成脚本反而容易失败。脚本应该从“操作系统安装完成之后”开始接管。再比如kubeadm init时的证书有效期问题,默认是一年,脚本可以自动处理,但一年以后证书续期是另一套流程,不适合塞进这个初始化脚本里。这个边界如果划不清楚,脚本会越来越臃肿,最后失去可维护性。

我的原则是:所有重复性强、出错率高、逻辑固定的步骤都自动化;所有依赖物理环境、需要人工决策的步骤保留手动。脚本是给人用的工具,不是代替人做所有决定。

2. 环境规划与版本选型:自动化之前先定规矩

2.1 虚拟机资源规划与网络拓扑

搭建这套集群之前,我先把虚拟机资源规划好了。我的配置是:三台虚拟机,一台作为master节点,两台作为worker节点。每台虚拟机的规格统一设置为4核CPU、4G内存、40G硬盘。

很多人在虚拟机里搭k8s会犯一个错误,就是内存给太小。k8s本身组件就多,如果还要在集群里跑业务Pod,2G内存的节点很容易出现系统OOM。我测试过,master节点建议至少3G内存,worker节点最少2G,但考虑到要跑实际负载,4G比较稳妥。系统选择的是Rocky Linux 9,因为它的包管理和RHEL完全兼容,稳定性也OK,而且内核版本默认就支持Kubernetes需要的网络模块。

网络拓扑的话,三台虚拟机全部接入VMware的NAT网卡,网段192.168.88.0/24。具体IP规划如下:

节点角色主机名IP地址配置
Masterk8s-master192.168.88.104C4G
Worker1k8s-worker1192.168.88.114C4G
Worker2k8s-worker2192.168.88.124C4G

主机名和IP的规划要放在脚本执行之前确定好,因为后面所有配置都会引用这些值。如果你照抄这个方案,主机名和IP可以根据自己环境改,但一定要提前定死,不要在配置做到一半时再改,那会非常痛苦。

2.2 软件版本选型与原因

版本选型这里我吃过不少亏,所以单独拿出来说。k8s的各个组件对版本非常敏感,kubeadm、kubelet、kubectl三者的版本必须保持一致,控制面的etcd和API Server版本由kubeadm统一管理,不用单独指定,但容器运行时和CNI插件版本也需要和k8s版本兼容。

我当前脚本锁定的版本组合如下,这个组合我实测可以正常跑通:

组件版本说明
Kubernetes1.28.x当前较稳定的版本线
containerd1.7.x容器运行时,替代Docker
runc1.1.xcontainerd底层运行器
CNI插件Calico 3.27网络方案,支持NetworkPolicy
kubeadm/kubelet/kubectl与K8s同版本版本一致性强制要求

为什么用containerd而不是Docker?这里也解释下。Kubernetes在1.24版本之后移除了对Docker的dockershim支持,虽然可以通过cri-dockerd桥接继续用Docker,但多一层桥接就多一层故障点。用containerd作为运行时是官方推荐路径,配置好sandbox_image之后,拉取pause镜像、cgroup驱动都能正常处理。至于网络插件用Calico,是因为它对跨节点通信的性能表现比flannel好,而且支持网络策略,后续如果要练服务间访问控制,功能更全面。

2.3 三台虚拟机的系统预配置

虚拟机建好、系统装完之后,第一件事不是跑脚本,而是先把三台虚拟机的系统基础配置统一。需要确认以下几点都满足,脚本才能顺利往下走。

首先,确保三台机器能够用SSH互通。脚本执行过程中会涉及master节点向worker节点推送join命令,如果SSH不通,后续流程就断了。其次,确认每台机器的时间是同步的,k8s的证书机制对时间偏差非常敏感,时钟不同步会导致部分组件出现证书校验失败,这个我在之前的项目里真实遇到过。如果服务器无法访问公网NTP服务,至少要保证三台机器的时间误差在1分钟以内。

系统本身的防火墙、SELinux、swap这些,正常逻辑都是关闭。这些配置在系统安装时手动处理就行,因为每台机器状态不一样,写进脚本反而要加各种判断,麻烦且没必要。脚本从安装containerd开始接管。

3. 脚本整体设计与模块拆解

3.1 脚本架构:一份配置,多份执行

脚本我按职责拆成了几个文件,核心设计理念是“配置与执行分离”。这样每个人拿到脚本后,只需要修改配置文件里的IP、主机名、版本号,不需要去翻一坨执行命令找哪里需要改。

k8s-auto-install/ ├── config.env # 全局变量配置,改这里就行 ├── 01-install-base.sh # 所有节点执行:装containerd、kubeadm、kubelet、kubectl ├── 02-init-master.sh # 只在master上执行:kubeadm init、装Calico、获取join命令 ├── 03-join-worker.sh # 只在worker上执行:加入集群 ├── 04-verify-cluster.sh # 任何节点执行:验证集群状态 └── lib/log.sh # 日志输出函数库

这里最关键的意识是模块化。以前我写安装脚本时习惯把一堆命令堆在一个install.sh里,一旦哪一步失败,整个脚本终止,想排查问题还得用bash -x去跟踪每一行,非常痛苦。拆成模块后,每个阶段独立运行,哪一步出错就到对应文件里排查,逻辑清晰很多。

每个脚本我都尽量做成“幂等”的,意思是重复执行不会产生副作用。比如用rpm -qa判断某个包是否已经安装,如果已经装了直接跳过;用kubectl get nodes判断节点是否已经在集群里,如果已经在就直接输出集群状态。这个设计对于脚本调试特别重要,因为第一次跑失败了,修复问题后重新执行,它能从上一步失败的位置继续往下走,而不是从头再来。

3.2 全局配置与环境变量的集中管理

config.env是整个脚本的入口,所有需要修改的参数都集中在这里。我写这份配置的初衷是:我折腾的这套脚本版本,很可能你拿过去以后集群节点数量、IP段、版本号都不一样,如果每个脚本文件里面都分散着各自写死的变量,那改起来非常容易遗漏,最后就会导致集群搭建失败。

# config.env 核心变量 K8S_VERSION="1.28.2" CONTAINERD_VERSION="1.7.19" CALICO_VERSION="v3.27.0" MASTER_IP="192.168.88.10" NETWORK_CIDR="10.244.0.0/16" SERVICE_CIDR="10.96.0.0/16" MASTER_HOSTNAME="k8s-master" WORKER_HOSTNAMES=("k8s-worker1" "k8s-worker2") WORKER_IPS=("192.168.88.11" "192.168.88.12") IMAGE_REPOSITORY="registry.aliyuncs.com/google_containers"

这里有个经验值得提一下:国内环境拉k8s组件镜像经常超时,所以我在配置里把kubeadm和kubelet的镜像仓库,单独指定为国内可访问的镜像地址。如果是海外环境,把IMAGE_REPOSITORY清空或者改成官方地址即可。这个看似很小的设计,实际上决定了脚本在什么样的网络环境下能跑通。脚本内部所有kubeadm init、kubeadm join的--image-repository参数,都统一引用这个变量,改一处即可全局生效。

3.3 日志输出、等待与重试机制

自动化脚本最怕的就是“静默失败”。命令执行完了你不知道是成功还是失败,等到最后发现集群没起来,再倒回去查是哪一步出的问题,太浪费时间了。所以我写了一个简单的日志机制,脚本执行时的每一步操作都会输出当前阶段、机器名称、耗时,以及命令执行结果。

# lib/log.sh 日志输出函数 log_info() { echo -e "\033[32m[INFO]\033[0m $(date '+%Y-%m-%d %H:%M:%S') - $*" } log_error() { echo -e "\033[31m[ERROR]\033[0m $(date '+%Y-%m-%d %H:%M:%S') - $*" } retry_cmd() { local retry_times=$1 local cmd="${@:2}" for ((i=1; i<=retry_times; i++)); do if eval "${cmd}"; then log_info "Command succeeded: ${cmd}" return 0 else log_error "Command failed (attempt ${i}/${retry_times}): ${cmd}" sleep 5 fi done return 1 }

日志输出的作用不只是好看。在实际排障过程中,如果脚本在master初始化阶段失败,日志会明确告诉你是在拉取镜像失败、生成证书失败,还是启动kubelet失败。这三个问题对应完全不同的排查路径。重试机制也很重要,尤其是网络拉镜像这种场景,首次失败往往只是网络波动或者镜像仓库限流,等几秒重试多半就成功了。

另外还有一个等待函数,我专门用来等Pod变Ready。k8s集群初始化后,核心组件的Pod需要拉镜像、启动容器、探活通过,整个过程需要1到3分钟。如果脚本里不等待就直接执行下一步,比如直接获取节点状态,大概率会看到一堆Pending或ContainerCreating的Pod,第一反应就会以为是安装有问题,其实只是还没启动完。

4. 实操过程:从干净虚拟机到可用集群

4.1 基础系统预处理

脚本实操部分,我按真实的执行顺序来写。先说第一步:基础系统预处理。这一个阶段本来应该做成脚本,但考虑到每台虚拟机的状态不一样,比如有的可能刚刚克隆出来、带有相同的主机名和UUID,有的可能来自不同模板,强行统一处理容易出问题,所以这部分我只做半自动化,用脚本辅助+手动确认。

需要处理的内容包括:设置主机名、关闭防火墙和SELinux、关闭swap、加载内核模块设置sysctl参数。其中有两个点新手容易忽略。

第一个是hostnamectl set-hostname设置主机名之后,一定要在/etc/hosts里加上master和worker的IP解析。否则kubeadm初始化时,节点之间通过主机名解析会失败,出现failed to resolve hostname之类的错误。

第二个是swapoff -a只是临时关闭swap,重启后会恢复。要让swap永久关闭,还需要注释掉/etc/fstab里swap相关的挂载行。我见过不少人在虚拟机里装完k8s,重启之后节点状态就变成NotReady了,查了一圈发现是swap又自动挂载上了。

除了以上两件事,还有内核模块。k8s网络组件和iptables规则依赖br_netfilter模块,需要手动加载并设置net.bridge.bridge-nf-call-iptables=1。这些配置通过/etc/modules-load.d/k8s.conf和/etc/sysctl.d/k8s.conf两个文件写入,比较规范,重启后也依然生效。

4.2 containerd与k8s核心组件安装

基础系统准备好之后,就可以执行第一个脚本01-install-base.sh了。这个脚本在每台虚拟机上都执行,作用是安装容器运行时和k8s的核心组件。

安装containerd时,最重要的配置是/etc/containerd/config.toml。我这里直接说明关键的几个配置项,因为不少人装完containerd后,kubelet一直报failed to connect to containerd,就是因为配置不对。

首先,要确保sandbox_image被设置成可访问的pause镜像地址。默认配置里这个地址是registry.k8s.io/pause:3.6,如果网络访问不了,kubelet在初始化Pod沙箱时会失败。其次,SystemdCgroup必须设为true。因为当前主流的Linux发行版都使用systemd作为init系统,kubelet和containerd的cgroup驱动如果不一致,运行Pod时会出现Failed to update cgroup的报错。

# 01-install-base.sh 关键步骤示意 install_containerd() { containerd config default | sudo tee /etc/containerd/config.toml sed -i "s#sandbox_image = .*#sandbox_image = \"${SANDBOX_IMAGE}\"#" /etc/containerd/config.toml sed -i "s#SystemdCgroup = false#SystemdCgroup = true#" /etc/containerd/config.toml systemctl enable --now containerd } install_kube_components() { cat <<EOF > /etc/yum.repos.d/kubernetes.repo [kubernetes] name=Kubernetes baseurl=https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/ enabled=1 gpgcheck=0 EOF dnf install -y kubeadm-${K8S_VERSION} kubelet-${K8S_VERSION} kubectl-${K8S_VERSION} systemctl enable --now kubelet }

安装k8s组件时需要注意,如果用国内镜像源,yum仓库指向mirrors.aliyun.com比指向官方packages.cloud.google.com更稳定。装完kubelet之后,暂时不需要启动它,或者启动之后它会报错,因为集群还没初始化,这种行为是正常的。kubelet会一直尝试连接API Server并处于CrashLoopBackOff,这不是故障,等master初始化后它会自己恢复正常。

4.3 Master节点初始化和CNI网络部署

基础组件在三台机器上都装完之后,接下来只在master节点上执行02-init-master.sh。我在这个脚本里写好了kubeadm init的命令。

# 02-init-master.sh 关键命令 kubeadm init \ --apiserver-advertise-address="${MASTER_IP}" \ --image-repository="${IMAGE_REPOSITORY}" \ --kubernetes-version="${K8S_VERSION}" \ --service-cidr="${SERVICE_CIDR}" \ --pod-network-cidr="${NETWORK_CIDR}" mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config

这里面的参数解释一下。--apiserver-advertise-address必须指定master节点的IP,如果不指定,kubeadm会用默认路由对应的IP,在某些多网卡虚拟机环境下可能会选错网卡。--pod-network-cidr和Calico的网络规划要对应上。如果Calico的IP池和这里不一致,跨节点Pod就会出现互相访问不了的现象。

kubeadm init成功之后,脚本会自动部署Calico网络插件。Calico部署用官方yaml文件,可以直接下载或放在脚本同级目录里。我在脚本中会先判断有没有这个yaml,没有的话自动下载。Calico的Pod启动需要拉取镜像并等待跨节点隧道建立,这个过程通常需要1到2分钟,脚本里的等待函数就是为这个阶段准备的。

等Calico的Pod全部Running之后,master节点本身的状态应该已经是Ready了。这时候脚本会把worker节点加入集群用的join命令保存到/root/join-command.txt。这个join命令是后续worker加入的关键凭证,包括token和CA证书哈希。如果脚本执行到这里,说明master侧已经没问题了。

4.4 Worker节点加入与集群验证

worker节点加入集群,理论上只需要在每台worker上执行从master拿到的join命令即可。但在自动化脚本里,我采用了一个更省事的方案,让master节点通过SSH直接到worker节点上执行join。为了这个步骤,master和worker之间需要提前配置免密SSH登录。

# 02-init-master.sh 新增逻辑 for i in "${!WORKER_IPS[@]}"; do HOSTNAME=${WORKER_HOSTNAMES[$i]} IP=${WORKER_IPS[$i]} JOIN_COMMAND=$(cat /root/join-command.txt) ssh ${HOSTNAME} "${JOIN_COMMAND} --node-name ${HOSTNAME}" done

上面这种方式的好处是不需要刻意在worker上保留token信息,join命令直接从master推送过去。但有个问题需要注意,kubeadm生成的token默认有效时间是24小时,如果你的自动化脚本是分多次执行的,比如今天初始化master,明天再让worker加入,这个token有可能已经过期了。我脚本里在获取join命令时会替换成24小时后才过期的长有效期token,或者直接在脚本中重新生成token。

所有worker加入完成后,执行04-verify-cluster.sh做最终验证。脚本会检查节点状态、检查集群组件状态、检查核心Pod是否全部Running。

# 04-verify-cluster.sh 核心逻辑 kubectl get nodes kubectl get pods -A kubectl get cs # 输出结果 # NAME STATUS ROLES AGE VERSION # k8s-master Ready control-plane 8m v1.28.2 # k8s-worker1 Ready <none> 2m v1.28.2 # k8s-worker2 Ready <none> 2m v1.28.2

如果三台节点都显示Ready,集群搭建就是成功的。为了让验证更充分,我还会在脚本末尾创建一个简单的nginx部署,验证Pod调度和跨节点访问。比如把nginx副本数设为2,观察两个副本是否被调度到不同节点上,再用另一个Pod去curl nginx的ClusterIP。这一步能同时验证调度器、网络插件和服务发现是否都正常工作。

5. 常见问题与排查技巧实录

5.1 典型报错速查表

自动化脚本跑的过程中,我积累了很多报错排查经验。下面这些是这一版本里出现频率最高的问题,做成速查表,方便你直接对照。

报错信息可能原因解决方式
[ERROR FileContent--proc-sys-net-bridge-bridge-nf-call-iptables]br_netfilter模块未加载执行modprobe br_netfilter并写入/etc/modules-load.d/k8s.conf
[ERROR Swap]: running with swap on is not supportedswap未永久关闭注释/etc/fstab中swap行并执行swapoff -a
connection refusedto 192.168.88.10:6443API Server未启动或master IP错误检查kubelet状态,确认apiserver-advertise-address,排查防火墙
cni plugin not initialized网络插件未安装或等待中kubectl applyCalico,等待Pod Running
NodeNotReady网络插件Pod异常或kubelet故障kubectl get pods -n kube-system,查看日志定位
ContainerCreating一直不Ready拉取镜像失败检查events输出,确认镜像地址可访问

排障的思路有一些通用规律。遇到问题先看事件,kubectl describe pod能告诉你这个Pod卡在什么阶段,是拉不到镜像、卷挂载失败还是探活失败。其次是看日志,kubelet的日志在journalctl -u kubelet -f,containerd的日志在journalctl -u containerd -f。不要一上来就怀疑是脚本问题,先看基础组件状态,逐步排查才高效。

5.2 重启后的状态验证

这一部分是我在反复测试中得出的比较有价值的经验。k8s集群搭建完以后,虚拟机如果重启了,集群状态能不能自动恢复?这个问题很多人容易忽略,因为搭建成功的那一刻看起来一切正常,实际上很多服务和容器都是靠systemd和kubelet自愈的,能不能恢复取决于依赖关系。

我专门做了几组重启测试,这里记录一下结果。

第一组是只重启worker节点。重启后kubelet会通过systemd自动启动,然后重新连接master节点,节点状态会在1到2分钟内从NotReady变成Ready。节点上的Pod会重新调度,如果负载允许,Pod会在原节点恢复运行。测试结果是可以自愈的,只要kubelet服务正常enable。

第二组是重启master节点。master重启后的情况会稍微复杂一些,因为etcd、API Server、controller-manager、scheduler这些组件都是作为静态Pod运行在kubelet下的,kubelet起来后会把这些静态Pod重新拉起来。理论上集群会自动恢复,但如果是单master架构,恢复期间整个集群的调度和管理能力都是中断的。我这套环境实测下来,master重启后5分钟左右能恢复到Ready状态。

第三组是master和worker全部断电重启。这种情况下如果系统里配置了/etc/fstab里面swap又自动挂载,或者某个服务没有设置开机自启,就可能导致部分组件起不来。我建议全部重启之后跑一遍kubectl get nodes确认状态,如果某个节点NotReady,优先检查交换分区、containerd和kubelet服务状态。这三家没问题,节点一般都能恢复。

5.3 这一版本还没验证的“雷区”

标题里写了“待验证版本”,就必须把哪些验证过、哪些没验证过说清楚。这套脚本在VMware Workstation环境、Rocky Linux 9下通过了多轮测试,但有以下场景我是没有完全验证的。

第一,CentOS 7或者Ubuntu系统没有兼容性测试。脚本里面用的是dnf/yum包管理,Ubuntu需要用apt,网络配置和systemd配置方式也不完全一样。思路可以复用,但脚本不能直接照搬。

第二,高可用master架构没有测试。目前这个脚本是单master节点方案,等这个版本稳定之后,可以再写一个keepalived + haproxy的高可用扩展,再加一个master节点作为冷备。生产环境至少两个控制面节点才是底线。

第三,多网卡环境的验证不充分。如果一台虚拟机有多个网卡,比如既有NAT又有桥接,kubeadm在选IP时可能会出问题。虽然配置了--apiserver-advertise-address可以规避一部分风险,但CNI插件对多网卡的处理我还需要再深入测试。

6. 这套脚本后续的优化方向

脚本的下一版我计划做三件事。第一件是把Base操作也脚本化,从装Docker、配镜像加速到内核模块参数调整,用条件判断去适配CentOS和Ubuntu两种系统。第二件是加入失败处理机制,比如初始化失败后自动保存日志并尝试修复,减少人工介入。第三件是支持配置文件导入,比如在一个yaml文件里定义好节点IP、角色、版本号,脚本直接读取生成集群,这样复用性就高一个层次。

从我实际折腾的角度看,自动化脚本最大的价值不是省时间,而是把集群搭建这个操作从“玄学”变成了“工程”。手动敲命令时,你可能会因为某一行的版本号写错就浪费一个小时排查,而脚本会把变量集中管理、按序执行、结果验证,整个安装过程变得可预期、可重复、可追踪。对于刚入门的人,先把手动安装流程走通一遍,再来改这套脚本是收益最高的学习路径;对于要频繁重建环境的人,记住核心思路比直接复制代码更重要。

这个版本我发出来就是给有同样需求的人做一个参考,当前环境测下来稳定可用。如果你也在虚拟机里做过k8s集群自动化,欢迎把你踩过的坑和验证过的新场景分享出来,我可以把这些经验合入下一版脚本。折腾自动化这事,最大的乐趣就是一个人踩坑,一群人避坑。

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

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

立即咨询