简介这是一套面向运维人员与Kubernetes初学者的集群快速部署脚本包用于解决多节点环境下Docker、cri-dockerd与k8s组件手动安装配置繁琐的问题。包内共7个文件以3个sh脚本为主配合yml、yaml配置、txt教程说明及rpm离线安装包压缩包约10.67MB覆盖安装、卸载、网络插件与kubeadm初始化等环节。使用者可按实际集群节点规划调整脚本参数与软件版本将文件上传至Master和Node的/home目录并执行安装脚本即可完成集群环境搭建。资源内置Docker 24.0.7、cri-dockerd 0.3.9与Kubernetes v1.28.2的版本组合便于快速复现实验环境、验证部署流程并积累排错思路。目前已有953人学习下载适合需要缩短搭建周期、减少重复配置的中级运维读者参考。1. 从一条命令到一套集群这份脚本到底替你扛了哪几步真正耗时间的从来不是kubeadm init那一分钟而是它之前那二十几步关 swap、载内核模块、改 cgroup driver、装 cri-dockerd、锁版本、预载镜像、算 token、等 CNI 起来。Docker 容器化 K8s 集群部署这件事难点不在 K8s 本身而在三台机器底座是否完全一致——只要有一台的ip_forward是 0你就会在凌晨两点对着NotReady发呆。这份一键部署 shell 脚本干的就是把底座固化变量区改六个值剩下交给函数重复执行不会把集群搞坏。它适合两类人需要给团队反复重建测试集群的以及第一次搭集群又不想在 sysctl 上耗两个晚上的。它不承诺生产级高可用但会把“能跑起来”到“跑得稳”之间的坑提前标在脚本注释里。2. 底座先立住Docker 运行时、内核参数与节点规划的三道关集群装不上九成不是 K8s 的问题是脚本跑之前机器就没准备好。这一章先把三件事说透运行时为什么选 Docker、内核参数到底在管哪条流量、节点和端口怎么规划才不会在 join 阶段返工。脚本里preflight()函数做的事就是把这三件事变成可判定的检查项而不是靠人肉记忆。2.1 为什么这份脚本还要保留 Docker 运行时K8s 从 1.24 起移除了 dockershimcontainerd 成为默认 CRI这是常识。但很多团队的镜像构建流水线、排障习惯、离线包分发全都长在 Docker 上docker images、docker save、docker load这套动作比crictl顺手得多。保留 Docker 作为运行时不是不行代价是要多装一个 cri-dockerd 做 shim把 Docker 的 API 翻译成 CRI 协议。选它还是选 containerd我一般按这张表判断判断项Docker cri-dockerd直接用 containerd已有构建流水线复用docker build零改动需要迁到 buildkit 或 nerdctl多一个故障点是cri-dockerd 服务挂了节点就 NotReady否链路短一跳离线镜像分发docker save/load最直观ctr images import参数略绕资源开销多一个 dockerd 常驻更轻排障熟悉度大多数人有肌肉记忆crictl需要适应如果集群是给业务长期跑生产的我倾向 containerd如果这套集群的定位是测试环境、教学环境、或者要和现有 CI 共用镜像Docker 路线的综合成本更低。脚本用RUNTIMEdocker这个变量控制分支两种都留了口子。不管选哪个daemon.json里有一行必须写死写不对后面一定翻车{ exec-opts: [native.cgroupdriversystemd], log-driver: json-file, log-opts: { max-size: 100m, max-file: 3 }, storage-driver: overlay2, insecure-registries: [registry.local:5000] }exec-opts那行是核心——Docker 默认用 cgroupfs而 kubelet 在 systemd 系统上默认认 systemd两者不一致时 kubelet 会反复报failed to run Kubelet: misconfiguration: kubelet cgroup driver systemd is different from docker cgroup driver cgroupfs。日志文件大小上限也得设不然跑几个月/var/lib/docker/containers能把磁盘吃干净这种事故我见过两次。insecure-registries按需用内网仓库时才填。2.2 内核模块与 sysctlbridge-nf-call-iptables 到底管什么这一节是整份脚本里最容易被跳过、也最容易出事的地方。br_netfilter模块不加载net.bridge.bridge-nf-call-iptables这个 sysctl 写进去也是摆设写了但值是 0桥接的 Pod 流量就不会走 iptables 规则链表现出来的现象是同节点 Pod 访问 Service 的 ClusterIP 超时而直接访问 Pod IP 又是通的——这个现象非常容易把人带到 CNI 的坑里去。# 加载容器与网桥过滤所需的内核模块并做开机持久化 modprobe overlay modprobe br_netfilter cat /etc/modules-load.d/k8s.conf EOF overlay br_netfilter EOF # 这三行是集群互通的地基缺一行都可能出现跨节点不通 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 sysctl net.ipv4.ip_forward net.bridge.bridge-nf-call-iptablesmodules-load.d下的文件保证重启后模块自动加载只modprobe一次是临时的机器一重启就回到原点。ip_forward1控制的是跨节点转发值为 0 时节点间流量直接丢弃kubectl get pods会看到大量CrashLoopBackOff。最后那行sysctl是自检脚本里应该把输出打印出来而不是静默通过——我吃过“以为生效了”的亏--system在某些精简镜像里会因为文件权限不执行。如果 kube-proxy 走 IPVS 模式还得额外加载一批模块比如ip_vs、ip_vs_rr、ip_vs_wrr、ip_vs_sh、nf_conntrack。少写一个kube-proxy 的 Pod 就会在CrashLoopBackOff里循环日志里写着cant set sysctl或者IPVS proxier will not be used。2.3 节点 IP、主机名与端口规划表脚本能不能一把过取决于变量区填得对不对。最容易忽略的是主机名——kubelet 默认用 hostname 向 apiserver 注册节点两台机器如果主机名相同后 join 的会把前一个节点信息覆盖掉kubectl get nodes里永远只有一个节点非常玄学。所以脚本第一件事应该是hostnamectl set-hostname或者校验 hostname 唯一。节点规划我一般按这张表填角色主机名示例 IP配置建议控制面node-master-0110.0.10.112C4G 起etcd 和 apiserver 同机工作节点 1node-worker-0110.0.10.214C8G按 Pod 密度调工作节点 2node-worker-0210.0.10.22同上Pod 网段—10.244.0.0/16与节点网段、Service 网段不重叠Service 网段—10.96.0.0/12kubeadm 默认值一般不用改网段重叠是最隐蔽的一类问题节点网段是10.244.x.x又把--pod-network-cidr设成10.244.0.0/16结果 Pod 路由和宿主机路由打架排查时看路由表能看半天。端口方面脚本至少要保证6443apiserver、10250kubelet、2379-2380etcd、8472/udpflannel vxlan、179calico BGP是通的。如果机器上有防火墙firewall-cmd --add-port要提前放行或者干脆在预检阶段直接停掉——测试环境我一般选择关掉省下的时间够写三个业务模块。3. 拆开脚本看流程四个阶段与它们各自的判断条件一键脚本之所以敢叫“一键”靠的不是顺序执行而是每一步都有幂等判断已经装过的跳过已经 init 过的换 join镜像已存在的跳过 load。这一章按脚本执行顺序拆四个阶段每个阶段给出关键片段和判断逻辑你可以对着自己的脚本逐段核对。3.1 阶段一环境自检与幂等开关自检阶段要做的事比想象中多root 权限、操作系统版本、swap 状态、SELinux、时间同步、端口占用、集群是否已存在。判断集群是否已存在最简单的标志是/etc/kubernetes/admin.conf是否可读——有它说明这台机器已经 init 过再跑一次kubeadm init会直接报错退出。#!/usr/bin/env bash set -euo pipefail # 所有日志统一带时间戳方便出问题时回溯 log() { printf [%s] %s\n $(date %H:%M:%S) $*; } check_root() { [[ $EUID -eq 0 ]] || { log 需要 root 权限; exit 1; } } disable_swap() { # swap 开着时 kubelet 会拒绝启动这里做临时关闭加持久化 swapoff -a sed -ri /\sswap\s/s/^/#/ /etc/fstab log swap 已关闭并写入 fstab } check_cluster_exists() { if [[ -f /etc/kubernetes/admin.conf ]]; then log 检测到已初始化的集群跳过 init 阶段 return 0 fi return 1 }set -euo pipefail是这类脚本的安全带出错立即退出、引用未定义变量报错、管道中任一环节失败都算失败。没有这三个选项的脚本某一步失败后它会继续往下跑最后报一堆无关错误等于把黑匣子埋了。sed -ri那行把/etc/fstab里的 swap 注释掉-i是原地改-r开扩展正则如果只swapoff -a不写 fstab重启后 swap 回来kubelet 又起不来。这段代码里check_cluster_exists返回 0/1主流程用它决定走 init 分支还是 join 分支这就是幂等的第一层。时间同步也得在这一阶段检查。chronyc tracking或者timedatectl一看就知道节点间时间差超过几秒证书校验和 etcd 选举都会出幺蛾子。3.2 阶段二运行时安装与离线镜像导入运行时安装的关键词是“版本锁”。kubeadm、kubelet、kubectl 三个包版本必须一致而且 kubelet 版本不能高于 apiserver 太多一般不超过一个小版本。装完立刻apt-mark hold或yum versionlock不然某次apt upgrade就能把集群升挂。K8S_VERSION1.28.4 RUNTIMEdocker install_runtime() { if [[ $RUNTIME docker ]]; then # 只装指定大版本避免源里最新版把 cgroup 行为改掉 apt-get install -y docker-ce5:24.0.* docker-ce-cli5:24.0.* containerd.io # cri-dockerd 是 Docker 与 CRI 之间的桥缺了 kubelet 连不上 dpkg -i ./pkgs/cri-dockerd_0.3.4.3-0.ubuntu-jammy_amd64.deb systemctl enable --now docker cri-docker fi } install_kube_tools() { apt-get install -y \ kubeadm${K8S_VERSION}-1.1 \ kubelet${K8S_VERSION}-1.1 \ kubectl${K8S_VERSION}-1.1 # 锁定版本防止后续无人值守升级 apt-mark hold kubeadm kubelet kubectl } load_images() { local dir$1 for img in ${dir}/*.tar; do [[ -e $img ]] || { log 镜像目录为空; return 0; } # 先查再 load重复执行时能省掉几分钟 local name name$(tar -xOf $img manifest.json | grep -o RepoTags:\[[^]]* | head -1) log 载入镜像 ${name} docker load -i $img done }cri-dockerd那行的版本号要和你RUNTIME对应装完必须systemctl enable --now cri-docker这个服务不开kubelet 连 socket 时会报failed to get container info。load_images里先解析manifest.json再决定是否 load是一种粗略的幂等——更严格的做法是拿docker images --format {{.Repository}}:{{.Tag}}和kubeadm config images list的输出做diff差集非空才导。离线环境里这一点特别值钱因为镜像包动辄几个 G每次全量 load 会让人怀疑人生。3.3 阶段三kubeadm init 与 token 的生成、续期init 是整份脚本里参数最多的一步也是最不该写死的一步。用配置文件比堆命令行参数好维护kubeadm config print init-defaults打一份模板改advertiseAddress、podSubnet、criSocket三个字段就够了。# kubeadm-config.yaml apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 10.0.10.11 bindPort: 6443 nodeRegistration: criSocket: unix:///var/run/cri-dockerd.sock name: node-master-01 --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.28.4 networking: podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/12criSocket是 Docker 路线特有的坑点kubeadm 默认探测 containerd 的 socket你要是不显式指定unix:///var/run/cri-dockerd.sock它会报“找不到 CRI 端点”然后停在那儿。advertiseAddress必须是其它节点能访问到的内网 IP填127.0.0.1的话 join 阶段会一直超时。podSubnet要和后面装的 CNI 对得上flannel 默认10.244.0.0/16calico 默认192.168.0.0/16两边不一致时节点NotReady是必然结果。init 成功后脚本立刻做两件事拷 kubeconfig 到当前用户目录以及生成 join 命令并落到/root/join.sh。token 默认 24 小时过期过期后 join 会报bootstrap token is expired所以脚本里应该提供--renew参数调kubeadm token create --print-join-command重新生成一条而不是让使用者自己翻文档。gen_join_cmd() { # 重新生成 token 并拼上 cri-socket 参数输出到 join.sh kubeadm token create --print-join-command \ | sed s/$/ --cri-socket unix:\/\/\/var\/run\/cri-dockerd.sock/ \ /root/join.sh chmod x /root/join.sh log join 命令已写入 /root/join.sh }sed那步是给 Docker 路线补 socket 参数kubeadm 打印出来的 join 命令不带它直接复制粘贴到 worker 上会失败。这个细节至少绊过我三次。3.4 阶段四CNI 落地与节点 join 的收尾校验CNI 是最后一块拼图也是“看起来装完了但没装完”的分水岭。kubectl apply -f一份 CNI 清单等kube-system下的 Pod 全Running节点才会从NotReady翻成Ready。这个等待要用轮询而不是sleep 30。wait_cni_ready() { local timeout${1:-180} local elapsed0 while (( elapsed timeout )); do local not_ready not_ready$(kubectl get nodes --no-headers | grep -c NotReady || true) if [[ $not_ready -eq 0 ]]; then log 所有节点已 Ready return 0 fi sleep 5 elapsed$((elapsed 5)) done log 等待超时请检查 CNI Pod 状态 kubectl get pods -n kube-system -o wide return 1 }超时后自动打印kube-system下的 Pod 列表是脚本里性价比最高的一个设计——大部分问题看一眼哪个 Pod 没起来就有方向了。--no-headers去掉表头grep -c计数|| true防止grep没匹配时被set -e直接干掉脚本。这几个小细节决定脚本是能自己报错还是无声无息地卡住。4. 变量表怎么改六个必改项、优先级与覆盖规则拿到脚本第一件事不是执行是打开变量区看一眼。这一章把必改项、可选项、覆盖规则讲清楚照着改完基本可以一次跑通。4.1 六个必须改的变量这六个值填错脚本跑完集群也是残的变量含义填错的表现MASTER_IP控制面内网地址join 超时、apiserver 无法访问NODE_IPS工作节点地址列表脚本跳过某台机器或 ssh 失败K8S_VERSIONkubeadm/kubelet 版本与离线镜像 tag 不匹配拉取失败POD_CIDRPod 网段与 CNI 默认值冲突节点 NotReadyRUNTIMEdocker 或 containerdsocket 路径错误kubelet 起不来IMAGE_DIR离线镜像目录导入为空ImagePullBackOffNODE_IPS建议用数组写脚本里for ip in ${NODE_IPS[]}遍历比空格分隔的字符串好处理边界。K8S_VERSION要和kubeadm config images list的输出对得上比如v1.28.4对应kube-apiserver:v1.28.4版本号差一个补丁号镜像就得重新拉。4.2 可选开关与默认行为的取舍除了必改项还有几个开关默认值值得掰扯OFFLINEtrue离线模式跳过所有外网访问全部走本地 tar 包。内网环境必开。INSTALL_CNIflannel换成calico时要同步改POD_CIDR两者默认网段不同。CLOSE_FIREWALLtrue测试环境的默认值。生产环境应该改成false并自己写放行规则。ENABLE_IPVStruePod 数量多时 IPVS 比 iptables 性能好但要求内核模块齐全。SET_HOSTNAMEtrue自动按节点前缀序号重命名省去手工改。默认值我一般只动CLOSE_FIREWALL和ENABLE_IPVS其它保持原样。开关太多反而增加认知负担一份脚本能覆盖 80% 的场景就够了。4.3 配置文件、环境变量与命令行参数的优先级脚本通常支持三种传参方式改脚本顶部的变量、用环境变量覆盖、命令行传参。优先级从高到低是命令行 环境变量 脚本默认值。实现上就是这几行# 脚本内默认值 MASTER_IP${MASTER_IP:-10.0.10.11} POD_CIDR${POD_CIDR:-10.244.0.0/16} RUNTIME${RUNTIME:-docker} # 解析命令行参数覆盖环境变量 while [[ $# -gt 0 ]]; do case $1 in --master-ip) MASTER_IP$2; shift 2 ;; --pod-cidr) POD_CIDR$2; shift 2 ;; --runtime) RUNTIME$2; shift 2 ;; --renew) RENEW_TOKENtrue; shift ;; *) log 未知参数: $1; exit 1 ;; esac done${VAR:-default}这套写法让环境变量天然具备覆盖能力case分支再叠一层命令行。关键是所有参数解析必须在任何实际动作之前完成别出现“前半段用默认值跑了后半段才读到用户传的值”这种半截配置。shift 2是因为带值参数占了两个位置漏写会死循环。5. 避坑与排查五条血泪记录的现象、原因与解法脚本能跑通不代表集群能用。这一章是我在不同机器上踩出来的五条记录按“现象 → 原因 → 解决”写遇到相似症状可以直接对号入座。5.1 网络与 CNI 类故障第一条所有节点NotReadyCNI Pod 全在CrashLoopBackOff。现象是kubectl get nodes全是NotReadykubectl get pods -n kube-system里 flannel 或 calico 反复重启日志里出现failed to set up mount或者cannot find network namespace。原因通常是 CNI 的 DaemonSet 没拿到宿主机路径挂载权限或者节点上残留了上一次部署的/var/lib/cni、/etc/cni/net.d脏数据。解决方式是先清空这两个目录再重装 CNI命令是rm -rf /etc/cni/net.d/* /var/lib/cni/*然后删掉 DaemonSet 重新 apply。如果是重装过集群的机器这一步几乎是必做项。第二条跨节点 Pod 互相 ping 不通同节点正常。现象很典型同一台机器上的两个 Pod 能通跨节点就不行kubectl exec进去ping一直超时。原因之一是宿主机防火墙拦了 VXLAN 的8472/udp或者云环境的网络策略把 UDP 封了。另一个原因是节点网卡不是同一网段路由表里没有对方的回程路由。解决方式是firewall-cmd --add-port8472/udp --permanent后 reload或者在脚本里直接把CLOSE_FIREWALL设成true。云上环境还得确认安全组放行这一步脚本管不了只能在文档里写清楚。5.2 运行时与 cgroup 类故障第三条kubelet 日志刷屏kubelet cgroup driver systemd is different from docker cgroup driver cgroupfs。现象是 kubelet 反复重启journalctl -u kubelet里满屏都是这行。原因就是 2.1 节说的Docker 和 kubelet 的 cgroup driver 不一致。解决办法是先确认/etc/docker/daemon.json里有exec-opts: [native.cgroupdriversystemd]然后systemctl daemon-reload systemctl restart docker。注意顺序必须重启 docker 再重启 kubelet反过来不生效。改完docker info | grep Cgroup应该显示systemd这一步验证不能省。第四条kubeadm init卡在Waiting for the kubelet to boot up the control plane。这个阶段超时通常是两件事要么 kubelet 根本没起来版本不匹配、配置错要么镜像没拉到。先看journalctl -u kubelet -n 50如果日志里有failed to pull image就是离线镜像没导全如果什么都没有可能是kubelet服务被 mask 了或者配置文件路径不对。解决方式是kubeadm config images list --kubernetes-version v1.28.4打一份期望清单和docker images的实际清单做 diff差什么补什么。5.3 证书、token 与时间类故障第五条第二天加节点join 报bootstrap token is expired。kubeadm init生成的 token 默认 24 小时过期隔天再 join 必挂。原因不是集群坏了是 token 有效期到了。解决方式是主节点执行kubeadm token create --print-join-command重新生成一条带新 token 的命令别忘了补--cri-socket参数。如果是证书过期默认一年用kubeadm certs check-expiration查看剩余时间kubeadm certs renew all续期后重启 control plane 的静态 Pod。这类问题最好的处理方式是写进脚本的--renew分支而不是等使用者撞上再去翻文档。6. 验收与进阶把一键部署变成可重复交付脚本跑完、节点全Ready只能算交付了一半。真正的验收要能证明“集群可以跑业务”而不只是“进程活过来了”。6.1 三类验收脚本第一类是节点与组件状态kubectl get nodes -o wide、kubectl get pods -n kube-system重点看有没有CrashLoopBackOff和重启次数非零的 Pod。第二类是网络连通性起一个测试 Deployment 扩到两个副本用kubectl expose建一个 ClusterIP Service再在另一个 Pod 里curl服务名能通说明 DNS、kube-proxy、CNI 三层都活着。第三类是调度与存储故意起一个带nodeSelector的 Pod确认它能落到指定节点如果接了存储类再跑一次 PVC 绑定确认Pending能在几十秒内变成Bound。# 一次跑完三类检查输出直接贴到交付记录里 kubectl get nodes -o wide kubectl get pods -n kube-system | awk NR1 || $4!Running || $5!0 kubectl create deployment verify --imageregistry.local:5000/busybox:1.36 --replicas2 -- sleep 3600 kubectl expose deployment verify --port80 --target-port80 kubectl run curl-test --rm -it --imageregistry.local:5000/curl:latest --restartNever -- \ curl -sS --max-time 5 verify.default.svc.cluster.local kubectl delete deployment verify kubectl delete svc verifyawk那行是过滤异常 Pod 的偷懒写法$4是 STATUS 列$5是 RESTARTS 列只要有不等于Running或者重启次数不为 0 的就打出来。这个过滤器比肉眼看几十行舒服得多交付前跑一遍基本能兜住八成问题。6.2 加一个本地仓库让第二次部署快一半第一次部署最慢的环节永远是镜像分发。三台机器各自docker load几十个镜像包磁盘 IO 和网络都没有并行起来。进阶做法是先在一台机器上起一个轻量 registry所有镜像推进去其它节点只 pull 不 load。# 主节点起一个本地仓库容器化方式即可 docker run -d -p 5000:5000 --restartalways \ -v /data/registry:/var/lib/registry \ --name registry registry:2 # 把离线镜像批量打上仓库 tag 并推送 for img in $(docker images --format {{.Repository}}:{{.Tag}} | grep -v none); do docker tag $img registry.local:5000/${img} docker push registry.local:5000/${img} doneregistry.local这个域名要提前写进各节点的/etc/hosts或者把镜像名统一换成IP:5000的形式否则 pull 时会解析失败。--restartalways保证 registry 容器在机器重启后自动拉起不然下次部署第一件事就是修仓库。这个改动看起来只是换了分发方式实际能把重复部署的时间压掉一半以上尤其节点数超过三台时差别非常明显。另外一个值得加进脚本的小动作是部署前后各打一次kubectl get events -A --sort-by.lastTimestamp的尾部若干行存成日志文件。出问题时不用从零开始回忆出问题前十几分钟的调度事件、拉镜像失败、探针失败都记在里面。从那以后我每次重建集群都会先拿kubeadm config images list的输出和本地镜像目录做一次 diff确认零差异再执行脚本宁可多花两分钟也不赌“应该能拉到”。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?