关于 upgrade-cluster
upgrade-cluster 是用于安全升级 kube-hetzner 集群的 Agent 技能,结合 Terraform 或 OpenTofu 与 Kubernetes 运行状态检查。它将模块变更、Kubernetes 版本升级和不可变节点替换分阶段处理,并设有保护现有基础设施的停止条件。
主要功能
- 01分离模块与运行时升级
- 02按序规划并应用变更
- 03检查计划中的基础设施销毁
- 04验证 Kubernetes 与工作负载健康
- 05指导不可变节点替换
适用场景
- 01升级运行中的 kube-hetzner 集群
- 02逐步更新 k3s 或 RKE2 版本
- 03疑似主机失陷后替换节点
- 04迁移节点池或服务器类型
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mysticaltech/ terraform-hcloud-kube-hetzner --skill upgrade-cluster安装后在 Claude Code 中输入/upgrade-cluster确认已加载。
附带文件references/immutable-node-replacement.md
详细介绍
升级 Kube-Hetzner 集群
使用 Terraform/OpenTofu 和 Kubernetes 运行时验证,安全升级由 kube-hetzner 管理的现有集群。
适用场景
- 用户要求升级正在运行的 kube-hetzner 集群。
- 必须将模块版本更新应用到现有集群。
- k3s 或 RKE2 应升级到更新的通道或指定版本。
- 必须协调 Provider/模块状态,同时不重建正在运行的节点。
- 用户询问集群在升级前或升级期间是否为 HA。
- 用户因怀疑遭到入侵、存在恶意软件、主机状态不佳、服务器类型弃用或架构/容量迁移而要求更换节点。
硬性规则
- 不得打印或提交机密。绝不要提交 kubeconfig、
*.tfstate、*.tfvars、本地环境文件、计划文件或.terraform/。 - 使用目标根目录已在使用的 IaC 运行器。以下示例使用
terraform;只有目标根目录已使用 OpenTofu 时才替换为tofu。 - 绝不要让模块重构意外重建正在运行的服务器、网络、负载均衡器、卷或主 IP。如果计划显示替换或销毁操作,请停止并查明根本原因。
- 将模块收敛和 Kubernetes 运行时版本升级分为不同阶段。
- 除非运维人员有明确的上游证据,证明针对确切的版本跨度跳过次版本是安全的,否则每次只升级一个 k3s/RKE2 次版本。
- 如果涉及主机遭到入侵或恶意软件,优先选择不可变节点替换,而不是就地更新软件包或重启。更新只能修补软件;新节点可以清除旧主机上的持久化威胁。
- 遵循 kube-hetzner 节点池生命周期规则:只在列表末尾添加/删除节点池;退役旧节点池前,将其
count设为0;不要重命名非零节点池;除非模块/根目录已明确建模安全的替换拓扑,否则第一个控制平面节点池的count必须至少为 1。 - 不要在健康仲裁组替换期间使用
k3s server --cluster-reset。这是一种丢失仲裁组后的灾难恢复工具,不是常规控制平面迁移手段。 - 将运维人员的防火墙访问视为临时访问。如果为维护而开放访问,请在报告完成前关闭访问并验证已关闭。
- 最终验证必须具体明确:Terraform 已收敛、节点版本符合要求、升级计划已完成、工作负载健康、API 已就绪、Ingress/LB 健康,并且应用已就绪。
需要确认的输入
- Terraform 根目录路径和后端类型。
- 当前模块源/版本及目标模块版本/提交。
- 当前的
kubernetes_distribution、节点版本和目标通道/版本。 - Kubeconfig/API 访问路径。
- 集群拓扑:控制平面数量、etcd 成员、Agent 池、自动扩缩容池、Ingress/负载均衡器。
- HA 风险:单副本 StatefulSet、已挂载的卷、PodDisruptionBudget,以及缺少副本的关键工作负载。
- 有状态数据安全:CSI/Longhorn/PV 附加情况、备份/快照路径、特定应用的刷新命令,以及迁移后的数据完整性检查。
- 威胁模型:常规升级/修补,还是不再信任现有主机。这将决定正确的工作流程。
- 健康检查:Kubernetes API 就绪状态、Ingress/负载均衡器健康状况,以及至少一个应用级就绪 URL 或命令。
- 维护限制和回滚预期。
- 临时访问模式:SSH/Kubernetes API 防火墙源规则、VPN/堡垒机访问,或控制平面 LB 暴露。
升级前检查
cd <terraform-root>
git status --short --branch
git pull origin <default-branch>
# Confirm local secret/state files are ignored or outside git.
git ls-files | rg '(^|/)(.*kubeconfig.*|.*\.tfstate(\.backup)?|\.terraform/|.*\.tfvars|.*\.auto\.tfvars|\.terraform\.local\.env)$' || true
# Backup state without printing it. Prefer a directory outside the repo.
RUN_DIR="${KUBE_HETZNER_UPGRADE_RUN_DIR:-${TMPDIR:-/tmp}/kube-hetzner-upgrade-$(date -u +%Y%m%dT%H%M%SZ)}"
mkdir -p "$RUN_DIR"
terraform state pull > "$RUN_DIR/terraform.tfstate.before.json"
kubectl --kubeconfig <kubeconfig> get nodes -o wide
kubectl --kubeconfig <kubeconfig> get deploy,sts,pdb -A
kubectl --kubeconfig <kubeconfig> get pods -A -o wide
kubectl --kubeconfig <kubeconfig> get --raw='/readyz?verbose'
SSH 访问检查
替换期间,必须能通过 SSH 正常访问运行中的节点。请分别检查防火墙连通性和 SSH 身份验证:
nc -vz -w 5 <node-ip> 22
ssh root@<node-ip> -i <path-to-private-key-from-kube.tf> -o StrictHostKeyChecking=no "hostname; id; systemctl is-active sshd || systemctl is-active ssh || true"
如果 nc 能连上 22 端口,而 SSH 返回 Permission denied (publickey,keyboard-interactive),说明防火墙路径已开放,问题出在身份选择或授权密钥上。不要通过放宽防火墙规则来解决身份验证失败。
使用目标根目录 kube.tf/vars 中配置的确切私钥。这遵循上游 kube-hetzner README 中的模式:ssh root@<control-plane-ip> -i /path/to/private_key -o StrictHostKeyChecking=no。
如果本地 SSH 配置指定了其他身份,那么即使节点和防火墙都正常,直接运行 ssh root@<node-ip> 也可能失败。请使用目标根目录 ssh_private_key 中的密钥,例如:
ssh root@<node-ip> -i /path/to/private_key -o StrictHostKeyChecking=no "hostname"
如果存在单实例有状态工作负载或严格的 PDB,请在执行任何 apply 前决定升级行为:
system_upgrade_use_drain = true:对副本式无状态工作负载更安全,但可能阻塞或迁移单实例有状态工作负载。system_upgrade_use_drain = false:使用 cordon 而非 drain;适用于 drain 会导致更严重的停机或卷重新挂载问题的情况。节点服务重启前,现有 Pod 会继续运行。system_upgrade_enable_eviction = false:仅在执行 drain 时相关;可解除被 PDB 阻塞的升级,但可能直接删除 Pod。
如果通常会关闭运维人员的访问权限,只能通过目标根目录本地且被忽略的变量来开放访问。将源 CIDR 范围限制得尽可能窄,通常为 /32 或 /128,并将开放/关闭防火墙的变更与节点变更分开规划和应用。
阶段 1:模块收敛
首先只更新模块/提供程序设置。除非无法避免,否则不要在同一个计划中更改 K3s 通道。
cd <terraform-root>
terraform init -upgrade -input=false
terraform fmt -check -diff
terraform validate
terraform plan -input=false -parallelism=1 -out=module-upgrade.tfplan
应用前请检查计划。
只有在计划内容清晰可解释,且不会意外销毁或替换运行中的基础设施时,才能继续。
对于生产环境的原地升级,MIGRATION.md 是安全操作规范。保存
计划 JSON,并在应用前运行受保护的 hcloud 无销毁检查:
terraform show -json module-upgrade.tfplan \
| jq -r '
.resource_changes[]?
| select(.type as $type | [
"hcloud_server",
"hcloud_network",
"hcloud_network_subnet",
"hcloud_load_balancer",
"hcloud_volume",
"hcloud_primary_ip",
"hcloud_placement_group",
"hcloud_firewall"
] | index($type))
| select(.change.actions | index("delete"))
| "\(.address): \(.type) \(.change.actions | join(","))"
'
任何输出都意味着必须停止。先查明根本原因,再执行 apply。
terraform apply -input=false -parallelism=1 module-upgrade.tfplan
terraform plan -input=false -parallelism=1 -detailed-exitcode
如果模块更改了资源归属或地址:
- 再次备份状态。
- 优先使用
terraform import、terraform state mv或模块支持的 moved 块,而不是重建资源。 - 每次迁移一个运行中的资源。
- 每次状态操作后都重新运行
terraform plan。 - 状态地址仍有歧义时,不要应用范围过大的计划。
现有节点接口检查
Kube-hetzner v2.20 要求私有网络接口命名为 eth1。新节点通过 cloud-init 设置该名称,但较早创建的、长期运行且版本低于 v2.20 的节点,私有接口可能仍使用 enp7s0 这类可预测的内核名称。如果 K3s 重启后 /readyz 卡住,且日志中出现类似 unable to find interface eth1 的信息,请先修复现有节点,再继续操作。
在确认模块收敛正常之前,请验证每个现有节点:
ssh root@<node-ip> "hostname; ip -br addr; systemctl is-active k3s k3s-agent 2>/dev/null || true"
ssh root@<node-ip> "journalctl -u k3s -u k3s-agent -n 80 --no-pager | rg 'unable to find interface eth1|server is not ready' || true"
如果节点上存在 /etc/cloud/rename_interface.sh,请使用模块提供的脚本进行修复,确保修复符合 kube-hetzner 的不变量:
ssh root@<node-ip> "if ! ip link show eth1 >/dev/null 2>&1; then /etc/cloud/rename_interface.sh; fi; ip -br addr"
ssh root@<node-ip> "systemctl restart k3s || systemctl restart k3s-agent"
修复后,请再次执行常规验证:
kubectl --kubeconfig <kubeconfig> get nodes -o wide
kubectl --kubeconfig <kubeconfig> get --raw='/readyz?verbose'
阶段 2:Kubernetes 运行时升级
确定当前和目标次要版本:
kubectl --kubeconfig <kubeconfig> get nodes -o wide
对于每个次要版本升级步骤:
- 除非操作员明确要重建或迁移运行时,否则保持
kubernetes_distribution稳定。对于 k3s,更新k3s_channel或k3s_version;对于 RKE2,更新rke2_channel或rke2_version。 - 根据 HA 风险评估,保持升级排空和驱逐设置一致。
- 按顺序逐项规划并应用。
- 等待 system-upgrade 计划和节点就绪。
- 通过运行时检查后,再继续升级到下一个次要版本。
terraform fmt -check -diff
terraform validate
terraform plan -input=false -parallelism=1 -out=kubernetes-<target-minor>.tfplan
terraform apply -input=false -parallelism=1 kubernetes-<target-minor>.tfplan
kubectl --kubeconfig <kubeconfig> -n system-upgrade get plans,jobs,pods -o wide
kubectl --kubeconfig <kubeconfig> get nodes -o wide
kubectl --kubeconfig <kubeconfig> get deploy,sts -A
kubectl --kubeconfig <kubeconfig> get --raw='/readyz?verbose'
出现以下情况时应停止:
- 节点未处于
Ready状态。 - 等待合理时间后,服务器或 Agent 升级计划仍未完成。
- Terraform 显示意外偏差。
- 公网入口或应用就绪状态反复检查失败。
- 单实例有状态工作负载因卷挂载/卸载而卡住,或不可用。
RKE2 不再意味着控制平面至少需要 8GB 内存。v3 中基于规格的 kubelet 预留默认值使 4GB 的 cx23 控制平面可用于 RKE2,而且该配置已在 CI 中经过验证。但生产环境中的实际资源池仍应为工作负载和 etcd 预留足够容量,而不应只满足模块最低要求。
阶段 3:不可变节点替换
当旧主机不可信,或需要彻底替换服务器类型/架构时,使用此阶段代替就地修补。
如需了解经过实战验证的完整工作流,请参阅 references/immutable-node-replacement.md。其中涵盖节点池计划结构、Hetzner 容量检查、负载均衡器目标验证、etcd 快照/成员管理、带挂载卷的 StatefulSet 迁移、Redis/Dragonfly 兼容性验证,以及最终防火墙收尾。
容量和计划结构
编辑节点池之前,请确认有足够的替换容量,并避免使用已弃用或不可用的类型:
hcloud server-type describe <candidate-type>
hcloud datacenter describe <candidate-datacenter-or-location>
建议追加一个使用新名称的新节点池,并在疏散完成后将旧池设为 count = 0。不要删除列表中间位置的节点池。Kube-hetzner 根据列表位置分配节点池子网/IP,因此遵循 FILO 顺序可避免意外的地址变更。
有状态工作负载和卷
对于每个单实例 StatefulSet 或带挂载卷的工作负载:
- 确认 PVC、PV、存储类、卷亲和性/位置以及当前所在节点。
- 移动 Pod 前,进行与应用状态一致的备份或快照。
- 如果应用提供专用刷新命令,请运行该命令,例如 Redis 兼容服务的
SAVE。 - 缩容 StatefulSet,等待 CSI/Longhorn 卸载卷,然后在新节点上扩容。
- 使用应用级完整性检查进行验证,不能只依赖 Kubernetes 就绪状态。
示例框架:
kubectl --kubeconfig <kubeconfig> -n <ns> get sts,pvc,pv -o wide
kubectl --kubeconfig <kubeconfig> -n <ns> exec <pod> -- <app-flush-or-backup-command>
kubectl --kubeconfig <kubeconfig> -n <ns> scale sts/<name> --replicas=0
kubectl --kubeconfig <kubeconfig> get volumeattachments
kubectl --kubeconfig <kubeconfig> -n <ns> scale sts/<name> --replicas=1
kubectl --kubeconfig <kubeconfig> -n <ns> exec <pod> -- <app-integrity-check>
Worker 替换
- 追加一条新的
agent_nodepools配置,使用可用的服务器类型、充足的容量,以及工作负载所需的相同标签/污点语义。 - 通过计划/应用操作,仅添加新 Worker。
- 等待新 Worker 进入
Ready状态。 - 确认入口/负载均衡器目标中包含健康的新 Worker。
- 有计划地迁移或重新调度有状态工作负载。
- 根据工作负载选择适当的标志,对旧 Worker 执行封锁和排空。
- 将旧 Worker 池缩容至
count = 0,应用变更,并确认云服务器已删除。 - 确认最终的负载均衡器目标仅引用新 Worker。
对于带挂载卷的单实例工作负载,不要仅依赖 Kubernetes 重新调度;请明确管理卸载/挂载,并验证数据平面。
控制平面替换
每一步都要维持 etcd 法定人数。
- 开始前,确认 etcd/控制平面节点数量为奇数且状态健康。
- 从健康的控制平面节点创建 etcd 快照,并将其复制到被替换节点之外的位置,或确认该位置已有副本。
- 先添加新的控制平面投票成员,再移除旧成员。
- 每次只移除一台旧的控制平面服务器。
- 移除服务器后,如果对应的 Kubernetes Node 对象仍存在,则将其删除。
- 每次移除后都要验证 etcd 成员状态。
示例框架:
ssh root@<healthy-control-plane> "k3s etcd-snapshot save --name pre-cp-replace-$(date -u +%Y%m%dT%H%M%SZ)"
kubectl --kubeconfig <kubeconfig> get nodes -l node-role.kubernetes.io/etcd=true -o wide
kubectl --kubeconfig <kubeconfig> get --raw='/db/info' || true
如果无法重新创建旧的首个控制平面服务器类型,不要在容量已耗尽时反复尝试运行 Terraform。追加一个有可用容量的控制平面池,加入新的投票成员,然后根据根模块的约束退役旧池。如果根模块无法安全地将首个池的 count = 0 纳入建模,则保留一个小型的非服务占位节点;或者只有在确认了具体资源地址会受到的影响后,才修改状态。
密钥与事件边界
不可变替换可降低旧主机持久留存的风险,但无法证明密钥此前未遭窃取。如果事件已确认,而非仅出于预防,则应说明仍需轮换凭据、审查镜像来源,并使用可信制品重新部署工作负载。
实时健康检查
根据集群情况使用合适的检查。示例:
# API readiness
kubectl --kubeconfig <kubeconfig> get --raw='/readyz?verbose'
# Workload readiness
kubectl --kubeconfig <kubeconfig> get deploy,sts,pods -A -o wide
# Ingress/LB readiness, if hcloud is configured
hcloud load-balancer describe <load-balancer-name>
# Application readiness, if available
for i in 1 2 3 4 5; do
curl -sS -o /dev/null -w "$i %{http_code}\n" <readiness-url>
sleep 2
done
节点重启后,旧的 system-upgrade Pod 可能显示为 Unknown,但其任务状态为 Complete。只有在升级计划已完成、所有节点均已就绪且工作负载运行正常时,才将其视为清理噪声。
SELinux 拒绝
如果工作负载在 SELinux 强制模式下无法运行,不要首先全局禁用 SELinux。请遵循 docs/selinux.md:从审计日志收集 AVC 记录,确认工作负载名称/版本以及 k3s/RKE2 发行版,优先尝试基于 udica 的工作负载策略,并且只有在有可复现的 AVC 证据时才提议上游模块策略。只有当特定节点池无法在策略下运行时,才将每个节点池的 selinux = false 作为最后手段。
防火墙关闭验证
如果本次操作开放了 SSH/API 访问,则在确认集群健康后,单独运行最终 Terraform 计划将其关闭。
# Remove local ignored access tfvars/env overrides first, then:
terraform plan -input=false -parallelism=1 -out=operator-close.tfplan
terraform apply -input=false -parallelism=1 operator-close.tfplan
rm -f operator-close.tfplan
terraform plan -input=false -parallelism=1 -detailed-exitcode
然后进行验证:
- 云防火墙规则中不再包含临时 SSH/API 源 CIDR。
- 从已关闭访问的源执行公共
kubectl命令时会失败或超时;如果集群有意通过控制平面 LB 暴露 API,则不在此限。 - 公共入口/应用仍能正常就绪。
销毁或中止时的清理
如果升级演练在一次性集群上进行且必须拆除,请从 Terraform 根目录运行
scripts/destroy.sh。该脚本只会自动重试已知的无害入口 LB 分离竞态,随后输出只读的孤立资源报告。只有当状态已损坏或报告中有遗留资源时,才将 scripts/cleanup.sh 用作强制回退方案。该脚本会将令牌所属的整个 HCloud 项目视为专供集群使用;请先检查试运行结果,且只有在有意为之时才包含持久数据。
自动扩缩器创建的服务器不在 Terraform 状态中。如果它们导致网络/子网无法删除,请在控制平面停止运行后再删除;或者先将自动扩缩器节点池缩容至 min_nodes = 0。在 Cluster Autoscaler 运行期间提前删除这些服务器可能会导致它们被重新创建。
最终报告
报告:
- 升级前后的模块版本和来源。
- 升级前后的 Kubernetes 发行版和版本/通道。
- HA 评估:控制平面/etcd 数量、Agent 池、Ingress 副本、应用副本,以及单实例有状态组件的风险。
- Terraform 证明:
fmt、validate、最终的plan -detailed-exitcode结果。 - Kubernetes 证明:节点版本、升级计划已完成、API readyz、Deployment/StatefulSet 已就绪。
- Ingress/应用证明:负载均衡器目标健康状况和应用就绪情况。
- 如使用不可变替换,提供相关证明:旧服务器已移除、替换节点处于 Ready 状态、etcd 成员中仅包含预期的投票成员、有状态数据完整性检查已通过,并且防火墙访问已再次关闭。
- 升级期间发生的任何故障,以及相应的修正设置或操作。
- 剩余风险,以及能确保其安全的确切条件。
Git 卫生
提交前:
git status --short
git ls-files | rg '(^|/)(.*kubeconfig.*|.*\.tfstate(\.backup)?|\.terraform/|.*\.tfvars|.*\.auto\.tfvars|\.terraform\.local\.env)$' || true
git diff --cached --check
如果已跟踪包含机密的本地文件,请使用 git rm --cached <path> 将其从索引中移除,添加忽略规则,并且仅在该本地文件仍有运维需要时才保留它。