mcpField
U
upgrade-cluster

upgrade-cluster

github.com/mysticaltech云与 DevOps3.9k78 次安装7 周前更新
代码仓库

关于 upgrade-cluster

upgrade-cluster 是用于安全升级 kube-hetzner 集群的 Agent 技能,结合 Terraform 或 OpenTofu 与 Kubernetes 运行状态检查。它将模块变更、Kubernetes 版本升级和不可变节点替换分阶段处理,并设有保护现有基础设施的停止条件。

主要功能

  1. 01分离模块与运行时升级
  2. 02按序规划并应用变更
  3. 03检查计划中的基础设施销毁
  4. 04验证 Kubernetes 与工作负载健康
  5. 05指导不可变节点替换

适用场景

  1. 01升级运行中的 kube-hetzner 集群
  2. 02逐步更新 k3s 或 RKE2 版本
  3. 03疑似主机失陷后替换节点
  4. 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

对于每个次要版本升级步骤:

  1. 除非操作员明确要重建或迁移运行时,否则保持 kubernetes_distribution 稳定。对于 k3s,更新 k3s_channel 或 k3s_version;对于 RKE2,更新 rke2_channel 或 rke2_version。
  2. 根据 HA 风险评估,保持升级排空和驱逐设置一致。
  3. 按顺序逐项规划并应用。
  4. 等待 system-upgrade 计划和节点就绪。
  5. 通过运行时检查后,再继续升级到下一个次要版本。
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 或带挂载卷的工作负载:

  1. 确认 PVC、PV、存储类、卷亲和性/位置以及当前所在节点。
  2. 移动 Pod 前,进行与应用状态一致的备份或快照。
  3. 如果应用提供专用刷新命令,请运行该命令,例如 Redis 兼容服务的 SAVE。
  4. 缩容 StatefulSet,等待 CSI/Longhorn 卸载卷,然后在新节点上扩容。
  5. 使用应用级完整性检查进行验证,不能只依赖 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 替换
  1. 追加一条新的 agent_nodepools 配置,使用可用的服务器类型、充足的容量,以及工作负载所需的相同标签/污点语义。
  2. 通过计划/应用操作,仅添加新 Worker。
  3. 等待新 Worker 进入 Ready 状态。
  4. 确认入口/负载均衡器目标中包含健康的新 Worker。
  5. 有计划地迁移或重新调度有状态工作负载。
  6. 根据工作负载选择适当的标志,对旧 Worker 执行封锁和排空。
  7. 将旧 Worker 池缩容至 count = 0,应用变更,并确认云服务器已删除。
  8. 确认最终的负载均衡器目标仅引用新 Worker。

对于带挂载卷的单实例工作负载,不要仅依赖 Kubernetes 重新调度;请明确管理卸载/挂载,并验证数据平面。

控制平面替换

每一步都要维持 etcd 法定人数。

  1. 开始前,确认 etcd/控制平面节点数量为奇数且状态健康。
  2. 从健康的控制平面节点创建 etcd 快照,并将其复制到被替换节点之外的位置,或确认该位置已有副本。
  3. 先添加新的控制平面投票成员,再移除旧成员。
  4. 每次只移除一台旧的控制平面服务器。
  5. 移除服务器后,如果对应的 Kubernetes Node 对象仍存在,则将其删除。
  6. 每次移除后都要验证 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> 将其从索引中移除,添加忽略规则,并且仅在该本地文件仍有运维需要时才保留它。

评论

支持 Markdown