☰
Kubernetes 1.31 集群初始化双脚本:Master与Worker一键部署指南
2026/10/16 7:36:58 网站建设 项目流程

说实话,第一次自己动手搭 Kubernetes 1.31 集群的时候,我还在老老实实一条条敲命令。一个 Master 加两个 Worker,敲完内核参数、换源、装容器运行时、再初始化、配置网络插件,一整套下来小半天就没了。更烦的是换一台机器、重装一次环境,同样的命令又得从头来过,错一个字符到后面排查起来就特别崩溃。后来我把这套流程沉淀成了两个脚本——一个初始化 Master,一个初始化 Worker,从此以后部署集群基本就是"执行脚本 + 喝杯水"的事。

如果你也准备上手 Kubernetes 1.31,或者不想把时间耗在重复的初始化命令上,这篇文章就是给你准备的。我会把双脚本的拆分逻辑、每一步命令背后的原因、踩过的坑,以及整个集群从执行到验证通过的过程全部拆开来讲。内容不追求高深,但求每一步都清楚,让你不仅能跑通,还能知道为什么这样写。

1. 为什么是双脚本:Master与Worker的初始化差异决定了拆分边界

先聊一个核心问题:既然是"快速部署",为什么不直接写一个大脚本在每台机器上跑一遍?很多人一开始都是这么想的,但实际跑过就会发现,Master 和 Worker 在集群里的职责完全不同,初始化的动作差异非常大,强行合并成一个脚本只会让逻辑越来越绕。

Master 节点要做的事是"生"——生成集群。它需要承载控制面组件,也就是 API Server、Controller Manager、Scheduler 和 etcd,这些组件通过kubeadm init一次性拉起。Master 脚本里必须包含集群初始化动作,执行完会生成证书、admin.conf 配置文件以及给 Worker 用的 join 令牌。而 Worker 节点的核心动作是"入"——加入集群,也就是执行kubeadm join,把当前节点注册进已有的控制面里。同一个脚本如果既包含 init 又包含 join,脚本就必须去判断当前机器到底扮演什么角色,判断条件越多,出错的面就越大。

再往细了看,Master 初始化完成后还有一些"收尾"动作只有 Master 能做:配置kubectl的访问凭证、安装 CNI 网络插件、生成 Worker 节点的 join 命令并保存到文件。这些动作放到 Worker 脚本里没有任何意义。反过来,Worker 脚本需要做的事情则简单很多,内核参数、容器运行时的部分可以复用同一段逻辑,但集群初始化相关的代码完全不需要。

双脚本方案的第二个好处是"可控性"。初始化集群是有顺序的,必须先 Master 后 Worker,而且所有节点在加入前,Master 必须已处于 Ready 状态。脚本拆开之后,执行顺序天然清晰:先跑 Master 脚本,跑完之后把生成的 join 指令带到 Worker 上跑 Worker 脚本就行。如果过程中某一步失败了,你也能精准定位是哪台机器、哪个阶段的问题,而不是在一个巨型脚本里从头到尾摸黑排查。

还有一个容易被忽略的理由:可维护性。Kubernetes 版本升级时,Master 和 Worker 端的变更节奏往往不同步。版本升级时控制面的kubeadm init参数、CNI 版本、镜像地址可能需要单独调整,而 Worker 侧通常只需要更新 kubelet、kubeadm 的版本。如果只有一个脚本,每次升级都要在一个文件里反复腾挪,很容易动一处伤另一处。拆开之后,各自独立维护,升级一个版本就是改两个文件里各自对应的那几行,清晰得多。

2. 动手写脚本前,必须先敲定的四个关键决策

脚本不是凭空写的。在我把命令落盘之前,先花了半小时确定了几件非常重要的事,这些决策直接影响脚本的稳定性和后续扩展性。

2.1 版本锁定:kubeadm、kubelet、kubectl 必须同一版本线

