入行这些年,我最大的感悟是:装 Kubernetes 这事儿,本身一点不玄乎,真正费时间的往往是那些反反复复的“手动重复劳动”——关交换分区、装运行时、对版本、改配置、等镜像,然后踩一遍别人早就踩过的坑。尤其是跑到 1.31 这个版本,Docker 时代遗留的那套“先装 Docker 再装 K8s”的旧习惯已经彻底过时了,官方默认的容器运行时就是 Containerd。这篇博文我把整个部署过程收敛成一个可以直接跑的一键式脚本,你只要改几个环境变量,执行完基本就是一个能用的 Kubernetes 1.31 集群。适合想快速搭一套环境做验证、学 kubeadm 流程、或者带新同事入手的场景。
1. 整体设计与思路拆解:为什么这套方案值得直接抄作业
动手写脚本之前,我先把设计逻辑说清楚。这套部署方案不是把网上零散的教程拼在一起,而是有一个明确的取舍思路:尽可能贴近 kubeadm 的官方默认路径,同时把那些容易反复出错的“脏活”全部脚本化。
1.1 为什么运行时选 Containerd 而不是 Docker
很多刚接触 Kubernetes 的人会惯性以为“先装 Docker 再装 K8s”是唯一正道,因为网上老教程太多,这个习惯一直延续到现在。但 Kubernetes 从 1.24 版本开始就把 dockershim 从代码里彻底移除了,换句话说K8s 官方已经不再内置对 Docker 的直接支持。现在的标准做法是:Kubernetes 通过 CRI(Container Runtime Interface)直接对接 Containerd,中间少了一层 Docker Daemon 的转发。
用一张简单的对比表就能看清差别:
| 项目 | Docker | Containerd |
|---|---|---|
| 与 K8s 关系 | 需要额外适配层,已被官方移除 | K8s 直接通过 CRI 对接 |
| 进程模型 | Docker Daemon + 守护进程 | 轻量守护进程,面向容器运行时 |
| 镜像构建 | 支持 | 不支持(构建交给 buildkit 等) |
| 命令行工具 | docker | ctr、crictl、nerdctl |
| 资源占用 | 相对较重 | 更轻,生产环境首选 |
打个比方:Docker 是一台带全套厨具的餐车,Containerd 是专门负责“出餐”的后厨。K8s 只需要有人把菜端出来就行,不需要餐车的其他功能。所以 1.31 时代老老实实用 Containerd,既省资源又少出兼容性问题。
1.2 为什么选择 Kubernetes 1.31 这个版本
版本选择也是门学问。1.31 属于当前维护期内的稳定版本,它在 1.30 的基础上补齐了一批问题,生态兼容性已经很成熟。对于想正经搭环境的人来说,选太新的版本(比如刚发布的 RC 版)容易碰到某些组件没跟上导致的小毛病,选太老的版本又要面对支持周期快到期的焦虑。1.31 处于“新但已被广泛验证”的区间,是快速部署和学习的合适选择。
另外从实用角度看,我要用 kubeadm 部署,就必须保证 kubeadm、kubelet、kubectl 三者版本一致。如果三者版本错开,kubelet 和 kubeadm 之间偶发的不兼容问题会让你排查到怀疑人生。所以我的脚本里把K8S_VERSION="1.31.0"设成一个变量,三个组件统一装同一个小版本。
1.3 一键式部署脚本的定位:能跑,但更要能看懂
我也看到过很多“一键脚本”是跑到最后没人知道干了什么,出问题时两眼一抹黑。所以我这个脚本做了折中:所有关键参数都放在文件顶部,中间每个阶段都加注释和输出提示。你直接执行没问题,但我也建议你至少完整读一遍脚本,知道每个步骤在干什么,出问题才能按提示去查日志。
这套方案的目标是:
- 新机器从零开始,一条命令完成基础环境和 K8s 组件安装
- 初始化控制平面,装好网络插件
- 打印出 worker 节点加入集群的命令
- 不覆盖额外的高可用方案,不引入复杂的生产级配置
一句话:适合快速得到一个“能用”的集群,不适合直接当生产高可用标准来抄。
2. 部署前的环境准备:这几个参数搞错,后面全白搭
写脚本之前,先看一下需要满足的机器条件。很多人喜欢跳过这一步直接跑脚本,结果跑到一半发现磁盘不够或者内核模块没启用,反而更费时间。
2.1 硬件与操作系统要求
硬件方面没有想象中那么夸张,但也不要太抠门。我列一个对照表,你可以按自己的用途对号入座:
| 用途 | CPU | 内存 | 磁盘 | 节点数 |
|---|---|---|---|---|
| 单机学习 | 2 核 | 4G | 40G | 1 |
| 基础实验 | 2 核 | 4G | 50G | 2-3 |
| 稍微正式的测试环境 | 4 核 | 8G | 100G | 3 |
操作系统优先推荐 Ubuntu 22.04/24.04 或 Debian 12,我的主脚本也是基于 Debian 系写的。如果你用的是 Rocky Linux / AlmaLinux / CentOS Stream,可以用 RPM 系的软件源做法,后面我会单独给替换示例。最重要的一点是:三台机器的系统版本尽量一致,内核版本差异太大会让排错变得复杂。
2.2 主机规划:IP 地址、主机名、端口
集群通信是走端口的,这一步千万别偷懒。假设我给三台机器做规划:
| 节点角色 | 主机名 | IP 地址 |
|---|---|---|
| 控制平面 | k8s-master | 192.168.1.10 |
| Worker 节点 | k8s-node01 | 192.168.1.11 |
| Worker 节点 | k8s-node02 | 192.168.1.12 |
主机名不要带下划线,不要有大写,Kubernetes 对主机名的要求比较严格,解析不干净会引发 kubelet 注册失败。
端口方面,控制平面至少要保证这些端口可通信:
- 6443:kube-apiserver,所有客户端入口
- 2379/2380:etcd 客户端和集群通信
- 10250:kubelet 健康检查和日志获取
- 10257:kube-controller-manager
- 10259:kube-scheduler
- 30000-32767:NodePort 服务端口段
如果你用的是云厂商的安全组,直接放行这些端口;如果是本机测试又懒得搞防火墙规则,临时关掉防火墙跑通流程,但正式环境一定要按最小放行原则配置。
2.3 系统基础项:关交换分区、加载内核模块、同步时间
Kubernetes 的 kubelet 强烈建议关闭 swap,原因很直接:Pod 的资源申请和限制是基于内存做的,如果系统出现 swap 交换,内存资源隔离就没意义了。脚本里会执行swapoff -a并注释掉/etc/fstab中 swap 相关行,保证重启后也不会自动挂载。
内核模块方面,需要加载overlay和br_netfilter。overlay是容器镜像分层存储的基础,br_netfilter让 iptables 能处理桥接流量,这是 Kubernetes 网络通信正常工作的前提。然后再设置一组 sysctl 参数:
cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.ipv4.ip_forward = 1 EOF sysctl --system最后是时间同步。etcd 对时钟偏移极度敏感,如果节点间时间差超过几百毫秒,集群可能会出现各种莫名其妙的间歇性故障。Ubuntu 上装个 chrony 或者直接用 systemd-timesyncd,只要保证节点时间一致即可。
3. 一键式部署脚本的实现:从零到集群可用
这一节是整篇的核心。我把完整脚本贴在下面,并且用注释标明每个阶段在做什么。先声明:脚本按 Ubuntu/Debian 系写的,你在 Rocky/CentOS 上跑,需要把包管理器部分替换成 dnf/yum 版,我后面会专门给出差异说明。
3.1 完整脚本内容
#!/usr/bin/env bash set -euo pipefail # ===================================================== # 快速搭建 Kubernetes 1.31 + Containerd 一键式脚本 # 适用系统:Ubuntu 22.04 / 24.04、Debian 12 # 用法: # 1. 修改下方 MASTER_IP、POD_CIDR 等变量 # 2. 以 root 或 sudo 权限执行本脚本 # ===================================================== # ---------- 按需修改的参数 ---------- K8S_VERSION="1.31.0" MASTER_IP="192.168.1.10" POD_CIDR="10.244.0.0/16" SERVICE_CIDR="10.96.0.0/12" # 如果你的网络环境访问 registry.k8s.io 较慢, # 可以把 IMAGE_REPO 改成你本地网络更容易访问的镜像仓库。 IMAGE_REPO="registry.k8s.io" # ---------- 全局变量 ---------- LOG_PREFIX="[K8s-Deploy]" log() { echo "$(date '+%F %T') ${LOG_PREFIX} $*" } # ---------- 1. 系统基础配置 ---------- log "关闭 swap..." swapoff -a sed -i '/ swap / s/^/#/' /etc/fstab log "加载内核模块..." cat <<EOF | tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF modprobe overlay modprobe br_netfilter log "配置内核参数..." cat <<EOF | tee /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.ipv4.ip_forward = 1 EOF sysctl --system >/dev/null log "设置主机名(可选,当前主机名:$(hostname))..." # 如果你需要统一主机名,取消下面这行并改成你的节点名称 # hostnamectl set-hostname k8s-master # ---------- 2. 安装 Containerd ---------- log "安装 Containerd..." apt-get update -y apt-get install -y ca-certificates curl gnupg install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg chmod a+r /etc/apt/keyrings/docker.gpg echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ tee /etc/apt/sources.list.d/docker.list >/dev/null apt-get update -y apt-get install -y containerd.io log "生成 Containerd 默认配置..." mkdir -p /etc/containerd containerd config default | tee /etc/containerd/config.toml >/dev/null log "开启 SystemdCgroup..." sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml log "重启 Containerd..." systemctl enable containerd systemctl restart containerd # ---------- 3. 安装 kubeadm / kubelet / kubectl ---------- log "添加 Kubernetes apt 源..." curl -fsSL https://pkgs.k8s.io/core:/stable:/v${K8S_VERSION}/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:/v${K8S_VERSION}/deb/ /" | \ tee /etc/apt/sources.list.d/kubernetes.list >/dev/null apt-get update -y apt-get install -y kubelet=${K8S_VERSION}-* kubeadm=${K8S_VERSION}-* kubectl=${K8S_VERSION}-* apt-mark hold kubelet kubeadm kubectl log "启动 kubelet(初始化前可能处于异常状态,正常现象)..." systemctl enable kubelet # ---------- 4. 拉取镜像并初始化控制平面 ---------- log "拉取 Kubernetes 组件镜像..." kubeadm config images pull --image-repository=${IMAGE_REPO} log "初始化控制平面..." kubeadm init \ --kubernetes-version=${K8S_VERSION} \ --control-plane-endpoint=${MASTER_IP} \ --pod-network-cidr=${POD_CIDR} \ --service-cidr=${SERVICE_CIDR} \ --image-repository=${IMAGE_REPO} \ --v=5 log "配置 kubectl..." mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config # ---------- 5. 安装网络插件 Flannel ---------- log "安装 Flannel CNI..." kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml log "等待控制平面 Pod 就绪..." kubectl wait --for=condition=Ready pods --all -n kube-system --timeout=300s || true log "部署完成!当前节点状态:" kubectl get nodes log "如果你要加入 Worker 节点,请复制下面的命令:" kubeadm token create --print-join-command3.2 脚本里最容易写错的两个地方
第一个是SystemdCgroup。Containerd 默认生成的配置里这一项是false,而 kubelet 如果通过 systemd 管理(kubeadm 默认就是),两边 cgroup 驱动不一致,节点会一直卡在 NotReady。脚本里用 sed 直接把它改成true,这步漏掉,后面必然翻车。
第二个是--pod-network-cidr和 CNI 插件的关系。我用的是 Flannel,它默认使用的网段就是10.244.0.0/16。如果你用 Calico,通常会把网段设置成10.244.0.0/16或者192.168.0.0/16,具体以你安装的插件为准。这里一旦不一致,Pod 能创建出来,但分配到的 IP 和路由对不上,网络直接不通。
3.3 在 Rocky Linux / AlmaLinux 上怎么改
如果你用的是 RPM 系系统,脚本的第三部分要换成这样:
cat <<EOF | tee /etc/yum.repos.d/kubernetes.repo [kubernetes] name=Kubernetes baseurl=https://pkgs.k8s.io/core:/stable:/v1.31/rpm/ enabled=1 gpgcheck=1 gpgkey=https://pkgs.k8s.io/core:/stable:/v1.31/rpm/repodata/repomd.xml.key EOF dnf install -y kubelet kubeadm kubectl --disableexcludes=kubernetesContainerd 的安装也可以直接用系统源里的containerd.io或containerd包,但一定要确认版本不要太老。我建议 RPM 系直接参考 Docker 官方仓库的安装步骤,或者使用系统自带的容器工具包,前提是版本至少为 1.7.x。版本太老的 Containerd 和 K8s 1.31 在 CRI 适配上有概率出现兼容问题。
3.4 脚本的实际执行流程
执行方式很简单:
chmod +x k8s-deploy.sh sudo ./k8s-deploy.sh整个流程跑完大概需要 5-10 分钟,具体看网络速度和机器配置。看到最后kubectl get nodes输出里 master 节点是Ready状态,就说明控制平面起来了。
注意:如果最后一步
kubectl wait超时,不要急着重启一切,先看 CNI 插件是否装好,再看 kube-system 命名空间里的 Pod 状态。这属于常见问题,我在第 5 节专门讲。
4. 集群初始化后的验证与基础配置
脚本跑完不等于万事大吉,至少要做一轮验证,确保新节点能正常调度 Pod。下面是我每次部署完都会走的检查清单。
4.1 检查控制平面核心组件
先看节点状态:
kubectl get nodes期望输出大概是:
NAME STATUS ROLES AGE VERSION k8s-master Ready control-plane 10m v1.31.0如果 STATUS 是NotReady,先排查 kube-system 里的 Pod:
kubectl get pods -n kube-system重点看 coredns 和 flannel 是否 Running。如果这两个组件都正常,节点从 NotReady 到 Ready 一般只是时间问题;如果一直 Pending 或 CrashLoopBackOff,问题大概率出在镜像拉取或 CNI 配置上。
4.2 kubectl 权限配置与自动补全
kubeadm init 完成后,默认情况下面向普通用户需要用admin.conf。脚本里已经帮你 copy 到了$HOME/.kube/config。如果你是 root 跑的,要记得给普通用户也配置相同内容。
顺便配置一下补全,省得后面敲命令太痛苦:
echo 'source <(kubectl completion bash)' >> ~/.bashrc source ~/.bashrc4.3 用一条命令验证集群调度能力
光看节点 Ready 还不够,我习惯起一个临时 Pod 做冒烟测试:
kubectl create deployment smoke-test --image=nginx:alpine kubectl expose deployment smoke-test --port=80 --type=NodePort kubectl get pods -o wide如果 Pod 顺利 Running,说明从镜像拉取、调度到网络分配整条链路都没问题。测试完删掉这两个资源就行:
kubectl delete service smoke-test kubectl delete deployment smoke-test4.4 把 Worker 节点加进集群
如果脚本是在所有节点上跑的,那么你只需要在 Worker 节点上执行之前打印出来的 join 命令,就能把节点加进来:
kubeadm join 192.168.1.10:6443 --token <token> \ --discovery-token-ca-cert-hash sha256:<hash>token 默认有效期是 24 小时,如果你跑完脚本忘了保存 join 命令,可以在 master 上重新生成:
kubeadm token create --print-join-commandWorker 节点加入后,回到 master 上kubectl get nodes就能看到新节点上线。
5. 实际部署中最容易翻车的几个问题
这部分是我最想写给你们的,全是实操中踩过的坑。我按出现频率从高到低排列。
5.1 镜像拉取失败或超时
症状:kubeadm init卡在拉镜像阶段,或者kube-system里的 Pod 频繁ImagePullBackOff。
排查思路就三步:
# 1. 手动拉一下,确认仓库可访问 kubeadm config images pull --image-repository=registry.k8s.io # 2. 如果超时,就换成网络环境容易访问的镜像仓库,再重新 init kubeadm init --image-repository=<可访问的镜像仓库> ... # 3. 查看运行时里到底有没有镜像 crictl images这里我要强调一句:改镜像仓库不是让你绕过网络限制,而是换一个更近、更快的源。部署前最好先确认自己所在网络到达registry.k8s.io的通畅程度,不通就直接把 IMAGE_REPO 替换成你网络环境更容易访问的镜像仓库,然后重新执行脚本。
5.2 cgroup 驱动不一致
症状:kubelet 一直在CrashLoopBackOff,journalctl -u kubelet里能看到类似failed to run Kubelet: failed to create kubelet component ... cgroup driver cgroupfs的报错。
这个问题的根源我在前面提过:Containerd 默认是cgroupfs,kubeadm 默认把 kubelet 配成systemd。两边不一致,kubelet 直接起不来。
解决办法就是保证两边统一,在/etc/containerd/config.toml中确认:
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] SystemdCgroup = true改完重启 containerd,再重启 kubelet:
systemctl restart containerd systemctl restart kubelet5.3 kubelet 无法启动,日志没有任何有用信息
有些时候journalctl -u kubelet打出来的日志很少,拼命刷content of /var/lib/kubelet/config.yaml之类。这种情况第一个要看的文件是/var/lib/kubelet/kubeadm-flags.env,确认 kubelet 启动参数是否正确。
另外还要检查 kubelet 的证书目录/etc/kubernetes/pki,如果 kubeadm init 因为前面某步失败导致残留文件不完整,再跑 init 时会卡在证书校验。老练一点的处理方法:
kubeadm reset -f rm -rf /etc/cni/net.d rm -rf /var/lib/kubelet/*然后再重新执行一次脚本,干干净净重来,比花半小时抠日志更快。
5.4 Flannel Pod 一直 CrashLoopBackOff
这个问题我遇到好几次,原因通常是旧的 flannel.1 网卡残留。当你重复执行脚本,或者切换过 CNI 插件时,节点上可能残留老网卡和路由,导致新的 flannel 起不来。
处理方式:
ifconfig flannel.1 down ip link delete flannel.1 # 清理 CNI 配置 rm -rf /etc/cni/net.d/*然后重新 apply Flannel 清单。
如果还是不行,检查节点之间的内网是否放行了UDP 8472端口,这是 Flannel VXLAN 模式用的端口。
5.5 常见问题速查表
| 症状 | 大概率原因 | 处理方式 |
|---|---|---|
| node 一直是 NotReady | CNI 没装好或 cgroup 不一致 | 检查 kube-system Pod 状态;确认 SystemdCgroup=true |
| coredns Pending | CNI 没就绪,或节点有污点 | 先装 flannel/calico;kubectl describe pod看事件 |
| 镜像拉取失败 | 仓库不可达 | 换镜像仓库后 kubeadm reset 重来 |
| join 命令失效 | token 过期 | 在 master 上重新生成 join 命令 |
| Pod 之间 ping 不通 | 网段与 CNI 不匹配 | 检查 POD_CIDR 与 flannel/calico 配置 |
| kubectl 命令提示拒绝连接 | KUBECONFIG 没配置 | export KUBECONFIG=/etc/kubernetes/admin.conf |
6. 写在最后的几句话
如果你是个新手,我强烈建议拿到脚本之后,先不要急着闭眼跑,而是打开脚本对着 kubeadm 官网文档一段一段读一遍。自动化的价值不是让你放弃理解,而是让你在理解之后免于重复劳动。我第一次搭集群全程手动敲,整整折腾了大半天,后来把所有步骤沉淀成脚本,后面每次重建环境基本都能在十分钟内搞定,这才是“一键式脚本”真正值钱的地方。
这套部署方案后续还有很多可以往外扩展的地方:比如把单控制平面改成高可用,给 etcd 加证书备份,用 Ansible 把脚本改造成批量执行,甚至把 Worker 节点初始化做成独立 playbook。但不管怎么扩展,底层核心还是这篇文章里这些东西。把它们吃透了,后面再怎么变,你都有一条清晰的排查路径,而不是遇到问题只会重装系统。