
简介面向需要快速搭建Kubernetes集群的运维与开发人员这是一套基于Docker容器化的Shell脚本部署方案覆盖Master与Node节点的初始化、安装、网络插件配置及集群卸载流程。脚本内置docker 24.0.7、cri-dockerd 0.3.9、Kubernetes v1.28.2等版本参数下载后仅需修改集群节点规划与软件版本信息上传至各节点并执行部署脚本即可完成环境搭建大幅减少手动插件配置的繁琐操作。资源包共7个文件以Shell脚本为核心3个sh安装脚本、公共初始化脚本与卸载脚本分工明确辅以Kubernetes配置文件、Flannel网络清单、教程地址说明及cri-dockerd RPM包整体约10.67MB。脚本版本参数清晰可见便于按需调整配套卸载脚本也能快速清理环境适合反复演练或生产初始化。资源已获953人学习适合具有一定Linux基础、希望以标准化方式落地K8s环境的读者同时提供了便捷的升级与排错入口。1. 把 K8s 从两天压到十分钟的那条 Shell 脚本到底在做什么我之前接过一个需求给一家要做国产化验证的客户现场交付一套自建 K8s机器是几台刚刚装好 Rocky Linux 的物理机没有外网只有一台能临时连网下载离线包的跳板机。最开始我心里也有点虚手工跑 kubeadm init 加配置节点少说也要一整天中间还要跟各种网络、证书、镜像源的问题缠斗。后来把一套“一键部署 shell 脚本Docker 容器化 K8s 集群部署”的思路捋清楚把 Docker 镜像预置、K8s 控制面初始化、节点加入三个环节全收进一个脚本里整个过程被压缩到十几分钟。这篇文章就讲我落这套脚本的过程以及里面真正值得投入精力收拾的细节。标题里最容易让人困惑的就是“Docker 容器化”和“K8s”的关系——很多人还在问“k8s 和 docker 区别”担心做 K8s 就不需要 Docker 了。实际工作中这两者的结合点比想象中更直白。适合看这篇文章的人是那些正在准备交付、培训或内部基线环境需要一套能重复执行、能修 bug、能解释清楚每一步在干什么的部署方案的人。是不是要引入 Ansible、Kubespray、Sealos 这类重量级工具看完你会有自己的判断。2. 先厘清 Docker 与 K8s 的真正分工脚本才不会写成“四不像”2.1 k8s 和 docker 区别一个是运行底座一个是编排平面做部署脚本前得先把这两个概念的边界划清楚否则脚本里容易出现“既要 docker 起容器又要 kubelet 管容器”这种打架的情况。K8s 本身是编排系统它管的是 Pod、Service、Deployment 这些抽象对象。而容器真正运行的时候需要一台“能跑容器”的机器环境早期这个环境依赖 Docker因为 K8s 默认通过 dockershim 调用 Docker 来启动容器。但从 K8s 1.24 起dockershim 被移除K8s 不再直接认知 Docker 了它依靠的是实现 CRIContainer Runtime Interface的容器运行时最常见的代表是 containerd。这句话怎么理解按不严格的通俗说法K8s 里可以没有 Docker但一定要有 containerd。然而现实远比理论埋人。虽然运行时换成了 containerd可你在做交付的时候集群里的镜像通常还是用 Docker 构建出来的。Docker 镜像的格式就是标准 OCI 镜像containerd 照样能加载运行。因此标题里说“Docker 容器化 K8s”我理解成两层落地含义第一节点装上 Docker用 Docker 完成镜像的拉取、tag、save 和离线打包再通过 ctr 或 crictl 导入到 containerd 里作为运行时仓库的预置。第二用 Docker 打包这个“一键部署脚本”的运行环境比如脚本里附带的一些辅助服务镜像仓库、离线 yum 源用容器跑起来避免污染客户节点。在做“一键部署”脚本时我建议把逻辑收敛成一句话用 Docker 处理镜像和辅助工具用 kubeadm 处理集群生命周期用 shell 做串场编排。三个工具的职责一旦清晰脚本结构就不会乱。2.2 为什么这种场景我用 Shell而不是先上 Ansible 或 Kubespray有人可能会问社区里 K8s 部署早就有 Kubespray、Kubeadm、Sealos 这些方案为什么还要自己写 shell我的观点是如果目标是给自己的几百台机器做标准交付Kubespray 确实香它封装了各种成熟参数。但如果是“给一个不联网机房里的几台机器快速建集群”你面对的是客户现场临时给出的 IP、主机名、版本锁定这些变量。Ansible 这类工具本身还要依赖 Python 环境、控制端和部署网络的连通性一旦客户现场的 SSH 策略比较严这些东西全都成了额外障碍。Shell 脚本的好处是“所求即所得”在任何一台 Linux 机器上都能直接跑依赖只有 bash、grep、awk 这类最基础的工具。把脚本放到一台跳板机或者直接拷到每台机器上执行过程完全透明客户审计时打开脚本就能看清楚每一步干了什么。这在不少做信创或涉密项目的场景里是个实打实的优点。当然我不反对用高级工具只是这套脚本在我的工作流里更像是“最小交付基线”。如果后面环境变大再拿它做底座外面包上 Ansible 或者继续维护成内部运维平台都比一上来就背一个大而全的工具轻快得多。2.3 脚本落地前必过的三个硬前置关闭 swap、调内核参数、同步时间写脚本之前先把 K8s 节点的系统基线处理好。这一步如果省略后面 kubeadm 预检preflight会直接报错。我见过不少人在网上搜“k8s安装部署失败卡在 kubelet 启动”最后发现是 swap 没关。这里的操作不复杂但必须写进“一键脚本”的最前面当作预检函数来用swapoff -a sed -ri s/.*swap.*/#/ /etc/fstab modprobe br_netfilter sysctl -w net.ipv4.ip_forward1 systemctl enable --now chronyd chronyc makestep为什么要做这三件事展开说一下。swap 不关kubelet 的 QoS 内存管理会不稳定Node 节点容易出现“内存压力大但回收不动”的情况。modprobe br_netfilter是为了让网桥流量经过 iptables这样后面 Pod 与 Service 的转发链路才通。systemctl enable --now chronyd解决的是时间同步问题K8s 的证书签发和有效期验证强依赖各节点时间一致这个坑前期不起眼后期证书过期排查时你会非常头疼。3. 用一条主脚本串起整条部署链路从环境检查到生成节点加入命令3.1 脚本的主体结构与状态机设计既然叫“一键部署”不是简单地把一堆命令从上到下扔进一个 .sh 文件里就完事了。真正做脚本的人会把整条链路当成一个有状态的过程来设计。我把脚本拆成四个阶段sys_prepare系统准备、runtime_prepare容器运行时准备、master_init控制面初始化、worker_join工作节点加入。每一阶段执行前会检查上一阶段是否成功失败就中止并打印排查提示。这种设计能避免你“稀里糊涂跑完一遍最后发现节点根本没起来还要从头查”。下面是我常用的脚本骨架按 bash 的规范来写便于新人阅读也便于后续维护#!/usr/bin/env bash set -o errexit set -o nounset set -o pipefail NODE_ROLE${1:-master} CLUSTER_NAMEmy-cluster function log() { echo -e \033[32m[t-$(date %H:%M:%S)] $*\033[0m } function fail() { echo -e \033[31m[*] $*\033[0m 2 exit 1 } function pre_check() { local mem mem$(free -g | awk /Mem:/{print $2}) [ ${mem} -ge 2 ] || fail 内存小于 2G无法初始化控制面 command -v kubeadm /dev/null 21 || fail 未发现 kubeadm请先安装 K8s 组件 } function prepare_runtime() { systemctl enable --now docker systemctl enable --now containerd ctr -n k8s.io images import /opt/k8s-images/images.tar } function init_master() { kubeadm init \ --image-repository ${IMAGE_REPO} \ --kubernetes-version ${K8S_VERSION} \ --pod-network-cidr ${POD_CIDR} \ --service-cidr ${SERVICE_CIDR} \ --apiserver-advertise-address ${API_SERVER_ADDR} \ --upload-certs } function gen_join_cmd() { local join_token join_token$(kubeadm token create --print-join-command) echo ${join_token} --control-plane --certificate-key $(kubeadm init phase upload-certs --upload-certs | tail -1) /opt/join-command/master-join.sh } pre_check prepare_runtime if [ ${NODE_ROLE} master ]; then init_master gen_join_cmd else bash /opt/join-command/master-join.sh fi这段代码里几个开关参数说明一下。set -o nounset会让脚本在遇到未定义变量时直接退出这能防止你在客户现场遇到“变量为空命令跑偏”的灵异问题。--image-repository用来指定内部镜像仓库地址否则 kubeadm 会去访问默认的 registry.k8s.io在无外网环境就会直接卡死。--upload-certs这个参数很关键它让控制面的证书可以被二次上传方便后续扩容控制面节点时直接复用不需要把证书手工拷来拷去。3.2 机器上没镜像怎么办用 ctr 导入兜底每台机器都手动 docker pull 再 tag 无疑是最笨的办法。场景是这样的控制面节点初始化时需要 pause、etcd、coredns、kube-apiserver 等一批镜像如果断网这些镜像一个都拉不下来你的 kubeadm init 会卡在等待镜像就绪那一步。常见做法是先把镜像在一台有网的机器上用 docker pull 拉好再 docker save 成一个 tar 文件。交付时把这个 tar 放进节点/opt/k8s-images/目录下脚本里跑一次导入# 在联网机器上预打包 docker pull registry.k8s.io/kube-apiserver:v1.28.2 docker save registry.k8s.io/kube-apiserver:v1.28.2 -o /opt/k8s-images/kube-apiserver.tar # 在离线节点上导入 ctr -n k8s.io images import /opt/k8s-images/images.tarctr -n k8s.io这个命名空间要特别留意。containerd 默认的命名空间是default但 K8s 使用的 containerd 命名空间是k8s.io如果你用ctr images list看不到 K8s 镜像多半是没加-n k8s.io。这个顺手操作很多人前面会忽略直到 Prieflight 报image not found才回去翻命令。3.3 join 命令的动态生成不给客户留“手抄 token”的土办法我平时写交付脚本最反感的是执行完 master 初始化后打印一行“请手动复制上面的 kubeadm join 命令去 worker 执行”。对于一个声称“一键”的流程这种交接方式太原始了。更好的做法是让脚本自动把 join 命令生成到指定文件worker 节点执行时直接引用它。这里有一个隐蔽点kubeadm token create --print-join-command只能生成不带动证书的 join 命令也就是只适合 worker 节点如果你想加控制面节点还得配合kubeadm init phase upload-certs或者--upload-certs时留下的 certificate-key。上面代码块里我把两个命令拼成了master-join.sh同时在生成 worker join 时用另一个只含 token 的文件即可。如果你希望更简单可以直接用kubeadm token create --print-join-command /opt/join-command/worker-join.sh一条命令把 worker 的 join 参数落盘。脚本层面上不需要过度设计关键是把生成的命令落到固定路径后面运维时有据可查。4. 把变量抽进独立配置一套脚本适配在线部署和离线交付4.1 配置文件的变量设计改这三个文件就等于改版本“一键脚本”如果写死了 IP 和版本换个环境就得重新改脚本正文这显然不合格。常见做法是把环境相关的参数全部抽到一个 install.conf 文件里脚本只负责加载和校验。我一般会这样切分# install.conf 示例 K8S_VERSION1.28.2 IMAGE_REPOharbor.internal.intra/k8s POD_CIDR10.244.0.0/16 SERVICE_CIDR10.96.0.0/12 API_SERVER_ADDR192.168.10.10 CONTAINER_RUNTIMEcontainerd OFFLINE_MODEtrue MIRROR_TAR_PATH/opt/k8s-images/images.tar脚本开头统一加载这个文件source /opt/k8s-installer/install.conf if [ ${OFFLINE_MODE} true ]; then ctr -n k8s.io images import ${MIRROR_TAR_PATH} else # 在线模式直接拉镜像什么都不做 : fi这样做的价值是交付人员到现场后只改配置文件里的 IP 和版本不需要理解脚本内部逻辑。版本升级、镜像仓库更换都变成了改变量而不是动源码。值得强调的还有POD_CIDR与SERVICE_CIDR的取值问题。POD_CIDR决定了每台 Node 上 Pod 网段的分配范围SERVICE_CIDR决定 Service 的虚拟 IP 段。两者都不能和节点所在的物理网络段重叠这个检查最好在 pre_check 阶段就加进去否则就算集群成功建起来了网络也会出现“狗咬狗”式的互访异常。4.2 离线镜像包的管理镜像清单要比脚本更早固化离线交付时钱往往会花在“镜像准备不全”这件事上。kubeadm 的好消息是它自带一个镜像列表生成命令你可以先跑一发把它当“采购清单”用kubeadm config images list --image-repository registry.k8s.io --kubernetes-version v1.28.2执行后它会列出 kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy、pause、etcd、coredns 这些镜像及对应版本。你把这串输出保存起来在联网机器上挨个 pull 再 save 就行。可能你会想用脚本自动处理这个过程while read -r img; do docker pull ${img} docker save ${img} -o $(basename ${img}).tar done /opt/k8s-installer/images.list这条循环里$(basename ${img})会把带仓库和版本号的镜像名简化成类似kube-apiserver.tar的文件名方便归档。需要提醒的是如果你换了私有镜像仓库比如harbor.internal.intra/k8s那 images.list 里也要相应加上仓库前缀否则 kubeadm init 时仍会从默认仓库找镜像导致“明明有包却拉不下来”的尴尬局面。4.3 用 namespace 组织资源脚本里最后三行要解决的问题有些交付脚本跑到节点 join 就停了从技术角度看没错但从可用性角度看很可惜。集群建好后第一件事应该是在脚本收尾阶段自动创建一批基础命名空间和默认服务账号。有个细节值得注意K8s 和 Docker 都讲“容器化”但 K8s 的命名空间namespace不是 Docker 那套资源隔离方案而是一个逻辑上的对象分组工具。在一键脚本里预留一个函数来创建 namespace能给后续业务上线节省不少手工操作。function setup_base_ns() { for ns in ops middleware business; do KUBECONFIG/etc/kubernetes/admin.conf kubectl create namespace ${ns} --dry-runclient -o yaml | \ KUBECONFIG/etc/kubernetes/admin.conf kubectl apply -f - done }这里用--dry-runclient -o yaml配合apply是为了保证重复执行脚本不会因为 ns 已存在而报错。运维习惯里把命名空间按用途划分而不是按环境划分后面配额控制ResourceQuota和网络策略都好做很多。5. 一键部署的常见问题与避坑排查这五个坑脚本里最好提前堵上5.1 Docker API 权限报错permission denied while trying to connect to the docker api这个报错在脚本执行时非常高频。现象是脚本里调用docker命令时报错提示无法连接 Docker 守护进程原因一般是当前执行用户不在 docker 用户组里或者/var/run/docker.sock的 socket 权限不对。解决方式最简单的是把执行用户加入 docker 组usermod -aG docker $USER newgrp docker但如果脚本是交付给客户执行客户可能用 root 跑此时反而是因为系统 SELinux 或 AppArmor 配置把 socket 访问拦住了可以在脚本开头检测 socket 可读性并给出明确优化提示。5.2 kubeadm init 卡在 preflight提示 swap 未关闭这个现象极其经典你明明跑了swapoff -a但 container 重启后 swap 又回来了。原因绝大多数是没改/etc/fstab系统重启时 swap 分区自动挂载回去了。解决方法是把/etc/fstab里的 swap 行注释掉同时用free -h验证不要只看当前会话的状态。这个检查写进脚本里是必须的操作不然你交付后客户一重启集群就“翻车”回头还要挣扎确认是不是脚本没跑完。5.3 CNI 插件导致的 Pod 跨节点不通网段冲突是重灾区问题表现是 Pod 能创建但跨 Node 的 Pod 之间 ping 不通。我为这事查了不知道多少个小时的 iptables 和路由表最终定位到原因是POD_CIDR与物理机所在 VLAN 段重叠导致一部分流量被当成了局域网流量处理。解决方式是在脚本里加上网络段冲突预检使用ipcalc或者 awk 简单比对一下三个网段有没有交集宁可提前中止也不要让客户拿一个残废集群。5.4 token 过期worker 节点怎么都 join 不进集群现象worker 节点执行 join 命令提示 token 过期或者被提示 certificate 校验失败。原因是 kubeadm 创建的 token 默认 24 小时过期你如果隔了一天再扩容节点原命令自然失效。解决思路是脚本里加一条 token 重建逻辑控制面节点上重新执行kubeadm token create --print-join-command生成新的 join 命令再复制给 worker。常见的错误是有人直接改系统时间想“续命”这种土方法千万别用在生产里K8s 的证书体系复杂得多时间一跳反而引入新故障。5.5 重复执行脚本导致加入过的节点再执行报错现象是重新跑安装脚本时worker 节点提示节点已存在或者老证书冲突。原因是脚本没有做幂等处理。解决方式是每条基础命令前加状态检查对 kubeadm 这种初始化工具要先跑kubeadm reset -f清掉旧状态再执行 join。关于 reset 这步多说一句它会把之前生成的证书和 etcd 数据都清掉只适合确定要重装的节点不能作为日常维护手段。if KUBECONFIG/etc/kubernetes/admin.conf kubectl get node $(hostname) 2/dev/null | grep -q Ready; then echo 节点已加入集群跳过 join else bash /opt/join-command/worker-join.sh fi6. 把“一键脚本”再往前推半步版本锁定、健康自检与可回滚设计脚本跑通一次不难难的是跑十次都不出意外。我平时做交付时会额外给脚本加三个东西你可以直接抄进自己的方案里。第一件事是版本文件锁。在/opt/k8s-installer目录放一个version.txt里面记录 k8s 版本、脚本版本、镜像包日期、容器运行时版本。每次执行开始时比对当前节点已安装版本和期望版本不一致就明确提示“需要重置环境”。这能帮你躲过“客户环境里残留旧版本导致新集群行为怪异”的玄学问题。第二件事是健康自检。脚本在集群初始化完成后写一个healthcheck.sh里面检查核心组件状态、节点状态、Pod 是否 Running。这些命令可以作为客户验收的证据也方便后期排障。kubectl get nodes -o wide kubectl get pods -n kube-system -o wide kubectl get cs curl -ks https://127.0.0.1:6443/healthz第三件事是保留“后悔药”。把执行日期、版本、关键配置备份到/opt/k8s-installer/backup下包括/etc/kubernetes、/etc/containerd/config.toml、/etc/cni/net.d。未来如果节点配置调整出问题能快速对比恢复。我自己的习惯是每次改完脚本一定会找两台一次性虚拟机从零跑一遍确认没有环境残留现象后才敢拿给客户。一次“一键部署”背后往往要付出比手工部署更多的调试精力但换来的是后续十几次复现的稳定。希望这篇关于 Docker 容器化 K8s 集群一键脚本的实战笔记能帮到你既能少走几年我走过的弯路也能把它接进你自己的交付标准里。本文还有配套的精品资源点击获取