Kubernetes 社区的版本策略是"kubelet 版本必须小于等于 kubeadm 版本",实操中为了省心,最佳实践是让所有组件保持完全同版本。我这次部署用的是 1.31 版本线,所以脚本里 kubeadm、kubelet、kubectl 安装时都要指定版本号,而不是直接装 latest。如果装了不同版本的组合,kubeadm init的阶段大概率会因为 API 版本不匹配而中断。

这里我用的安装方式是 apt 源锁定版本。具体来说,在/etc/apt/sources.list.d/kubernetes.list里配置好官方源之后,用apt-cache madison kubeadm可以列出所有可用版本,然后在安装命令里直接指定版本号外加hold操作防止意外升级:

apt-get install -y kubelet=1.31.0-* kubeadm=1.31.0-* kubectl=1.31.0-* apt-mark hold kubelet kubeadm kubectl

容器运行时我选择 containerd,版本 1.7.x。这里有个关键配套动作:containerd 和 Kubernetes 是通过 CRI 接口通信的,版本上只要保证 containerd 官方支持 Kubernetes 1.31 即可,不需要和 kubeadm 保持同一个版本号。我在生产实践中更推荐 containerd 而非 docker 作为运行时,因为 containerd 更轻、CRI 兼容更直接,不需要额外装 docker-shim。

2.2 cgroup 驱动:一律使用 systemd

这是初始化容器运行时阶段最容易踩的坑。Kubernetes 从 1.21 开始建议 kubelet 与容器运行时使用 systemd cgroup 驱动,而 containerd 2.x 的某些发行版默认配置仍然是 cgroupfs。如果两边驱动不一致,kubelet 启动后大概率会报failed to run Kubelet: misconfiguration: kubelet cgroup driver: "systemd" is different from docker cgroup driver: "cgroupfs"。

我的处理方式是在 Master 和 Worker 脚本里都加了一段 containerd 配置修正逻辑,强制把/etc/containerd/config.toml里的SystemdCgroup设为true:

mkdir -p /etc/containerd containerd config default | sed 's/SystemdCgroup = false/SystemdCgroup = true/' > /etc/containerd/config.toml systemctl enable --now containerd

这段命令的逻辑是:先用containerd config default生成一份完整的默认配置,再用 sed 替换关键行。注意不要自己在配置文件里手写有限几行配置,containerd 的默认配置包含了很多重要的参数项,直接覆盖会导致运行时行为异常。

2.3 网络插件与 Pod CIDR:我选了 Flannel 配 10.244.0.0/16

Kubernetes 本身不提供 Pod 间网络,初始化之后必须手动安装一个 CNI 插件,否则节点状态永远是 NotReady,Pod 永远调度不上去。主流的 CNI 有 Calico、Flannel、Cilium 等,我这次选的是 Flannel,理由很实际:它是一个纯 overlay 网络方案,部署最轻量,配置文件直接 apply 就能用,不依赖额外的数据面组件;而且对实验环境、中小规模集群来说性能足够。代价是功能上比 Calico 少一些,比如 NetworkPolicy 默认不支持。

选择了 Flannel 也就确定了 Pod CIDR 必须使用10.244.0.0/16,因为 Flannel 的默认配置文件里预设了这个网段。如果在kubeadm init时指定了别的 Pod 网段,应用 Flannel 配置文件后会网段冲突,点位之间完全不通。所以脚本里要写死:

kubeadm init \ --apiserver-advertise-address="${MASTER_IP}" \ --pod-network-cidr=10.244.0.0/16 \ --kubernetes-version=1.31.0

2.4 主机规划与前置检查:时间同步、合理主机名、swap 关闭

很多新手初始化失败的反而不是 kubeadm 本身,而是基础环境不达标。kubeadm 的 preflight 检查非常严格,任何一个条件不满足都会中断。我在脚本里内置了以下三项处理:

第一,swap 必须关闭。Kubernetes 1.31 的 kubelet 默认还不支持启用 swap 的节点,所以脚本里执行swapoff -a只是临时关闭,还需要在 fstab 里注释掉 swap 条目,保证重启后依然生效。第二,时间同步必须做好,证书校验强依赖时钟一致性,我会在初始化之前确保 chrony 服务正常运行。第三,主机名不能重复,也不能包含下划线等特殊字符。两个 Worker 如果重名,注册时节点会互相覆盖,表现非常诡异。最好在跑脚本前手动设置好有区分度的 hostname。

