upgrade-cluster について
upgrade-cluster は、Terraform または OpenTofu と Kubernetes の稼働確認を用いて、kube-hetzner クラスターを安全にアップグレードするエージェントスキルです。モジュール変更、Kubernetes のバージョン更新、不変ノード置換を分け、既存インフラを守る停止条件を設けています。
主な機能
- 01モジュールとランタイムの更新を分離
- 02変更を順番に計画・適用
- 03インフラ破棄の計画を確認
- 04Kubernetes とワークロードの健全性を検証
- 05不変ノード置換の手順を案内
活用シーン
- 01稼働中の kube-hetzner クラスターの更新
- 02k3s または 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 を、より新しいチャネルや明示的なバージョンに移行する場合。
- 稼働中のノードを再作成せずに、プロバイダー/モジュールの状態を調整する必要がある場合。
- アップグレードの前または実施中に、クラスターが HA かどうかをユーザーが確認したい場合。
- 侵害、マルウェア、ホストの状態不良、サーバータイプの非推奨化、アーキテクチャ/容量の移行が疑われ、ユーザーがノードの置き換えを希望する場合。
絶対に守るルール
- シークレットを出力またはコミットしないでください。kubeconfig、
*.tfstate、*.tfvars、ローカルの環境ファイル、プランファイル、.terraform/は決してコミットしないでください。 - 対象のルートですでに使用されている IaC ランナーを使用してください。以下の例では
terraformを使用しています。対象のルートですでに OpenTofu を使用している場合に限り、tofuに置き換えてください。 - モジュールのリファクタリングによって、稼働中のサーバー、ネットワーク、ロードバランサー、ボリューム、プライマリ IP が誤って再作成されないようにしてください。プランに置き換えや破棄が示された場合は、作業を止めて根本原因を特定してください。
- モジュールの収束と Kubernetes の実行時バージョンのアップグレードは、別々のフェーズとして実施してください。
- 対象のバージョン範囲でマイナーバージョンをスキップしても安全だと、上流の明確な根拠をオペレーターが確認している場合を除き、k3s/RKE2 は一度に 1 つのマイナーバージョンずつアップグレードしてください。
- ホストの侵害やマルウェアが対象となる場合は、インプレースでのパッケージ更新や再起動よりも、不変なノードへの置き換えを優先してください。更新はソフトウェアにパッチを適用しますが、新しいノードは古いホストに残る永続化の痕跡を取り除きます。
- kube-hetzner のノードプールのライフサイクルルールを守ってください。ノードプールの追加/削除は各リストの末尾でのみ行い、廃止前に古いプールの
count = 0を設定し、ゼロでないプールの名前は変更しないでください。また、モジュール/ルートで安全な置き換えトポロジーを意図的にモデル化している場合を除き、最初のコントロールプレーンのノードプールは count >= 1 のままにしてください。 - 正常なクォーラムの置き換えで
k3s server --cluster-resetを使用しないでください。これはクォーラム喪失時の災害復旧ツールであり、通常のコントロールプレーン移行に使用するものではありません。 - オペレーターのファイアウォールアクセスは一時的なものとして扱ってください。メンテナンスのためにアクセスを開放した場合は、完了を報告する前に閉じ、閉じたことを確認してください。
- 最終確認では具体的な根拠を示してください。Terraform の収束、ノードのバージョン、アップグレード計画の完了、ワークロードの健全性、API の準備完了、Ingress/LB の健全性、アプリケーションの準備完了を確認してください。
確認する入力情報
- Terraform のルートパスとバックエンドの種類。
- 現在のモジュールソース/バージョンと、対象のモジュールバージョン/コミット。
- 現在の
kubernetes_distribution、ノードのバージョン、対象のチャネル/バージョン。 - kubeconfig/API へのアクセス経路。
- クラスタートポロジー:コントロールプレーンの数、etcd メンバーシップ、エージェントプール、オートスケーラープール、Ingress/ロードバランサー。
- HA のリスク:単一インスタンスの StatefulSet、アタッチされたボリューム、PodDisruptionBudget、レプリカのない重要なワークロード。
- ステートフルデータの安全性:CSI/Longhorn/PV のアタッチメント、バックアップ/スナップショットの経路、アプリケーション固有のフラッシュコマンド、移動後のデータ整合性チェック。
- 脅威モデル:通常のアップグレード/パッチ適用か、既存ホストを信頼できない状態か。この違いによって適切なワークフローが変わります。
- ヘルスチェック:Kubernetes API の準備状況、Ingress/ロードバランサーの健全性、少なくとも 1 つのアプリケーションレベルの準備状況 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) を返す場合、ファイアウォール経路は開いており、問題は ID の選択または authorized keys にあります。認証失敗を解決するためにファイアウォールルールを緩めないでください。
対象ルートの kube.tf/vars で設定されている秘密鍵を正確に使用してください。これは、上流の kube-hetzner README のパターンに従ったものです:ssh root@<control-plane-ip> -i /path/to/private_key -o StrictHostKeyChecking=no。
ローカルの SSH 設定で別の ID が固定されている場合、ノードとファイアウォールが正常でも、単純な 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 を実行します。ドレインによってより大きなダウンタイムやボリュームのアタッチ変更が生じる場合に有用です。ノードサービスが再起動するまで、既存の Pod は残ります。system_upgrade_enable_eviction = false:ドレイン時のみ関係します。PDB によって妨げられているアップグレードを進められる場合がありますが、Pod を直接削除する可能性があります。
通常、オペレーターのアクセスを閉じている場合は、対象ルートのローカルな無視対象の変数のみを通じて開いてください。ソース CIDR は狭く保ち、通常は /32 または /128 にしてください。また、ノードの変更とは別に、ファイアウォールを開閉する差分を plan/apply してください。
フェーズ 1:モジュールの収束
まずモジュール/プロバイダーの設定のみを更新してください。避けられない場合を除き、同じ plan で 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
適用前に plan を確認してください。
plan の内容を説明でき、稼働中のインフラストラクチャを予期せず破棄または置き換えない場合にのみ、先へ進んでください。
本番環境のインプレースアップグレードでは、MIGRATION.md が安全性を保証する基準となります。plan の 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
モジュールによってリソースの所有権やアドレスが変更された場合:
- state を再度バックアップしてください。
- 再作成よりも、
terraform import、terraform state mv、またはモジュールがサポートする moved ブロックを優先してください。 - 稼働中のリソースを一度に 1 つずつ移行してください。
- state 操作のたびに
terraform planを再実行してください。 - state のアドレスがまだ曖昧な間は、広範囲に及ぶ plan を apply しないでください。
既存ノードのインターフェース確認
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ではありません。 - 妥当な時間待っても、サーバーまたはエージェントのアップグレードプランが完了しません。
- 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>
ワーカーノードの置換
- 利用可能なサーバータイプ、十分なキャパシティ、ワークロードに必要なラベル/テイントのセマンティクスを備えた、新しい
agent_nodepoolsエントリーを追加します。 - 新しいワーカーだけを追加して plan/apply を実行します。
- 新しいワーカーが
Readyになるまで待ちます。 - イングレス/ロードバランサーのターゲットに、正常な新しいワーカーが含まれていることを確認します。
- ステートフルワークロードを慎重に移動または再スケジュールします。
- ワークロードに適したフラグを使用して、古いワーカーを cordon および drain します。
- 古いワーカープールの
count = 0を設定して apply し、クラウドサーバーが削除されたことを確認します。 - 最終的なロードバランサーのターゲットが、新しいワーカーのみを参照していることを確認します。
アタッチされたボリュームを使用する単一インスタンスのワークロードでは、Kubernetes の再スケジュールだけに頼らないでください。デタッチ/アタッチを明示的に管理し、データプレーンを検証してください。
コントロールプレーンの置き換え
各手順で etcd のクォーラムを維持してください。
- 開始前に、etcd/コントロールプレーンの台数が奇数で、正常であることを確認します。
- 正常なコントロールプレーンノードから etcd スナップショットを取得し、置き換え対象のノード外にコピーするか、ノード外にあることを確認します。
- 古い投票メンバーを削除する前に、新しいコントロールプレーン投票メンバーを追加します。
- 一度に削除する古いコントロールプレーンサーバーは 1 台だけにします。
- サーバーの削除後も該当する 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 を繰り返し実行しないでください。利用可能な容量で新しいコントロールプレーンプールを追加し、正常な投票メンバーを参加させてから、root/モジュールの制約に従って古いプールを廃止します。root で最初のプールの count = 0 を安全にモデル化できない場合は、小規模でサービスを提供しないプレースホルダーを残すか、正確なリソースアドレスへの影響を確認した後に限り、state の直接編集を使用してください。
シークレットとインシデントの境界
変更不可の置き換えによって、古いホストに残存するリスクは低減します。ただし、置き換え前にシークレットが流出していないことの証明にはなりません。インシデントが予防措置ではなく実際に確認された場合は、認証情報のローテーション、イメージの来歴確認、信頼できるアーティファクトからのワークロード再デプロイが引き続き必要であることを報告してください。
稼働中のヘルスチェック
クラスターに適したチェックを使用してください。例:
# 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 ポッドが Unknown と表示されても、ジョブが Complete である場合があります。プランが完了し、すべてのノードが Ready で、ワークロードが正常な場合に限り、クリーンアップに伴うノイズとして扱ってください。
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 が含まれていないこと。
- クラスターがコントロールプレーン LB 経由で API を意図的に公開している場合を除き、閉鎖対象の接続元からの公開
kubectlアクセスが失敗するかタイムアウトすること。 - 公開イングレス/アプリケーションのレディネスチェックが引き続き成功すること。
破棄または中止後のクリーンアップ
アップグレードの演習が破棄可能なクラスター上で行われ、クラスターを削除する必要がある場合は、Terraform root から
scripts/destroy.sh を実行してください。このスクリプトは、既知の無害なイングレス LB 切り離し競合に限って自動再試行し、その後、読み取り専用の孤立リソースレポートを出力します。state がすでに破損している場合、またはレポートで残存リソースが確認された場合に限り、強制的な代替手段として scripts/cleanup.sh を使用してください。このスクリプトは、トークンの HCloud プロジェクト全体がクラスター専用であるものとして扱います。ドライランを確認し、永続データを含めるのは意図した場合に限ってください。
オートスケーラーが作成したサーバーは Terraform の state 外にあります。サーバーがネットワーク/サブネットの削除を妨げる場合は、コントロールプレーンの停止後に限り削除してください。または、先にオートスケーラープールを min_nodes = 0 にスケールしてください。Cluster Autoscaler の稼働中に先に削除すると、サーバーが再作成される可能性があります。
最終レポート
報告内容:
- モジュールのアップグレード前後のバージョンとソース。
- Kubernetes のディストリビューションとアップグレード前後のバージョン/チャネル。
- HA の評価:コントロールプレーン/etcd の台数、エージェントプール、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> でインデックスから削除し、除外ルールを追加してください。そのローカルファイルが引き続き運用上必要な場合に限り、手元に残してください。