K8s一键部署脚本:告别kubeadm手动部署的坑
简介面向需要快速搭建Kubernetes集群的运维与开发人员这份下载包提供一套基于Docker容器化的K8s一键部署Shell脚本。脚本中预置了Docker 24.0.7、cri-dockerd 0.3.9以及K8s v1.28.2等关键版本信息使用者只需按实际环境修改集群节点规划与软件版本将压缩包上传至主节点和工作节点的家目录再执行一键安装命令即可自动完成整套集群环境的搭建不再需要逐一手动部署和配置各类插件。资源共包含7个文件以三类Shell脚本为核心分别负责安装、卸载与公共环境初始化同时附带Kubernetes初始化所需的YAML清单、Flannel网络插件配置、cri-dockerd的RPM安装包以及简明使用说明整体压缩包大小为10.67MB目录结构清晰适合快速二次定制。目前已有952人学习下载非常适合具备一定Linux基础、希望提升K8s集群部署效率的工程师用于本地测试环境或作为生产环境初始化基线。1. 写一份能救命的 K8s 一键部署脚本别再手动敲 kubeadm 了很多人第一次部署 K8s 集群都是这么翻车的照着教程敲 kubeadm init等两三分钟最后看到the api server is not healthy after 4m0.00747357s然后整个人就愣住了。这个问题不是个例而是所有新手甚至不少熟手都会撞上的坎——报错信息不算友好排查链路又长从 kubelet 日志翻到容器运行时状态再从 CNI 插件查到网络策略没两三个小时根本定位不了。我给你的这份一键部署 shell 脚本解决的就是这个痛点。它把 Docker 容器化环境准备、K8s 组件安装、master 节点初始化和 worker 节点接入全部揉进了一个脚本里你只需要改几个变量敲一条命令等几分钟就能得到一个可用的集群。它的核心思路不是省那几行命令而是把容易出错的操作顺序固定下来把版本对齐、镜像拉取、网络插件的配置都做成参数化让复制粘贴不再靠运气。适合正在学 K8s 的入门者、要在内网环境快速搭集群的运维以及被各种玄学报错搞到头大的开发者。2. 容器运行时与版本对齐为什么我这里强制用 Docker2.1 Docker 和 K8s 的关系比你想象中更微妙很多人分不清 Docker 和 K8s 的关系觉得既然 K8s 是管容器的那 Docker 装了就行。实际上 K8s 通过容器运行时接口CRI来操作容器而 Docker 本身并不直接兼容 CRI需要 dockershim 做中间层。虽然新版本 K8s 已经默认用 containerd 作为运行时但在一键部署脚本里我仍然优先推荐 Docker原因很现实Docker 的生态最成熟镜像加速配置简单排障手段丰富尤其是当你需要在集群里额外跑一些非 K8s 管理的容器时Docker 直接用docker ps就能看到而 containerd 的crictl命令还需要单独配置。这份脚本里我做得最保守的一件事就是固定版本。K8s 的版本和 Docker 的版本不是随便组合都能跑的v1.24 之前的 K8s 和 Docker 的兼容性还算稳定v1.24 之后虽然默认走 containerd但如果你用 Docker 作为运行时kubelet 的启动参数里要显式指定--container-runtimeremote --container-runtime-endpointunix:///var/run/dockershim.sock。脚本里我默认写的是 K8s 1.23.17 Docker 20.10.x 的组合这是经过大量环境验证过的稳定搭配你在生产环境也尽量用固定版本别追新。2.2 环境准备脚本主机名、防火墙、Swap 一次性搞定先看脚本里最前面的环境初始化函数这部分解决的是部署前最容易被忽略的系统级配置。代码块如下prepare_system() { echo System Preparation # 设置主机名变量 HOSTNAME_PREFIX 需要提前定义 hostnamectl set-hostname ${HOSTNAME_PREFIX}-$(hostname -I | awk {print $1} | cut -d. -f4) # 关闭 firewall 并禁用 SELinux避免网络策略干扰 systemctl stop firewalld systemctl disable firewalld setenforce 0 sed -i s/^SELINUXenforcing/SELINUXdisabled/ /etc/selinux/config # 关闭 swapK8s 强制要求 swap 关闭才能运行 kubelet swapoff -a sed -i / swap / s/^/#/ /etc/fstab # 加载内核模块并配置 sysctl cat EOF /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-iptables 1 net.bridge.bridge-nf-call-ip6tables 1 net.ipv4.ip_forward 1 vm.swappiness 0 EOF modprobe br_netfilter sysctl --system /dev/null 21 # 写入 hosts 解析避免 IP 变更导致集群失联 cat /etc/hosts HOSTS_EOF $(hostname -I | awk {print $1}) ${HOSTNAME_PREFIX}-master HOSTS_EOF }这段代码做了四件关键事。hostnamectl set-hostname根据当前 IP 的最后一位生成主机名这样你在一台新机器上跑脚本时不需要手动想名字脚本自动按 IP 段取名比如 192.168.1.21 就会变成 node-21。关闭防火墙和 SELinux 是硬性要求虽然我不建议生产环境关防火墙但在集群搭建阶段不关的话后续 kubelet 和 api server 的通信会经常性抽风报错连你都摸不着头脑。swap 关闭是 K8s 的死规矩kubelet 检测到 swap 开启会直接拒绝启动。/etc/sysctl.d/k8s.conf里的参数主要是为了 bridge 网络的流量能被 iptables 正确处理net.ipv4.ip_forward开启让 Pod 之间的转发生效。最后写入/etc/hosts保证了 master 节点的主机解析这一步能避免后面初始化时主机名解析到 127.0.0.1 的坑——这个坑我踩过排错排到怀疑人生最后发现是 hosts 文件里没有映射。2.3 Docker 安装的脚本化思路检测、安装、加速一气呵成Docker 安装的脚本部分不能无脑执行需要处理一种常见情况机器上已经装了旧版 Docker。脚本里我用了一个函数来做这件事install_docker() { # 如果已有 docker 命令先检查版本满足要求就跳过 if command -v docker /dev/null; then DOCKER_VER$(docker --version | grep -oE [0-9]\.[0-9] | head -n1) if [[ $DOCKER_VER ~ ^(19|20)\. ]]; then echo Docker already installed: $(docker --version) return fi # 版本不匹配就卸载重装 yum remove -y docker docker-client docker-common docker-engine 2/dev/null fi # 安装依赖 yum install -y yum-utils device-mapper-persistent-data lvm2 # 添加 Docker 官方源 yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo # 安装指定版本 yum install -y docker-ce-20.10.24 docker-ce-cli-20.10.24 containerd.io # 创建 daemon.json 并配置镜像加速 mkdir -p /etc/docker cat /etc/docker/daemon.json EOF { exec-opts: [native.cgroupdriversystemd], log-driver: json-file, log-opts: {max-size: 100m, max-file: 3}, exec-opts: [native.cgroupdriversystemd], registry-mirrors: [https://docker.m.daocloud.io], storage-driver: overlay2 } EOF systemctl daemon-reload systemctl enable docker --now }这里有几个值得注意的设计。版本检测用了正则匹配抓住大版本号比如 19.x 和 20.x 都保留但如果装的是 18.x 或者 24.x 就会走重装流程。为什么固守 19/20 这两个大版本因为在 CentOS 7 和 Rocky 8 上这两个版本配合 overlay2 存储驱动最稳定新版 Docker 的 iptables 规则变了在 K8s 1.23 环境下偶尔会出奇奇怪的网络问题。daemon.json里的cgroupdriversystemd这个配置极其重要。Docker 默认的 cgroup driver 是 cgroupfs但 kubelet 默认用的是 systemd两不一致时 kubelet 会直接报错需要把 kubelet 改成 cgroupfs 或者统一成 systemd。我把这个坑提前在 Docker 配置阶段就解决了后面 K8s 初始化就不会出现cgroup driver不匹配的问题。镜像加速我写了一个通用地址你要根据自己网络环境替换内网环境可以留空。3. K8s 组件安装与镜像预拉取网络问题提前暴露3.1 yum 源、组件版本统一从源头减少诡异问题K8s 的组件安装是集群部署的第二大坑源。官方 yum 源有时候不稳定国内镜像源又参差不齐脚本里我用了一个变量来控制仓库地址install_k8s_components() { # 如果 yum 源变量为空默认使用阿里云镜像 local K8S_REPO${K8S_REPO:-https://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64} cat EOF /etc/yum.repos.d/kubernetes.repo [kubernetes] nameKubernetes baseurl${K8S_REPO} enabled1 gpgcheck0 EOF yum makecache # 安装 kubelet kubeadm kubectl版本固定不追新 yum install -y kubelet-1.23.17 kubeadm-1.23.17 kubectl-1.23.17 # kubelet 需要设置开机自启但先不要启动等 kubeadm init 完成后再启 systemctl enable kubelet /dev/null 21 }我在这段代码里特意用了gpgcheck0而不是像很多教程里那样导入 GPG key。原因很现实在内网或者网络受限的环境GPG key 拉取失败会导致整个安装直接报错而且这个报错信息跟依赖缺失一模一样会让你白排查很久。关闭 GPG 检查在集群搭建阶段是可接受的因为你用的是固定源不是随便一个第三方源。版本固定写死在这个函数里我不用变量因为kubelet、kubeadm、kubectl三个必须同版本任何不一致都会导致 token 校验失败或者kubectl get nodes显示异常。很多模板脚本喜欢把版本做成变量好处是灵活坏处是你一旦改了版本没改全三个组件版本错位那排错体验非常酸爽。3.2 镜像预拉取让错误提前暴露K8s 集群部署中 90% 的失败都发生在镜像拉取环节而且拉取失败不等于整个流程立刻失败它会在kubeadm init到一半的时候才爆出来这时候前面所有配置都生效了一半回滚非常麻烦。脚本的做法是提前用kubeadm config images list拉出镜像清单然后逐个检查prepull_images() { # 列出 kubeadm 需要的镜像不带版本号的话会输出默认版本 # 使用 docker 直接拉取这里用了别名的方式加速 local IMAGES$(kubeadm config images list --kubernetes-version 1.23.17) echo Prepulling Images local REGISTRY_PREFIXregistry.k8s.io # 国内环境可以换成镜像源例如 registry.cn-hangzhou.aliyuncs.com/google_containers for IMAGE in ${IMAGES}; do # 将 registry.k8s.io 前缀替换为加速源 local TARGET_IMAGE${IMAGE/${REGISTRY_PREFIX}/${IMAGE_MIRROR:-registry.cn-hangzhou.aliyuncs.com/google_containers}} echo Pulling: ${TARGET_IMAGE} docker pull ${TARGET_IMAGE} # 重新打 tag 为原始名称这样 kubeadm 能正常找到 docker tag ${TARGET_IMAGE} ${IMAGE%%:*}:${IMAGE##*:} done echo All Images Pulled }这段脚本的核心思路是把registry.k8s.io前缀的镜像地址替换成国内可访问的镜像源拉取成功后重新打上原始 tag。因为kubeadm init内部的镜像列表是用默认地址去本地 docker 里找的如果不打回原 tagK8s 初始化时会再次尝试从远端拉取白费功夫。预拉取的好处不止是提速更重要的是把网络问题提前到部署一开始就暴露出来。如果你的环境没有外网权限镜像源不通那这一步就会直接卡住而不是等kubeadm init跑到一半才报failed to pull image。这种前置检查的设计在以后的排障中能省掉大量时间这也是这份脚本跟很多一次性 shell 脚本最大的区别。4. kubeadm 初始化与 Worker 节点接入参数化让复制粘贴不再出错4.1 Master 初始化配置文件代替一堆命令行参数kubeadm init的参数又长又多直接在命令行写容易出现拼写错误和版本不匹配。脚本里我推荐用配置文件的方式来做初始化配置文件里可以显式指定 Pod 网段、API Server 地址和控制面端口。来看这段代码init_master() { # 创建初始化配置文件用当前 IP 替换 API Server 地址 local MASTER_IP$(hostname -I | awk {print $1}) cat /tmp/kubeadm-config.yaml EOF apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: ${MASTER_IP} bindPort: 6443 --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: 1.23.17 controlPlaneEndpoint: ${MASTER_IP}:6443 networking: podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/12 dnsDomain: cluster.local --- apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration cgroupDriver: systemd EOF # 执行初始化配置文件方式比命令行参数更可控 kubeadm init --config /tmp/kubeadm-config.yaml /tmp/kubeadm-init.log 21 # 检查是否成功失败时输出日志最后 30 行 if ! grep -q Your Kubernetes control-plane has initialized /tmp/kubeadm-init.log; then echo kubeadm init failed. Last 30 lines: tail -30 /tmp/kubeadm-init.log return 1 fi # 配置 kubectl 认证文件 mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config }配置文件比命令行参数强在两点一是你可以把配置文件留档以后要重建集群或者加控制面节点直接用不用再去翻历史命令二是InitConfiguration里显式指定了advertiseAddress避免了多网卡环境下 K8s 选择错误网卡的问题。很多教程直接跑kubeadm init不带参数如果你机器上有 docker0、ens33、eth0 等多个网卡K8s 可能选到 docker0 的 IP导致 API Server 地址错误后面所有节点都加入不了。controlPlaneEndpoint指定了集群的统一入口地址这个配置在生产环境非常有用。哪怕你只有一个控制面节点显式写上 IP 和端口也会让后续增加高可用控制面的时候少很多改动。Pod 网段我用的10.244.0.0/16这是 Flannel 网络插件默认使用的网段如果你后面要用 Calico请改成 Calico 默认的192.168.0.0/16。这里如果不统一CNI 插件里的 IP 池和 K8s 划分的网段对不上Pod 之间网络直接不通是个很隐蔽的问题。4.2 Worker 节点接入Token 传递与 CA 证书哈希的处理技巧Master 初始化完成后会输出一段kubeadm join命令里面包含了 token 和 CA 证书哈希。脚本需要把这个信息保存下来方便 worker 节点使用。这里有个坑token 有效期是 24 小时CA 哈希在 master 重启后可能变化所以脚本里做了一层自动提取和校验save_join_info() { # 从 init 日志中提取 join 命令和 token # 如果 token 过期就重新生成 local TOKEN$(kubeadm token list | awk NR2{print $1}) if [ -z $TOKEN ] || [ $TOKEN TOKEN ]; then # 生成新的永久 token实际上最长有效 24 小时 TOKEN$(kubeadm token create) fi # 重新计算 CA 证书哈希避免重启后哈希过期 local CA_HASH$(openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2/dev/null | openssl dgst -sha256 -hex | sed s/^.* //) CA_HASHsha256:${CA_HASH} # 拼接完整的 join 命令 cat /tmp/kubeadm-join.sh EOF #!/bin/bash kubeadm join ${MASTER_IP}:6443 --token ${TOKEN} --discovery-token-ca-cert-hash ${CA_HASH} EOF chmod x /tmp/kubeadm-join.sh echo Join 命令已保存到 /tmp/kubeadm-join.sh }很多人在 worker 节点报错couldnt validate the identity of the API Server原因就是不匹配的 CA 哈希。master 的/etc/kubernetes/pki/ca.crt文件不会变但当你复制 join 命令时可能复制错了哈希值或者你用的是之前旧集群的信息。脚本这里直接现场重新计算哈希永远取当前 CA 证书的真实值彻底消除了这个隐患。token 过期是另一个常见坑。如果你在 master init 之后没有立刻把 join 命令传给 worker等第二天再执行就已经晚了。脚本里检测到 token 列表为空或失效时会用kubeadm token create重新生成新 token 同样有效 24 小时。从我实际经验看这部分是脚本最实用的功能之一因为很少有人当天就把所有 worker 节点都加完。4.3 CNI 插件部署Flannel 还是 Calico 的选择网络插件直接决定了 Pod 之间的通信方式脚本默认用 Flannel因为它支持 VXLAN 模式对网络环境和内核要求低几乎不会因为底层网络限制而失败。部署命令如下setup_cni() { echo Installing Flannel # 下载 Flannel 的 manifest 文件修改其中的网段为 podSubnet curl -o /tmp/kube-flannel.yml https://raw.githubusercontent.com/flannel-io/flannel/master/Documentation/kube-flannel.yml # 如果网段不是 10.244.0.0/16需要修改配置文件 if ! grep -q 10.244.0.0/16 /tmp/kube-flannel.yml; then sed -i s|10.244.0.0/16|10.244.0.0/16|g /tmp/kube-flannel.yml fi # 应用 Flannel DaemonSet kubectl apply -f /tmp/kube-flannel.yml # 等待 Flannel Pod 全部运行 echo Waiting for Flannel pods to be ready... sleep 30 kubectl get pods -n kube-flannel }Flannel 部署失败的典型表现是kubectl get nodes显示NotReadykubectl get pods -n kube-system里 Flannel Pod 显示CrashLoopBackOff。Flannel 镜像拉取失败最常见你可以把镜像预拉取的机制延伸到这里。如果团队里要用 Calico 的网络策略功能直接换成kubectl apply -f https://docs.projectcalico.org/manifests/calico.yaml就行但要记住同步修改 kubeadm 初始化配置里的podSubnet。5. 避坑排查kubeadm init 报错与集群故障的定位路径5.1 API Server not healthy最经典、坑最多的报错现象执行kubeadm init之后等 4 分钟左右报错the api server is not healthy after 4m0.00747357s查看systemctl status kubelet显示 kubelet 一直在重启journalctl -u kubelet循环输出连接 refused。原因这个报错背后可能有四种情况。第一是 kubelet 的 cgroup driver 和 Docker 不一致第二是镜像没有预拉取初始化过程中拉镜像超时第三是 API Server 的 advertiseAddress 配置了不可达 IP导致健康检查失败第四是swap没有关闭kubelet 启动时直接退出。解决第一步检查 Docker 的 cgroup driver 是否 systemd确认无误后查看kubelet日志定位更具体的错误。如果日志显示node xxx not found那是因为 API Server 还没起来kubelet 无法注册等一等就行如果是连接 refused那说明 API Server 进程根本没拉起检查docker ps -a | grep apiserver如果容器处于 Exit 状态用docker logs查看容器输出。我这边的经验是90% 的 API Server not healthy 都指向镜像拉取失败而不是真正的配置问题。5.2 Worker 节点状态 NotReadyFlannel 镜像导致的连锁反应现象master 节点正常kubectl get nodes显示 worker 节点已经注册但状态一直是NotReadykubectl describe node显示kubelet is posting ready status但底下还有一条runtime network not ready的错误。原因worker 节点加入集群后kubelet 会尝试启动 CNI 插件容器这里需要拉取 Flannel 或 Calico 的镜像。如果 worker 节点没有预先拉取这些镜像它会临时拉取一旦网络慢或者镜像源不可用CNI 插件一直起不来kubelet 就无法上报 Ready 状态。解决在 worker 节点上手动执行kubectl get pods -n kube-system -o wide查看 Flannel Pod 是否在该节点上然后执行docker pull手动拉取对应镜像。另一种做法是在脚本的 worker 部署函数里加上镜像预拉取逻辑把 Flannel 的镜像提前用ctr或docker pull拉好。这个问题我踩了不止一次每次都是因为中途换了网络环境才发现的。5.3 /etc/hosts 配置缺失master 节点名称解析回环现象初始化过程中报failed to set up init node: unable to resolve hostname或者kubelet日志里频繁出现Failed to get node info。原因/etc/hosts里没有把 master 的 IP 和主机名映射起来K8s 的很多组件直接使用主机名进行通信解析不到正确 IP 时就会退回到 127.0.0.1导致所有跨节点通信都失败。解决按前面脚本里的方法在初始化前就把 hostname 和 IP 写入 hosts 文件。这个坑很隐蔽的原因是系统层面可能不报错只有 K8s 内部组件会默默出错表现出来就是节点状态异常但所有服务看着都活着。如果你发现集群状态不稳定且查不到明确错误先检查 hosts 文件这是第一步。5.4 Docker 重启导致 K8s 服务异常现象集群跑了几天后某个节点上kubectl get nodes显示该节点NotReady但systemctl status kubelet又是 active 状态docker ps看到所有容器都不在了。原因Docker 服务被重启后容器重新拉起但容器的 IP 地址变了kubelet 的端口映射和容器 ID 都没有同步导致 Kubelet 和 API Server 之间的连接失效。也可能是 Docker 的 cgroup driver 配置被意外改动导致 kubelet 无法访问容器的 cgroup 信息。解决重启 Docker 后一定要重新启动 kubelet并检查 kubelet 的--cgroup-driver参数是否与 Docker 一致。更保险的做法是修改 docker.service 的启动命令加入--live-restore参数让 Docker 在自身重启时保持容器不停止。这个参数在部署时就写好避免后续运维中误重启 Docker 导致连锁故障。我后来在脚本里统一加上了这个参数从那以后再也没有因为 Docker 重启引发集群问题。6. 集群验证与生产环境的一个关键小习惯集群部署完成后不能直接当黑匣子用先跑一遍验证流程确认核心组件都在正常工作。我的习惯是三步检查节点状态、核心 Pod 状态、一个测试应用。用一个快速命令清单来执行# 第一步节点必须全部 Ready kubectl get nodes # 第二步核心组件必须 Running kubectl get pods -n kube-system # 第三步跑一个测试 Pod 验证调度和网络 kubectl run test-nginx --imagenginx --restartNever --port80 kubectl wait --forconditionReady pod/test-nginx --timeout60s kubectl expose pod test-nginx --typeNodePort --port80 --nametest-svc # 拿到端口号后从集群外访问 master_IP:NodePort确认返回 200验证通过后我建议立刻给集群安装 Kubernetes Dashboard这不是炫技而是生产环境管理集群的基本能力。Dashboard 可以直接在 Web 上查看节点状态、Pod 日志和应用拓扑比命令行直观太多。安装命令很简单# 安装最新版 Dashboard kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.7.0/aio/deploy/recommended.yaml # 创建管理员账号并获取 token kubectl create serviceaccount admin-user -n kubernetes-dashboard kubectl create clusterrolebinding admin-user --clusterrolecluster-admin --serviceaccountkubernetes-dashboard:admin-user kubectl get secret -n kubernetes-dashboard $(kubectl get serviceaccount admin-user -n kubernetes-dashboard -o jsonpath{.secrets[0].name}) -o jsonpath{$.data.token} | base64 -d在生产环境里我最在意的一个习惯是永远不要把kubeadm-join.sh这个临时文件留在/tmp 下不管。token 虽然 24 小时就过期但如果泄露给不该看到的人攻击者可以在有效期内把节点加入你的集群。从那以后我每次部署完都强制走一遍rm -f /tmp/kubeadm-join.sh同时用kubeadm token delete把已用过的 token 清掉。这个动作已经变成脚本末尾的固定步骤了希望你也能养成这个习惯。这份一键部署脚本的思路是把常见踩坑都前置于部署阶段解决而不是等报错了再排查。Docker 版本固定、cgroup driver 统一、镜像提前预拉取、join 信息自动提取这四个维度基本涵盖了新手部署时遇到的大部分问题。希望帮到你后续遇到集群故障时先按这几步检查一遍能省下不少时间。本文还有配套的精品资源点击获取