3. Master初始化脚本逐段拆解:从内核调优到CNI安装一气呵成

现在进入正式内容。我会把 Master 脚本的设计思路按执行顺序拆开,每一段函数解决什么、为什么要这样写都会说清楚。这段脚本在初始化一个干净机器时可以直接复制使用,但更建议你理解了之后再根据实际环境微调。

3.1 预检与公共依赖安装

脚本第一段是预检逻辑。因为是快速部署,我不想让脚本跑到一半因为权限、缺依赖之类的问题失败,所以预检写得很"凶":

#!/usr/bin/env bash set -euo pipefail MASTER_IP="${1:?Usage: $0 <master_ip>}" POD_CIDR="${POD_CIDR:-10.244.0.0/16}" if [[ $EUID -ne 0 ]]; then echo "必须使用 root 账号执行本脚本" exit 1 fi # 关闭 swap 并注释掉 fstab 中的条目 swapoff -a sed -ri 's/.*swap.*/#&/' /etc/fstab # 加载内核模块 modprobe overlay modprobe br_netfilter cat > /etc/modules-load.d/k8s.conf <<EOF overlay br_netfilter EOF

overlay模块是 containerd 的 overlayfs 存储驱动要用的,br_netfilter则是让 iptables 能处理 bridge 流量,Kubernetes 的 Service 转发依赖这个能力。只执行modprobe还不够,万一重启后会丢失,所以我在/etc/modules-load.d/k8s.conf里固化了下来。

3.2 sysctl 参数设置:让内核允许流量转发

接着写 sysctl 配置。Kubernetes 节点需要开启 IP 转发,并且要让 iptables 能看见 bridge 上的流量:

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

很多人会漏掉第一行。如果net.bridge.bridge-nf-call-iptables不设为 1,从 Pod 发往 Service 的流量可能会在 bridge 层被绕过 iptables 规则,导致 Service 负载均衡失效。这个参数在旧内核上是默认关闭的,所以必须显式打开。不用纠结这些参数为什么这么难记,这是 Kubernetes 官方文档 preflight 检查中的标准配置项,照做就行。

3.3 容器运行时与 kubeadm 工具链安装

安装 containerd 和 kubeadm 工具链的代码占了脚本近一半的篇幅。这里我不推荐用发行版自带的 containerd,版本更新滞后且配置路径不一致。我的做法是直接配置 docker apt 源来装 containerd,然后通过 kubernetes apt 源装 kubeadm 全家桶:

