最近在给内部一个模拟项目搭 k8s 环境时,我连续在几台机器上重复敲同样的命令,第二遍就意识到这套安装必须脚本化。标题里说的“k8s和harbor脚本安装的一系列操作”,其实就是我这次做的一整套方案:从系统初始化、Kubernetes 集群搭建,到 Harbor 镜像仓库的独立部署和联调,全部用脚本串起来。最终效果是原来折腾一整天的环境,现在只需要十几分钟;更重要的是出问题以后可以重跑、可以翻日志、可以离线部署。这篇文章就是我自己的完整笔记,把拆分思路、关键参数、脚本片段和踩过的坑都写下来,适合准备用脚本方式批量部署 Kubernetes 和私有仓库的运维或开发同学参考。
1. 先谈整体设计:为什么我把集群安装拆成五类脚本
1.1 手工安装的痛点和脚本化的收益
先说痛点。我第一次装这套环境时,顺序是:先初始化系统,再装容器运行时,然后初始化控制平面,接着让工作节点加入,最后部署仓库。每一个环节都有不少命令,而且有些命令必须等前面某个服务 ready 才能继续,中间夹杂着改配置、重启服务、确认状态。一个人盯着一台机器还凑合,一旦换成 4-5 台节点,或者需要测试环境重新搭建一遍,手工操作就会漏掉细节点。比如少关一次 swap、selinux 策略没改、容器运行时和 kubelet 的 cgroup driver 没对齐,后面 kubelet 就直接起不来,而且日志信息很误导人。
把这些操作脚本化,好处是显而易见的:第一,可重复,同一套脚本可以在任何一台干净的机器上还原出同样的环境;第二,可排错,每个阶段打日志,失败时能看到在哪一步挂掉,而不是靠人脑回忆;第三,可离线,把所有安装包和镜像提前准备好,内网环境也能完整装起来。我这次把整套安装拆成了五类脚本,并不是为了炫技,而是让每段脚本只负责一个清晰边界,这样任何一步出问题都能快速定位。
1.2 安装阶段的拆解与脚本目录约定
我给你看下最终脚本目录的布局,后面每个章节基本就是沿着这个目录讲:
install-k8s-harbor/ ├── 00-init-sys.sh # 系统初始化:swap、防火墙、内核参数 ├── 01-install-containerd.sh # 容器运行时 containerd 安装与配置 ├── 02-control-plane.sh # 控制平面 kubeadm init + CNI 部署 ├── 03-join-node.sh # 工作节点加入 ├── 04-harbor/ │ ├── harbor-offline-installer-v2.10.0.tgz # Harbor 离线安装包 │ ├── harbor.yml # Harbor 配置模板 │ └── install-harbor.sh # Harbor 独立脚本 ├── 05-fullcheck.sh # 安装完成后的自检脚本 └── logs/ # 所有执行日志统一落盘我刻意把系统初始化、运行时安装、控制面初始化、节点加入、Harbor 部署拆开,原因有两个。一是职责单一:系统初始化脚本在每台机器上都要跑,而控制面初始化只有第一个 master 节点跑;如果写成一个巨型脚本,跳流程会非常别扭。二是参数传递简单:main master 节点初始化完成后,会把 token、hash、certificate-key 写到一个共享文件join-command.txt里,工作节点脚本读取这个文件后自动执行,不需要人在终端之间复制粘贴。
这个阶段的实践结论是:不要试图写一个“一键装好所有东西”的保姆脚本,除非你永远只部署一台机器。凡是涉及多节点、多角色的安装,宁可多拆几个脚本,把“什么时候在哪台机器上跑哪个脚本”作为部署文档的一部分,执行起来反而更可靠。
2. 系统初始化脚本:swap、防火墙和 cgroup driver 不能各装各的
2.1 版本组合和资源规划要固定下来
脚本安装最容易出现的问题,反而是“看起来一样,实际版本不一样”。我这次直接把版本组合固定在脚本里,避免不同节点拉取到不一致的组件:
| 组件 | 版本 | 说明 |
|---|---|---|
| Kubernetes | v1.29.x | 控制面和工作节点版本保持一致 |
| containerd | 1.7.x | 作为 k8s 容器运行时,替代 docker |
| Harbor | v2.10.x | 离线包自带镜像和 docker-compose 编排 |
| CNI 插件 | flannel v0.24.x | 简单、够用,内网环境首选 |
资源上我按最小可用配置估算:控制面节点 4C/8G,工作节点 4C/8G,Harbor 单独放一台 4C/8G 的机器,数据盘预留 200G。注意 Harbor 如果没有独立数据盘,镜像和数据库很快会把系统盘占满,这一点后面我会再讲。
2.2 初始化脚本里每行配置背后的原因
系统初始化脚本是所有节点的第一步,它做的不只是“关掉多余的东西”,更重要的是让操作系统满足 kubelet 和 containerd 的运行要求。下面是我实际脚本中的核心片段:
#!/usr/bin/env bash set -o errexit set -o nounset set -o pipefail init_system() { # 关闭 swap。kubelet 在默认配置下对 swap 非常敏感, # 虽然新版支持 swap 运行,但生产环境关闭 swap 仍是主流做法。 swapoff -a sed -i '/ swap / s/^/#/' /etc/fstab # 内网测试环境直接关闭防火墙和 selinux,省事。 systemctl stop firewalld || true systemctl disable firewalld || true setenforce 0 || true sed -i 's/^SELINUX=enforcing/SELINUX=disabled/' /etc/selinux/config # 加载内核模块 modprobe overlay modprobe br_netfilter # 让 bridge 流量经过 iptables,这是 k8s 网络插件工作的前提 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 # 时间同步,证书校验和集群心跳都依赖它 systemctl enable chronyd && systemctl start chronyd }每一条配置背后都有对应的问题:不关 swap,kubelet 经常报failed to run kubelet;不开br_netfilter,CNI 插件创建的网桥流量没办法被 iptables 规则处理,NodePort 和跨节点 Pod 通信就会异常;ip_forward不开,Pod 出网直接不通。时间不同步的坑更隐蔽,kubeadm 生成的证书有效期是基于当前时间的,节点间时间差太大,apiserver 和 kubelet 之间的 TLS 握手就会出现证书验证失败。
这套初始化脚本里还有一个容易被忽略的变量:内核参数net.ipv4.conf.*.rp_filter。如果你的机器默认开了严格反向路径过滤,并且网络结构比较复杂,CNI 插件建的 veth 可能被内核直接丢弃报文。遇到跨节点 ping 不通时,先查这一项。
3. 控制平面安装脚本:kubeadm init 的参数必须提前算好
3.1 初始化参数与默认网段冲突
控制面脚本的核心是kubeadm init,参数看起来不多,但每一个都关系到后续网络、证书和高可用。我的脚本里这样写:
K8S_VERSION="v1.29.6" API_ENDPOINT="192.0.2.10" # 主 master 内网 IP 或负载均衡 VIP POD_CIDR="10.244.0.0/16" # 必须和你使用的 CNI 默认网段一致 SERVICE_CIDR="10.96.0.0/16" IMAGE_REPO="<你的内网镜像仓库地址>" # 或用云厂商的公开加速地址 kubeadm init \ --kubernetes-version=${K8S_VERSION} \ --apiserver-advertise-address=${API_ENDPOINT} \ --control-plane-endpoint=${API_ENDPOINT} \ --pod-network-cidr=${POD_CIDR} \ --service-cidr=${SERVICE_CIDR} \ --image-repository=${IMAGE_REPO} \ --upload-certs这里最需要注意的是--pod-network-cidr。以 flannel 为例,它的网络模型默认使用的 Pod 网段就是10.244.0.0/16,如果你在 init 时把 Pod 网段写成另一个值,CNI 插件下发配置后节点网络就跟声明不一致,严重情况下 Pod 一直处于 ContainerCreating。我见过有人随手抄了一个10.32.0.0/16的配置,结果换一套网络插件就不通。所以我会先把所有机器内网网段、Pod 网段、Service 网段列在一个表格里,脚本通过读同一个配置文件来取值,而不是散落在各个脚本里。
--image-repository也建议提前决定。如果确实无法直接访问默认镜像源,就把参数指到内部同步好的镜像仓库或云服务商提供的公开加速地址。这一步千万不要偷懒,否则kubeadm init会在拉取 sandbox 镜像时卡很久,最后因为网络超时失败。
3.2 从 init 到 CNI 就绪的脚本编排
初始化成功后,脚本会自动生成工作节点和管理员 kubeconfig。这里要做的关键动作是:把 join 命令保存为共享文件,同时把管理员配置放到当前用户目录下,方便用 kubectl 操作:
mkdir -p /root/.kube cp -i /etc/kubernetes/admin.conf /root/.kube/config chown $(id -u):$(id -g) /root/.kube/config # 生成供后续节点加入使用的命令 kubeadm token create --print-join-command > /root/join-command.txt echo "KUBELET_EXTRA_ARGS=--node-ip=${API_ENDPOINT}" >> /etc/sysconfig/kubelet然后部署网络插件。flannel 的 yaml 是标准内容,但我在脚本里做过一次调整:因为集群节点不止一台,且未来可能要换成其它 CNI,所以把 yaml 中的镜像地址做了一层 sed 替换,统一走内网镜像仓库。
kubectl apply -f kube-flannel.yml # 等待 flannel 相关 pod 就绪 kubectl -n kube-flannel rollout status ds/kube-flannel \ --timeout=120s || true等待 Pod 就绪这一步一定要用rollout status或等待循环,而不是sleep 30了事。网络插件没起来时kubectl get nodes看到节点都是 NotReady,你在上一个阶段跑得过快,后面的健康检查就会误判失败。我习惯在所有关键动作之后写一个等待函数,比如:
wait_ready() { for i in $(seq 1 60); do if kubectl get nodes | grep -q ' Ready '; then break fi sleep 5 done kubectl get nodes }控制面脚本跑完,目标状态是:kubectl get nodes能看到 master 节点为 Ready,kube-system和kube-flannel命名空间的核心 Pod 全部 Running。
4. 工作节点加入脚本:token 过期和重复执行这两个问题绕不开
4.1 token、hash 和 certificate-key 如何跨脚本传递
工作节点加入集群,核心只有一条命令:kubeadm join ...。但这条命令没法直接手工粘贴地持续用,因为 token 的默认有效期是 24 小时,而通过--print-join-command生成的命令里同时包含了 token 和 CA 证书 hash,如果主 master 在初始化后长时间没有加入工作节点,这条命令会过期。
我的处理方式是:不要只用一次 join 命令,而是在工作节点脚本里加入“重新获取 join 参数”的逻辑。如果join-command.txt存在,先判断集群里是否已经有这个节点;如果没有,允许工作节点脚本通过一个预共享的 token 重新生成 join 参数:
if [ ! -s /root/join-command.txt ]; then kubeadm token create --print-join-command > /root/join-command.txt fi对于多 master 高可用场景,--print-join-command生成的命令还要附带--certificate-key,这个参数用于解密由--upload-certs上传的控制面证书。默认这个密钥也有一段时间有效期,我建议把它和 join 命令写到同一个文件,其它控制面节点加入时直接追加--control-plane --certificate-key参数执行。
4.2 加入脚本怎么做成幂等
“幂等”是脚本化安装比手工安装强得多的地方。工作节点脚本最怕的是:你执行到一半发现 join 失败,修复后重跑脚本,结果 kubelet 已经初始化过了,脚本在同一个动作上再次执行就报错。所以我的脚本开头会做三处检查:
# 1. 检查 kubelet 是否已经在运行 if systemctl is-active --quiet kubelet; then echo "kubelet already running, skip join" exit 0 fi # 2. 检查是否已经有 kubeconfig 残留 if [ -f /etc/kubernetes/kubelet.conf ]; then echo "node already joined" exit 0 fi # 3. 确认该节点没有在集群列表里面 if kubectl get nodes | grep -q "$(hostname)"; then echo "node exists in cluster" exit 0 fi这三项检查有一个逻辑顺序问题:如果节点已经加入过,但之后删除了节点,kubelet 配置还在,脚本会认为“已加入”而退出。这时候需要手工清理。所以我在脚本里提供了一个--force-reset参数,内部执行:
kubeadm reset -f rm -rf /etc/kubernetes/manifests /etc/kubernetes/kubelet.conf rm -rf /etc/cni/net.d iptables -F && iptables -t nat -F || true ipvsadm --clear 2>/dev/null || true注意kubeadm reset -f只是把 kubelet 和集群配置重置,不会清理容器运行时里的既有镜像。如果你真的想让这台机器完全回到初始状态,还得配合crictl rm --all --force。网上很多“重置集群”教程只讲前半段,结果节点 IP 冲突或者端口占用问题依旧在。
工作节点 join 时还有一个隐藏参数容易被漏掉:--node-ip。在生产服务器有多块网卡的环境里,不显式指定 node-ip,kubelet 会随机选一个把节点注册到集群,之后 apiserver 和 kubelet 之间的连接就非常不稳定。脚本里我给这个参数单独留了变量NODE_IP,并且要求部署人员为每台机器填写。
5. 独立部署 Harbor:离线包加载、证书信任和数据持久化
5.1 离线部署 Harbor 的目录与配置
Harbor 我选择用官方离线安装包配合 docker-compose 部署,而不是 Helm。原因是:Harbor 本身要承担“内网镜像源”的角色,如果它自身的部署再依赖 k8s 里的另一个仓库,会形成鸡生蛋的问题。离线安装包内已经打包好了所有镜像和启动脚本,最适合在没有外网的环境里先把它拉起来。
下载离线包后,我建议先做一步校验:把官方提供的 sha256 值和实际文件的摘要做对比,脚本里直接写成:
echo "下载校验和" | sha256sum -c harbor-offline-installer-v2.10.0.tgz.sha256 || exit 1解压后,所有配置都集中在harbor.yml。需要重点改的地方有这几个:
hostname: harbor.local http: port: 80 https: port: 443 certificate: /data/certs/harbor.local.crt private_key: /data/certs/harbor.local.key harbor_admin_password: "改成一个复杂的初始密码" data_volume: /data/harbor/datahostname不是简单的展示字段,它最终会写入镜像仓库的访问地址。如果在docker login时用了 IP 或另一个域名,就会出现证书和 hostname 不匹配的问题。我这里统一用harbor.local这样一个内部域名,并在所有节点的/etc/hosts里加上解析。
data_volume也要特别注意。Harbor 的设计里,数据库、镜像存储、Redis 数据都在这个目录下。如果系统盘不大,建议单独挂一块数据盘,比如/data。不然镜像推多了,占满根分区之后 Harbor 的 jobservice 和 registry 组件会一起出问题,而且很难清理。
5.2 证书信任链:从自签证书到 containerd 客户端
Harbor 默认推荐启用 HTTPS,但在内网环境用公开 CA 证书比较麻烦,所以常见做法是自签 CA,并为harbor.local签发服务证书。自签不是问题,问题在于后续每一个使用这个仓库的客户端都要信任这个 CA。
我的证书生成脚本很简单:
# 1. 自签 CA openssl req -new -x509 -days 3650 \ -keyout ca.key -out ca.crt \ -subj "/CN=Harbor-CA" # 2. 签发服务端证书 openssl req -newkey rsa:2048 -nodes \ -keyout harbor.local.key -out harbor.local.csr \ -subj "/CN=harbor.local" openssl x509 -req -in harbor.local.csr \ -CA ca.crt -CAkey ca.key -CAcreateserial \ -out harbor.local.crt -days 3650 \ -extfile <(printf "subjectAltName=DNS:harbor.local,DNS:*.harbor.local")之后需要把这个ca.crt分发到两类地方。一类是 docker 客户端,Linux 下放到/etc/docker/certs.d/harbor.local/ca.crt或执行cp ca.crt /usr/local/share/ca-certificates/ && update-ca-certificates。另一类是 k8s 节点的 containerd,这个会在下一章详细说。
我踩过的坑是:只分发了证书,忘记让 containerd 里的 registry 配置读取它,结果crictl pull harbor.local/...一直报x509: certificate signed by unknown authority。后来干脆在联调脚本里把“证书分发”写成强制步骤,任何节点加入后先校验证书文件是否存在,再继续后续操作。
Harbor 安装脚本的执行顺序也要固定:先确认 docker-compose 可用,再执行./install.sh,最后等待服务健康。Harbor 的install.sh会自己调用 prepare 生成配置并启动全部容器。它启动失败的高频原因就两个:端口被占用,或者harbor.yml解析不过。脚本里检查端口那一步不能省,尤其如果机器上已经跑着别的服务占用了 80/443。
6. 把 Harbor 配置成 k8s 默认私有仓库的联调脚本
6.1 打通 containerd 的 registry 配置
现在 k8s 集群有了,Harbor 也起来了,最后一步是把两者连起来,让 k8s 节点能从 Harbor 拉取私有镜像。如果节点用的是 containerd,配 docker 的/etc/docker/daemon.json是完全没用的,改成配置 containerd 的 TLS 证书目录:
# /etc/containerd/certs.d/harbor.local/hosts.toml server = "https://harbor.local" [host."https://harbor.local"] ca = "/etc/containerd/certs.d/harbor.local/ca.crt"同时还要在 containerd 的默认配置中让它读取这个目录。注意有些安装方式默认没有启用config_path,需要加上:
[plugins."io.containerd.grpc.v1.cri".registry] config_path = "/etc/containerd/certs.d"改完后不用重启整个 containerd,最好执行systemctl restart containerd后再用crictl pull harbor.local/k8s-apps/demo:v1验证一次。这里有一个非常容易踩的细节:hosts.toml里server字段需要和镜像仓库地址完全一致,如果 hosts.toml 写的server = "https://192.0.2.30",而镜像名用的是harbor.local/k8s-apps/...,仍然会走默认的普通 HTTPS 通道,证书校验照样失败。把这些领域写在联调脚本里,就比人肉排查省事很多。
6.2 创建项目、账号与拉取验证
Harbor 的项目和账号我建议用脚本创建。比起在网页上点按钮,脚本创建的方式更适合复制到另一个环境,也方便以后对照检查。Harbor 有完整的 REST API,创建项目只需要一个请求:
curl -u "admin:<初始密码>" -X POST \ "https://harbor.local/api/v2.0/projects" \ -H "Content-Type: application/json" \ -d '{"project_name":"k8s-apps","public":true}'我一般会把项目设置为公开,或者使用独立的 robot 账号。k8s 里拉取镜像最终需要的是docker-registry类型的 Secret,如果项目是公开的,这个 Secret 可以不写;如果项目是私有的,机器人账号比普通用户账号更合适,因为它的权限可以限定到某个项目,而且密码可以旋转。脚本中创建账号并绑定项目后,生成 k8s Secret 的命令是:
kubectl -n default create secret docker-registry registry-secret \ --docker-server=https://harbor.local \ --docker-username="robot$k8s-apps" \ --docker-password="<机器人账号token>"最后用一个 Deployment 做全链路验证。比如:
kind: Deployment apiVersion: apps/v1 metadata: name: harbor-pull-test spec: replicas: 1 template: metadata: labels: app: harbor-pull-test spec: containers: - name: demo image: harbor.local/k8s-apps/demo:v1 imagePullPolicy: IfNotPresent imagePullSecrets: - name: registry-secret验证成功后,先查看kubectl get events --all-namespaces有没有Failed to pull image,再看crictl ps容器是否正常启动。这一步如果通过,基本说明整个 k8s 和 Harbor 的链路已经打通,后面就是业务镜像如何推送和配置的问题了。
7. 实测中的坑和对应的脚本加固手段
7.1 多网卡环境下节点 IP 错乱
这个问题我在前面提到过一次,但值得单独拿出来讲,因为它的表现太有迷惑性:kubeadm init 正常,join 也显示成功,可kubectl get nodes看到的节点 IP 是公网网卡地址,或者每过几秒 apiserver 就报连接超时。根因是机器上有多块网卡,kubelet 注册节点时无法确定应该上报哪个 IP。
脚本加固的方式非常简单,给 kubelet 统一指定--node-ip:
echo "KUBELET_EXTRA_ARGS=--node-ip=${NODE_IP}" > /etc/sysconfig/kubelet systemctl daemon-reload && systemctl restart kubelet同时kubeadm init里也显式指定--apiserver-advertise-address,避免 API Server 监听在错误的网卡上。这类问题不写进脚本,每次重建环境都会原地踩一次。
7.2 cgroup driver 不一致导致 kubelet 起不来
Kubernetes 1.28 之后的版本对 cgroup driver 要求更加明确:建议 containerd 使用systemdcgroup driver,kubelet 也一样。如果 containerd 的配置还是默认的cgroupfs,kubelet 启动时会直接报一个类似 “misconfiguration: kubelet cgroup driver: systemd but Cgroup driver: cgroupfs” 的错误。
在安装脚本里,初始化 containerd 后必须马上处理这个配置:
containerd config default > /etc/containerd/config.toml sed -i 's/SystemdCgroup = false/SystemdCgroup = true/' /etc/containerd/config.toml systemctl restart containerd如果不重启 containerd,改配置是没有意义的。我在第一次跑脚本时漏了重启,结果 kubelet 照样起不来,日志翻了好久才发现问题不在 kubelet 而在 containerd。从此我把“改配置后必须重启服务并检查状态”写成了固定模板,执行后立即systemctl is-active containerd,不通过就中止脚本。
7.3 Harbor 存储暴涨和 GC 策略
Harbor 在大量推送和删除镜像后,磁盘使用量会持续增长。原因一是 registry 的 layer 文件不会因为删除了 tag 就立刻释放;二是 jobservice 和日志组件也在写日志。镜像清理需要手动触发 GC,但前提是被删除的 tag 在数据库里已经标记为删除,否则 GC 看不到可清理的 layer。
我给的脚本化方案分两步。第一步是在推送流程上只保留必要 tag,删除不要的镜像后调用 GC API:
curl -u "admin:<密码>" -X POST \ "https://harbor.local/api/v2.0/system/gc/schedule" \ -H "Content-Type: application/json" \ -d '{"schedule":{"type":"Manual"}}'第二步是给 Harbor 的 data_volume 所在分区加磁盘告警,定期du -sh /data/harbor/data/database /data/harbor/data/registry检查容量趋势。真正的大头永远是 registry 目录,如果磁盘短期内被占满,先看是不是有人推送了大镜像又反复删除。
7.4 踩坑问题对照表
| 现象 | 根因 | 脚本加固 |
|---|---|---|
| kubelet 起不来,日志提示 cgroup driver 不一致 | containerd 未启用 SystemdCgroup | 初始化脚本强制 sed 修改并重启 |
| 节点 IP 错乱,apiserver 连接不稳定 | 多网卡未指定 node-ip | 统一注入 KUBELET_EXTRA_ARGS |
| crictl pull 报证书未知颁发者 | containerd 未信任 Harbor CA | 证书分发脚本强制校验文件存在 |
| Harbor 端口正常,但 docker login 失败 | hostname 与证书域名不匹配 | harbor.yml 模板中锁定 hostname 变量 |
| 删除镜像后磁盘不释放 | registry 未 GC | 删除后调用 GC API + 容量告警 |
8. 离线包、日志与回滚:脚本安装工具的最后一公里
8.1 离线安装包的准备和校验
内网环境装 k8s 最麻烦的是镜像。我常用的做法是:在一台能访问境内外镜像仓库的机器上,先把需要的镜像全部 pull 下来,再用docker save或ctr导出成 tar 包,最后带到目标机器上导入。但用脚本自动安装时,要有一个“导入”的原子操作:检查镜像是否已经存在,不存在才导入,导入完成后校验数量。比如 containerd 环境的脚本可以这样写:
ctr -n=k8s.io images import k8s-images.tar如果镜像较多,导入过程可能会比较长,脚本要允许失败重试,而不是一失败就退出。我在脚本里给这类操作加了--retry逻辑,重试三次,每次间隔 5 秒。Harbor 离线包本身也是同样的校验思路,先sha256sum再解压。这些看起来很简单,但很多手工安装的失败正是因为镜像没同步完整,中间缺了一个 layer,导致 kubeadm init 中途卡住。
8.2 日志规范与失败重试机制
脚本化安装还有一个容易被忽视的打磨点:日志。我要求每个脚本至少做到两件事。第一,所有输出统一追加到logs/install-YYYYmmddHHMMSS.log,同时仍然让关键状态输出到终端,方便人实时观察;第二,给关键函数加上时间戳标记:
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${LOG_FILE}" }失败重试不只是网络操作需要。系统初始化阶段执行sysctl --system后,部分云环境可能会有覆盖内核参数的提示,这本身不是错误,但脚本如果用set -e加持会直接把脚本终止掉,导致后面的步骤永远无法执行。我的经验是对这类“可忽略”的错误统一捕获,只在真正影响后续动作的命令上严格退出,比如kubeadm init失败、systemctl restart containerd失败。判断标准很简单:如果这个动作失败后,重跑脚本也无法恢复,那它就必须让脚本停止。
回滚方面,k8s 侧用一条kubeadm reset -f配合清理 CNI 配置即可;Harbor 侧就比较粗放,直接停掉 compose 服务、把 data_volume 目录改名备份,然后重新执行安装脚本。因为 Harbor 的配置基本都是声明式的,回滚成本明显低于 k8s 集群。所以在脚本设计上,我让 Harbor 脚本天然支持“重新安装”,而不是依赖一个复杂的 rollback 逻辑。
写到这里,我把这次 k8s 和 harbor 脚本安装的整个思路和命令都列完了。个人感受是,脚本化的真正收益不只是“快”,它逼着我把每一步的原理都查清楚,否则脚本里任何一个参数错误,重跑多少次都是白费。最后再分享一个小建议:不要照着别人的脚本直接复制,而是先在自己的一台机器上手工跑通一遍,把关键动作和报错都记录下来,再把这些记录翻译成脚本。这样维护起来的脚本,才能在未来某一天环境出问题时,真正帮你定位问题所在。