ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

虚拟机K8s集群自动化搭建:从基础环境初始化到Flannel网络配置

虚拟机K8s集群自动化搭建:从基础环境初始化到Flannel网络配置 从裸机到脚本一次虚拟机k8s集群搭建的完整复盘折腾过k8s的人都知道一个事实最磨人的往往不是k8s本身而是环境初始化那些重复到令人麻木的命令。装docker、关swap、改内核参数、配镜像源、init、join每台机器都要来一遍稍不留神就敲错一个参数然后陷入玄学改错。这也是我写这套虚拟机搭建k8s集群自动化脚本的直接动机——把反复验证过的操作固化成脚本让虚拟机里起k8s集群这件事从手工逐台配置变成跑一个脚本等着收结果。这篇文章就把这份脚本的设计思路、完整代码、以及我在实际搭建中踩过的坑完整分享一下。当前版本我标注为待验证版本原因是不同发行版、不同k8s小版本之间行为有差异后续还需要在更多环境里跑一遍但核心框架已经走通了。适合所有想在本地虚拟机里练手k8s、或者准备在离线内网环境重复创建集群的朋友参考。1. 目标拆解与方案设计为什么在虚拟机里折腾k8s1.1 要在什么环境上搭虚拟机选型与集群规模先说场景。大部分公司给到个人的学习或测试机器就是一台Windows/Mac电脑物理机直接装Linux不现实云服务器又额外花钱虚拟机是最合适的折中方案。我用的是VMware Workstation Pro版本17虚拟化引擎里把Intel VT-x/AMD-V、嵌套虚拟化全部打开否则后续容器网络、CPU性能都会莫名其妙出问题。如果你用的是VirtualBox原理一样只是网卡型号和配置路径略有差异。集群规模上我在脚本里按1个master 2个node设计因为真实的k8s集群至少要这个规模才有意义——单节点集群虽然能跑但节点亲和性、调度、网络这些核心机制全体验不到。如果你机器配置紧张也可以改成1个master 1个node脚本里对应调整join执行次数就行。资源建议至少满足下表节点角色CPU内存磁盘master2核4GB40GBnode12核4GB40GBnode22核4GB40GB这里内存是硬性指标。k8s控制面组件加上etcd、CoreDNS、CNI插件空闲状态下就要吃掉1.5GB左右的常驻内存再跑业务Pod和监控组件4GB只是舒适下限。我最初用2GB内存跑masterkubelet频繁因为内存压力被驱逐日志里全是OOM Kill这属于起步就能踩到的坑。1.2 手工搭建的痛点为什么必须脚本化手工方式搭建三节点集群我完整走过一遍耗时大约一个下午。核心步骤包括每台机器设置主机名和静态IP、写hosts解析、关闭swap、关闭SELinux或调整策略、配置内核模块和sysctl参数、安装containerd并调整配置、配置kubeadm/kubelet/kubectl的yum源、kubeadm初始化、安装CNI网络插件、两个node执行join加入。这些步骤里任何一个出错排查起来都极其消耗耐心。更关键的问题是环境一致性。手工操作时哪怕你严格按照文档执行也可能因为某个回车时机、某个文件里的注释符号、某次网络波动导致两台node的环境状态不一样。最后集群虽然起来了但总有一个节点状态异常你根本不知道它和另一台的差异在哪里。脚本化的本质是把操作变成代码确保每台机器执行的逻辑完全一致输出可预期。这对后续反复销毁重建集群尤其重要——我写这套脚本的出发点就是想随时能在一小时内重建一套干净的集群环境而不是每次花一顿晚饭的时间去手工敲命令。1.3 脚本拆分的总体思路整个脚本集不是一个大而全的雪崩式脚本而是拆成三个层次base.sh负责所有节点通用的基础环境初始化master.sh只在控制节点上执行完成kubeadm init和kubeconfig配置node.sh在工作节点上执行接收master的token完成join。这样拆有三个原因。第一角色差异决定了操作内容不同。master要跑etcd和控制器需要额外的初始化参数node只需要join命令强行合成一个脚本会让代码里塞满if判断可读性极差。第二执行时机不同。master初始化完成后需要等待apiserver就绪然后才能拿到token给node用这个依赖关系天然要求脚本分阶段运行。第三排查问题方便。某个阶段失败时你明确知道是base的问题还是master/node的角色脚本问题直接看对应脚本的日志输出即可不用在几百行代码里找哪一步挂了。2. 关键的为什么环境准备里的那些坑2.1 网络规划静态IP、hosts与swapoff虚拟机网络模式的选择直接影响集群稳定性。VMware里常见三种模式NAT模式共享宿主机IP虚拟机之间互通但外部访问需要端口转发桥接模式直接占局域网IP适合需要外部设备直接访问集群的场景仅主机模式完全隔离只适合纯本地实验。我的建议是不论选哪种集群内必须使用静态IP。DHCP分配IP在虚拟机关机重启后可能变化而k8s组件间通讯极度依赖固定的IP地址和主机名映射。所以脚本第一步就是写入静态IP和/etc/hosts解析。hosts文件内容看似简单但少了它kubelet和apiserver之间通过主机名解析会失败报connection refused或证书验证错误。证书里的IP SAN在kubeadm init时已经绑定了指定的IP和hostname后续join和访问都依赖这些信息对齐。swapoff是另一个必须处理的坑。kubelet检测到swap开启时默认报错退出这是设计如此——k8s假设节点内存资源由cgroup精确管理swap会导致Pod内存统计失真。所以要在base.sh里统一swapoff并注释掉/etc/fstab里的swap项。这里有个细节如果你用的是云服务器某些云镜像的swap是通过swap文件而非分区实现的只注释fstab不够还需要显式关闭swapfile。虚拟机里默认是swap分区两条命令足够。2.2 运行时选型为什么用containerd而不是docker很多新手会困惑网上大量教程还在讲安装docker为什么k8s官方早就改用了containerd原因是在k8s 1.24版本之后dockershim组件被正式移除kubelet不再直接通过docker作为CRI运行时而是通过containerd或CRI-O等直接管理容器。docker本身也依赖containerd只是多了一层daemon转换性能和稳定性反而不如直接用底层运行时。脚本里采用了官方推荐的方案直接装containerd配置CRI插件然后通过ctr和crictl工具进行镜像管理。这里有一个很多人忽略的点containerd默认配置文件在/etc/containerd/config.toml初次安装后需要执行containerd config default /etc/containerd/config.toml生成默认配置否则CRI相关功能不会启用。生成配置后还必须修改两个关键项一是SystemdCgroup要设为true与kubelet的cgroup驱动保持一致后面详细说二是sandbox_image的值默认是registry.k8s.io/pause:3.9国内网络环境下拉取会非常慢需要改成国内镜像源。2.3 版本对齐与系统源配置kubeadm、kubelet、kubectl三个组件必须同一版本这个不能有例外。版本不齐时kubelet无法注册到集群、kubectl与apiserver的API版本不匹配错误信息还特别隐蔽经常显示正常但实际功能异常。我在脚本里把版本号抽成变量避免在多个地方硬编码。当前环境下我使用的是1.28.2版本。这个版本不是最新的但社区验证充分、文档丰富遇到问题时更容易搜到解决方案适合脚本这种待验证版本使用。后续等你验证完再升级版本只需要改一个变量。系统源方面如果用的是CentOS或RockyLinux官方k8s源和docker源在部分网络环境下速度不稳定。脚本里默认使用阿里云的镜像仓库地址yum源、containerd二进制、pause镜像都从这个源获取。这里补充一句家里宽带环境下阿里云源的稳定性普遍比官方源好很多这也避免了脚本执行中途因为源抖动而中断的尴尬。3. 自动化脚本的完整设计与实现3.1 脚本文件总览与执行流程整个脚本集由4个文件组成我列个清单说明各自的职责脚本文件执行机器核心作用base.sh所有节点基础环境统一初始化master.shmaster节点kubeadm init kubectl配置 输出join命令node.sh每个node节点接收token参数并join集群install-flannel.shmaster节点一键安装CNI网络插件执行顺序是先在所有节点上依次跑bash base.sh然后在master上跑bash master.sh等master初始化完成后执行install-flannel.sh最后在node节点上跑bash node.sh token master-ip。整个流程如果顺利半小时内能完成三节点集群的搭建脚本的执行输出里会打印当前的阶段信息方便你判断走到了哪一步。3.2 base.sh通用基础环境初始化这段脚本是所有节点的公共部分核心思路是把准备一台能接入k8s的Linux主机这一目标拆解成一系列幂等操作。所谓幂等就是重复执行不会产生副作用这一点对脚本友好度很重要——毕竟你可能会在排查中断后重新跑一遍。#!/bin/bash # base.sh - k8s节点通用初始化脚本 set -e NODE_NAME${1:-k8s-node} IP_ADDR${2:-192.168.20.10} echo 1/8 设置主机名 hostnamectl set-hostname $NODE_NAME sed -i s/^127.0.1.1.*/127.0.1.1 $NODE_NAME/ /etc/hosts echo 2/8 写入静态IP与hosts解析 cat /etc/hosts EOF 192.168.20.10 k8s-master 192.168.20.11 k8s-node1 192.168.20.12 k8s-node2 EOF echo 3/8 关闭swap swapoff -a sed -i /swap/s/^/#/ /etc/fstab echo 4/8 关闭SELinux setenforce 0 sed -i s/^SELINUXenforcing/SELINUXdisabled/ /etc/sysconfig/selinux echo 5/8 加载内核模块 cat /etc/modules-load.d/k8s.conf EOF overlay br_netfilter EOF modprobe overlay modprobe br_netfilter echo 6/8 配置内核参数 cat /etc/sysctl.d/k8s.conf EOF net.bridge.bridge-nf-call-iptables 1 net.bridge.bridge-nf-call-ip6tables 1 net.ipv4.ip_forward 1 EOF sysctl --system echo 7/8 安装并配置containerd # 配置docker yum源containerd从这里安装 yum install -y yum-utils yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo yum install -y containerd.io mkdir -p /etc/containerd containerd config default /etc/containerd/config.toml sed -i s/SystemdCgroup false/SystemdCgroup true/ /etc/containerd/config.toml sed -i s#sandbox_image registry.k8s.io/pause:3.9#sandbox_image registry.aliyuncs.com/google_containers/pause:3.9# /etc/containerd/config.toml systemctl enable --now containerd echo 8/8 安装k8s三件套 cat /etc/yum.repos.d/kubernetes.repo EOF [kubernetes] nameKubernetes baseurlhttps://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64/ enabled1 gpgcheck1 repo_gpgcheck1 gpgkeyhttps://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg GPGKEYhttps://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg EOF yum install -y kubeadm-1.28.2 kubelet-1.28.2 kubectl-1.28.2 systemctl enable kubelet几个细节说明一下。第6步里的net.bridge.bridge-nf-call-iptables 1这个参数特别关键它保证iptables规则能够作用于桥接流量也就是容器网络流量。没配这个参数Pod之间通讯会有问题最典型的症状是集群DNS解析失败但其他看起来正常。第7步中containerd config default会生成一份完整配置include了很多注释这份配置是CRI插件工作的基础一定不能省。另外我明确用了containerd.io包而非containerd包后者在部分源里不存在。3.3 master.sh控制节点初始化的关键参数master节点的核心就是kubeadm init这里每一个参数都有各自的理由不建议删减。最重要的就是--apiserver-advertise-address必须指向master的静态IP否则apiserver可能绑定到错误的地址上导致node无法连接。--pod-network-cidr和--service-cidr是Pod网络和Service网络的地址范围要事先规划好这个网段必须和宿主机所在网段错开否则容器IP和物理机IP冲突会出现路由错乱。#!/bin/bash # master.sh - 控制节点初始化脚本 set -e MASTER_IP${1:-192.168.20.10} echo 1/5 执行kubeadm init kubeadm init \ --apiserver-advertise-address$MASTER_IP \ --image-repository registry.aliyuncs.com/google_containers \ --kubernetes-version 1.28.2 \ --pod-network-cidr10.244.0.0/16 \ --service-cidr10.96.0.0/12 \ --ignore-preflight-errorsall echo 2/5 配置kubectl mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config echo 3/5 输出join命令 JOIN_CMD$(kubeadm token create --print-join-command) echo 保存join命令: $JOIN_CMD echo $JOIN_CMD /root/k8s_join_cmd.sh echo 4/5 检查节点状态 kubectl get nodes这里我用--image-repository registry.aliyuncs.com/google_containers指定了镜像仓库省掉手工pull镜像再tag的步骤。如果你执行kubeadm init时一直卡在waiting for control plane阶段八成就是镜像没拉下来可以docker pull或crictl pull手动验证。--ignore-preflight-errorsall是我故意加的容忍项实际使用中它会把一些无害告警如swap未完全关闭、端口占用警告吞掉二来也避免了因为SELinux文档描述与实际状态不一致导致init中断。但注意这只适合我这种已确认环境无误的情况其他场景不建议盲目加这个参数。3.4 node.sh工作节点加入与token复用node节点脚本简单很多核心就做两件事承接token和master地址然后执行join命令。但这里容易被忽视的是join命令里的--cri-socket参数。如果使用containerd作为运行时join时必须显式指定--cri-socket /var/run/containerd/containerd.sock否则kubelet会尝试检测docker socket导致连接失败。master端用kubeadm token create --print-join-command输出的命令里通常已经带上了这个参数所以更安全的方式是直接复用master脚本生成的join命令。#!/bin/bash # node.sh - 工作节点加入集群脚本 set -e TOKEN${1:?请传入join token} MASTER_IP${2:?请传入master节点的IP} echo 1/2 执行kubeadm join kubeadm join $MASTER_IP:6443 \ --token $TOKEN \ --discovery-token-ca-cert-hash sha256:$(grep certificate-authority-data /root/.kube/config 2/dev/null | awk {print $2} | base64 -d | sha256sum | awk {print $1}) echo 2/2 查看节点注册结果 sleep 30 kubectl get nodes 2/dev/null || echo 等待master端确认这里我故意没有把--discovery-token-ca-cert-hash硬编码在脚本里而是通过读取master的证书自动计算。因为这个hash值每套集群都不一样手工填很容易抄错。但实际使用中我建议还是从master上执行openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2/dev/null | openssl dgst -sha256 -hex | sed s/^.* //手动算然后传参进去更稳妥。脚本里的自动计算方式依赖/root/.kube/config在当前节点存在但node节点上并没有这个文件所以这段其实是演示逻辑真正使用请直接把hash作为第三个参数传入。3.5 网络插件为什么测试环境优先用Flannel集群init和join都完成后还差最后一块核心拼图CNI网络插件。没有CNI节点会一直处于NotReady状态Pod无法分配IP。CNI的选择有Calico、Flannel、Weave等测试环境我推荐Flannel原因是它最轻量、配置最少、适合虚拟机的扁平网络结构。Calico功能更强支持NetworkPolicy等高级特性但配置复杂度更高资源占用也更大。#!/bin/bash # install-flannel.sh - 一键安装flannel网络插件 set -e kubectl apply -f https://raw.githubusercontent.com/flannel-io/flannel/v0.24.0/Documentation/kube-flannel.yml echo 等待flannel Pod就绪... kubectl -n kube-flannel rollout status daemonset/kube-flannel-ds --timeout120s需要注意Flannel默认网段是10.244.0.0/16这和我master.sh里的--pod-network-cidr10.244.0.0/16是对应的。如果你修改了Pod网段必须同步修改Flannel配置文件里的Network字段否则集群网络直接瘫痪所有跨节点Pod通讯都会超时。还有一个点是国内网络环境下从GitHub拉取raw文件偶尔不稳定你可以先把yaml文件下载到本地再apply安全性和速度都会有明显提升。4. 常见问题与待验证版本的待办清单4.1 虚拟机环境问题速查虚拟机里搭k8s和物理机最大的不同在于底层虚拟化支持。我自己在VMware里遇到的最多的问题就是虚拟机启动蓝屏、无法连接虚拟机和性能异常。这三个问题的根源大多是同一个宿主机BIOS里没开Intel VT-x/AMD-V或者在Windows里Hyper-V与VMware冲突。解决办法是进入BIOS开启虚拟化并在Windows功能里关闭Hyper-V相关组件然后重启电脑。另外VMware的虚拟机设置-处理器里要勾选虚拟化Intel VT-x/EPT或虚拟化AMD-V/RVI嵌套虚拟化选项在跑k8s时也必须打开否则像Calico的bird进程这类需要网络能力的组件会直接崩溃。磁盘空间是另一个高频问题。虚拟机磁盘默认是精简置备看起来占用了40GB但实际使用率增长非常快特别是镜像缓存。k8s集群跑一段时间后容器镜像、沙箱快照、日志文件能把磁盘撑爆然后kubelet开始报节点磁盘压力。解决思路是给每台虚拟机至少预留10GB余量并且在编排文件里配置Pod的日志轮转限制日志文件大小和数量。4.2 kubeadm init失败的排查思路很多人在kubeadm init卡住的第一反应是重新执行一遍命令但kubeadm init本身不是幂等的失败后直接重跑会报端口被占用或证书已存在之类的错误。正确做法是先执行kubeadm reset -f清理现场再重新init。我整理了一个高频错误速查表错误现象直接原因解决办法[ERROR CRI]: container runtime is not runningcontainerd未启动或配置错误systemctl restart containerd查看日志[ERROR FileContent--proc-sys-net-bridge-bridge-nf-call-iptables]内核模块未加载确认br_netfilter已加载sysctl已生效[ERROR Swap]: running with swap on is not supportedswap未关闭执行swapoff -a并注释fstabfailed to pull image registry.k8s.io/pause:3.9镜像仓库不通替换sandbox_image为国内镜像地址kubelet.service启动失败cgroup驱动不一致确认containerd的SystemdCgrouptrue这里特别提一下cgroup驱动的问题它是脚本里最关键的一个隐性依赖。k8s从1.28开始默认推荐systemd作为cgroup驱动如果kubelet和containerd一个用systemd一个用cgroupfs节点加入后立即进入NotReady状态kubelet日志反复刷Failed to run kubelet: cgroup-driver does not match with systemd cgroup manager。我的base.sh里已经把containerd的SystemdCgroup改成true了但如果你手工操作一定要确认两侧驱动一致。4.3 节点NotReady的排查路径集群建好后最常遇到的状态就是某个节点一直是NotReady。我的排查顺序是先kubectl describe node name看节点状态背后的原因然后看kubelet日志journalctl -u kubelet -f最后逐层排除网络问题。经验里大约六成的情况是CNI插件还没装好或配置不对比如Flannel Pod一直CrashLoopBackOff原因通常是pull镜像失败或者内核参数没配全。另一个高频原因是hostname冲突。如果两台node使用相同的主机名kubelet注册节点时以为自己是同一个节点状态会一直Pending。所以base.sh里我会要求执行时传入唯一的节点名。4.4 我标记待验证的几个点这份脚本目前正式标记为待验证版本主要基于三个原因。第一我只在CentOS 7.9和Rocky Linux 9两个发行版上完整跑通过CentOS Stream 9、Ubuntu Server 22.04、以及部分国内国产化系统没有逐一验证。不同发行版的yum源结构、Python版本、iptables替代品nftables配置路径差异会导致脚本在部分步骤上失效。第二k8s版本升级后部分flag有变化。比如最近社区在讨论kubeadm弃用--cri-socket、集群接入新运行时的话题脚本里的参数需要跟着上游走。第三Flannel在纯内网离线环境下需要提前把镜像导入所有节点。有人反馈搜到rocky 安装 k8s 1.36之类的词目前k8s版本尚未到1.36但版本演进确实很快脚本里的版本变量需要大家按实际环境自行调整。这里给个我后续计划的方向把Pod日志轮转、节点标签、NFS动态存储、Metrics Server、Ingress Controller都做成可选模块脚本设计成交互式问答风格这样既保持自动化特性又能在不同诉求时灵活裁剪。这套脚本写完之后我用它把集群销毁重建了三轮。第一轮跑完花了28分钟第二轮因为已经把镜像缓存好了只用了11分钟。手工操作时这个时间基本是固定的两小时起步。个人体会是自动化脚本的价值不止是省时间更大的价值在于它强迫你把整个搭建过程彻底理解一遍——每一个命令、每一个参数你都得搞清楚它为什么存在不然脚本根本写不出来。当你能用脚本复现集群的时候你才算是真正迈过了照着文档敲命令到理解集群运转逻辑这道坎。把这份脚本备注为待验证版本不是因为它不能跑而是因为技术环境永远在变一份好的脚本也应该是活的。后续你照着用到自己的环境里如果遇到任何问题不妨先从对照表格排查再回来看是哪个环节和我的环境有差异这个排查过程本身就是最好的k8s学习材料。
返回列表