apt-get update apt-get install -y containerd.io kubelet_install() { curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.31/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.31/deb/ /" > /etc/apt/sources.list.d/kubernetes.list apt-get update apt-get install -y kubelet=1.31.* kubeadm=1.31.* kubectl=1.31.* apt-mark hold kubelet kubeadm kubectl }

这里有个版本细节值得留意:官方源目录结构里带了v1.31这个路径层级,不同大版本对应不同的源目录。如果你的环境是 1.30 或 1.32,源地址里的版本号也要跟着变,否则 apt 会告诉你找不到安装包。

3.4 集群初始化与kubectl配置

工具链装好、containerd 配置完SystemdCgroup = true之后,就可以正式初始化集群了。我的脚本在 init 前加了一个状态判断,避免重复执行时二次 init:

if [[ ! -f /etc/kubernetes/admin.conf ]]; then kubeadm init \ --apiserver-advertise-address="${MASTER_IP}" \ --pod-network-cidr="${POD_CIDR}" \ --kubernetes-version=1.31.0 fi

--apiserver-advertise-address必须指定为 Master 的局域网 IP,也就是说脚本需要你传入一个参数,不能写成0.0.0.0或 default。这决定了其他节点通过哪个地址访问 API Server,写错了 Worker 将永远无法加入集群。

初始化完成后,/etc/kubernetes/admin.conf会生成管理员证书和访问配置。脚本接下来自动配置当前用户的 kubectl:

mkdir -p "$HOME/.kube" cp -i /etc/kubernetes/admin.conf "$HOME/.kube/config" chown "$(id -u):$(id -g)" "$HOME/.kube/config"

注意cp -i如果目标已存在会交互式询问是否覆盖。脚本模式里如果担心卡住,建议改成cp -f。我自己实际用的时候改成了强制覆盖,因为重复执行脚本本来就是为了最后一次配置生效。

3.5 CNI安装与join命令落盘

集群拉起后,核心组件都处于 Running 状态,但节点还是 NotReady,因为没有任何一个 CNI 插件承担 Pod 网络。我直接用 kubectl 应用官方 Flannel 配置文件:

kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml

关键一步来了。Worker 脚本需要执行的kubeadm join命令并不是凭空生成的,它包含一个一次性令牌和 CA 证书的哈希指纹,而这两样只有在 Master 初始化完成之后才会产生。所以 Master 脚本跑完之后,我让它自动把 join 命令落盘保存,存到一个固定路径:

kubeadm token create --print-join-command > /root/k8s_join_cmd.sh chmod +x /root/k8s_join_cmd.sh

--print-join-command会直接输出一条完整的kubeadm join命令,包括 token 和 CA hash。把这个内容重定向到文件后,Worker 脚本可以从这个文件里读取并执行,整个过程不需要手工复制任何信息。这个设计的细节我会在下一节里展开。

4. Worker加入脚本逐段拆解:Join凭证的获取与落地实现

Worker 脚本的骨架和 Master 有约 60% 是重合的——预检、swap 关闭、内核模块、sysctl、containerd 安装、kubeadm 工具链安装,这些都完全一致。这也是为什么我建议你把公共初始化逻辑抽出来,或者在两个脚本之间复制保持一致。我在实际项目中其实是把公共部分直接复制到两个脚本里的,因为它们会分别在各自节点上跑,互相调用的依赖关系越少越好。

4.1 Join命令的三种获取方式与最终选择

Worker 脚本真正和 Master 不同的部分只在最后一段:怎么拿到 join 命令并执行。这里我对比过三种方式:

第一种,在 Master 上把/root/k8s_join_cmd.sh的内容通过 SSH 分发到 Worker。这种方式自动化程度最高,但要求所有节点之间配置了免密登录,对一台全新机器来说多了一层前置配置负担,不够"快速"。

第二种,用配置管理工具如 Ansible 做统一分发、统一执行。这当然是最工程化的方案,但会让"双脚本"变成"双脚本+配置管理工具三件套",增加学习成本。对于个人实验或小规模集群,有点重。

第三种,也是我最终采用的:Worker 脚本接收 Join 命令文件路径作为参数,或者更简单一点,脚本里写死从同一个文件读取。实际部署时,通过 USB、scp、文件共享或者任何顺手的工具把/root/k8s_join_cmd.sh传到 Worker 的对应路径即可。等 Worker 完成基础环境初始化后,直接执行这个脚本文本。

实际使用中我用了一个更省事的小技巧:Master 脚本跑完后,在最后几行把生成的 join 命令内容也一并输出到终端。这时候我只需要复制这行输出,作为参数传给 Worker 的 join 脚本就行,连文件传输都省了。所以 Worker 脚本里有一小段逻辑,允许你把 join 命令作为字符串参数传进来:

JOIN_CMD="${1:-$(cat /root/k8s_join_cmd.sh 2>/dev/null)}" if [[ -z "$JOIN_CMD" ]]; then echo "找不到 join 命令,请从 Master 拷贝 /root/k8s_join_cmd.sh 的内容" exit 1 fi

4.2 Worker脚本执行与状态确认

当所有前置准备就绪,Worker 脚本最终执行的就是这段:

eval "$JOIN_CMD"

为什么用eval而不是bash "$JOIN_CMD"?因为当 JOIN_CMD 是直接传进来的字符串参数时,用bash执行需要先写入文件,而eval直接在当前 shell 上下文执行,适合这种场景。如果 JOIN_CMD 是从文件读的,那两种写法等价。

执行完 join 之后,脚本还会做一次状态确认,询问用户是否要在 Master 上检查节点状态:

echo "Worker 已成功加入集群。可在 Master 节点执行: kubectl get nodes"

我用了一段简单的循环等待,让脚本在 join 后持续检查 kubelet 是否存活,直到输出active (running)或超时退出。这比直接打完命令就结束要可靠得多,能第一时间发现因为版本不匹配、证书问题导致的 kubelet 启动失败。

Worker 脚本的核心就这么简单。相比 Master 脚本的"重",Worker 脚本真正独有部分只有 join 凭证的获取和最终执行,其余的都是在为这段服务。这也是为什么你会看到 Worker 脚本里同样完整地写了 sysctl 和 containerd 配置——缺少任何一部分,kubelet 都无法在这个节点上正常拉起 Pod。

5. 双节点实测:从脚本执行到Pod跨节点通信的完整验证

纸上谈兵到此结束。我拿一台临时环境做了完整实测,两台虚机,一台扮演 Master,一台扮演 Worker。配置是 4C8G,操作系统 Ubuntu 22.04 LTS,内核版本 5.15。这个配置足够跑 Flannel 加几个测试 Pod。

5.1 执行顺序与耗时的真实记录

首先是 Master 节点。上传脚本后执行:

chmod +x master_init.sh ./master_init.sh 192.168.31.10

脚本执行耗时大约在 5-7 分钟,时间大头全部花在 apt 安装和拉取镜像上。第一次看到 kubeadm init 输出的那一大串内容不要慌,只需要关注最后几行。我实测中最后的输出里有这么一段关键信息:

Your Kubernetes control-plane has been initialized successfully!

接着脚本自动执行完了 kubectl 配置和 Flannel 安装。我在 Master 上执行kubectl get nodes,输出如下:

NAME STATUS ROLES AGE VERSION master-1 Ready control-plane 3m10s v1.31.0

注意 STATUS 要在Ready状态。这中间有个时间差:Flannel 的 Pod 要先拉镜像、启动,然后节点才会从 NotReady 变 Ready。期间如果看到NotReady,别急着排查,等一两分钟再kubectl get nodes,大概率已经恢复正常。

接下来是 Worker 节点。我把 Master 上生成好的/root/k8s_join_cmd.sh拷贝到 Worker,然后执行:

chmod +x worker_init.sh ./worker_init.sh

Worker 脚本执行时间更短,大概 3-4 分钟,因为不需要拉控制面镜像。执行完后回到 Master,再次查看节点状态:

kubectl get nodes NAME STATUS ROLES AGE VERSION master-1 Ready control-plane 15m v1.31.0 worker-1 Ready <none> 2m20s v1.31.0

如果 Worker 一直处于 NotReady,最常见的两个原因我留在下面的踩坑清单里说。

5.2 部署测试Pod验证跨节点通信

节点 Ready 不代表网络没问题。我用最简单的 Nginx Deployment 做了连通性测试,副本数设置为 2,这样两个副本大概率会被调度到不同节点上:

kubectl create deployment nginx --image=nginx --replicas=2 kubectl rollout status deployment/nginx kubectl get pods -o wide

看到两个 Pod 分别运行在 master-1 和 worker-1 上之后,再验证跨节点访问。进入其中一个 Pod,去 curl 另一个 Pod 的 IP:

kubectl exec -it <pod-a> -- curl <pod-b-ip>

能返回 Nginx 页面说明两个节点上的 Pod 网络已经通了,VXLAN 隧道正常,Flannel 配置无误。这一步是整条链路最后的承重墙。如果这里不通,问题会集中在 Flannel 的网段配置或者交换机对 VXLAN 流量的限制上,排查思路和 Service 调不通完全不同。

5.3 我踩过的几个常见坑与此解决方案

部署过程中我特意复现了几个高频故障,看看脚本是否能承受,顺便记录下处理方式。

第一,token 过期。kubeadm 默认创建的 bootstrap token 有效期是 24 小时。如果生成的 join 命令放了超过一天没用,Worker 执行 join 就会报 token 过期。解决方式是回到 Master 重新执行一次kubeadm token create --print-join-command,把新命令传给 Worker。我的脚本里已经把这一步独立封装了,所以临时加节点时不用重新初始化集群。

第二,主机名冲突。我有一次因为忘记设置主机名,两台机器都叫 ubuntu,结果 Worker 注册后,Master 上的节点列表里只显示一个节点,而且两个节点的名字互相覆盖。解决方法是分别在两台机器上执行hostnamectl set-hostname master-1和hostnamectl set-hostname worker-1,然后重新 reset 并加入。

第三,版本不匹配。Worker 脚本里如果 apt 装到的是 1.31.1,而 Master 初始化时用的是 1.31.0,kubelet 和 kubeadm 的版本一般不会造成致命问题,但 kubelet 与 API Server 的次版本差距超过一个版本时,连接就会失败。这也是脚本里显式 lock 版本的原因。遇到这种情况时,kubeadm reset后重新执行 Worker 脚本即可。

6. 脚本沉淀后的扩展:加节点、重跑与集群重置

双脚本方案一旦跑通,你接下来一定会遇到这几个场景:集群扩容加 Worker、脚本因为故障需要重跑、整个环境推倒重建。这些场景不写进脚本里的话,每次都会手忙脚乱。我把扩展思路一并写在最后。

6.1 加新Worker:token过期后的快速处理

新加一台 Worker 时,Master 上原来生成的/root/k8s_join_cmd.sh大概率已经过期。不用重新跑 Master 脚本,只要在 Master 上执行:

kubeadm token create --print-join-command

拿到新命令后,在 Worker 上直接执行即可。前提是 Worker 已经装好了 kubeadm、kubelet 这些组件。如果你复用了我上面的 worker_init.sh,就会看到它已经支持把 join 命令字符串直接作为参数传入:

./worker_init.sh 'kubeadm join 192.168.31.10:6443 --token xxxxx --discovery-token-ca-cert-hash sha256:xxxxx'

这比重新拷贝文件再执行要顺滑得多。

6.2 重跑脚本是否安全

我的 Master 脚本本身是幂等的——只要admin.conf存在,就跳过 init 直接走后续 kubectl 配置和 CNI 安装。但如果你真的想完全重来,不能只靠重跑脚本,要先销毁已有集群:

kubeadm reset -f rm -rf /etc/cni/net.d "$HOME/.kube"

然后重新执行 Master 脚本。Worker 同理,执行kubeadm reset -f后重新跑 worker_init.sh。如果你遇到节点状态异常想剔除旧节点,在 Master 上用kubectl drain加kubectl delete node的标准流程也可以,这个问题和脚本关系不大,不展开讲了。

6.3 这套脚本的后续演进方向

双脚本方案是"能用、够用"的底线,不是终点。如果你接手了多套环境,后续可以考虑几个方向的改造。一是参数化已经做了,IP 通过命令行传入,下一步可以把镜像仓库、Kubernetes 大版本号都抽成环境变量,方便多环境切换。二是把公共部分(sysctl、containerd 配置)抽成一个真正的公共模块,比如独立成common.sh供两个脚本 source,避免改动一处两处不同步。三是如果你管理超过五台节点,建议还是引入配置管理工具统一编排,此时双脚本就从"最终方案"降级为"初始原型",但脚本里的所有逻辑依然可以作为工具的基础模块原样复用。

我在实际项目中的体会是,技巧很多时候并不在于命令本身,而在于把整套流程想清楚后,让一个全新的人也能照着脚本不假思索地复现集群。如果你也正准备搭一套 1.31 集群,先从拷这两个脚本开始,跑通一次之后再回头改参数,基本就不会再想问"为什么我的节点又 NotReady"这种问题了。

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

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

立即咨询