1. 障害の発生 — ArgoCDは全部Syncedなのに、アプリだけが起動してこない

K3sクラスタをGitOpsで作り直す作業は、本来「コードがすべて」のはずだった。Terraformでノードを立て、ArgoCDに向けてGitリポジトリを指せば、数十個のマニフェストが次々とSyncedになっていく。ところが、いくつかのアプリケーションだけがいつまで経ってもReadyにならない。ログを見ると、DBのパスワードやAPIトークンを読み込む段階でクラッシュしている。
調べていくと、原因は意外な場所にあった。K3sクラスタを再構築したタイミングで、Sealed Secretsが一つも復号できない状態になっていたのだ。Gitリポジトリには暗号化済みのSealedSecretリソースが正しくコミットされており、ArgoCD側の適用ログにもエラーはない。それでも、実際のSecretとして展開されない。GitOpsで「コード化」していたはずのシークレットだけが、再構築のたびに再現性を失っていた。
こうした「コード化したはずの運用が、再構築のたびに一部だけ再現できない」という落とし穴は、K3sクラスタを自前で積み上げるほど増えていく。KuboのようなマネージドK3s環境を使うかどうかの分かれ目は、まさにこうした地味な運用設計の差にある。
2. 原因 — Sealed Secretsの秘密鍵は、クラスタを作り直すたびに"別物"になる

Sealed Secretsは、kubeseal CLIで平文のSecretを公開鍵暗号化し、SealedSecret というCRDとしてGitにコミットできるようにする仕組みだ。クラスタ内で稼働するコントローラだけが対応する秘密鍵を持っており、SealedSecretリソースが適用されると、コントローラがそれを復号して通常のKubernetes Secretとして生成する。
ここで見落とされがちなのが、その秘密鍵自体はGitに入っていないという点だ。秘密鍵はkube-systemNamespace内にある、ごく普通のKubernetes Secretとしてクラスタ内部にのみ存在する。公式リポジトリのドキュメントにもある通り、この鍵はkubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-keyで個別にバックアップしない限り、クラスタの外には一切出てこない。
今回のケースでは、K3sクラスタをゼロから新規構築し、GitOpsでマニフェストを流し込む運用だった。この手順では、新しいSealed Secretsコントローラが起動時にまったく新しい鍵ペアを自動生成する。Gitにコミットされていた既存のSealedSecretは、旧クラスタの鍵でしか復号できないため、新しい鍵を持つコントローラにとっては単なる暗号化されたゴミデータでしかない。GitOpsで「全部コード化」していても、鍵というコード化されない一点だけが引き継がれなかったのだ。
3. 「K3sはetcdをバックアップしているから大丈夫」という誤解

ここで整理しておきたいのが、K3sの標準的なバックアップ機構との関係だ。K3sは組み込みetcdデータストアを使う場合、k3s etcd-snapshotによる定期スナップショットをデフォルトで有効にしており、0時と12時にスナップショットを取得し5世代保持する。このスナップショットには秘密鍵を含むすべてのSecretが含まれているため、etcdスナップショットから正しく復元した場合は、Sealed Secretsの鍵も一緒に復元され、問題は起きない。
問題が起きるのは、今回のように「スナップショットからの復元」ではなく「新規にクラスタを構築してGitOpsで流し込む」運用のときだけだ。この違いを認識せずに「バックアップは取れているから安心」と考えていると、実際に再構築が必要になった瞬間に初めて気づくことになる。なお、K3sのドキュメントはデータストアのスナップショットだけでなくサーバートークンファイルのバックアップも必須としている点も見落とされがちな注意点だ。トークンが一致しないとスナップショット自体が使用不能になる。
4. 復旧手順 — 鍵のバックアップがあるかどうかで、結果はまったく違う

鍵のバックアップが残っていれば、復旧作業はシンプルだ。バックアップした鍵のYAMLを新クラスタにkubectl applyし、Sealed Secretsコントローラのポッドを再起動すれば、コントローラが既存の鍵を読み込み直し、Gitにある旧SealedSecretを正しく復号できるようになる。クラスタがダウンしている場合でも、kubeseal --recovery-unsealコマンドでオフラインでの復号が可能だ。
一方、鍵のバックアップが一つも残っていなかった場合は厳しい。Sealed Secretsのドキュメントが明記している通り、鍵を失った状態でSealedSecretを復号する方法は存在しない。この場合、該当するシークレットについては、パスワードマネージャーやクラウドプロバイダーのシークレットストアなど、Git以外の経路で保管していた平文の値を手作業で再登録し、新しい鍵で再度シールし直すしかない。つまり、「シークレットの原本をGit以外のどこに控えているか」が、障害発生後の作業時間を大きく左右する。
5. 予防策 — 鍵のバックアップとローテーションを運用として仕組み化する

再発を防ぐための打ち手は大きく3つある。
- 鍵のバックアップを手順化する:
kubectl get secret -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key -o yamlで取得したファイルを、Gitとは別のセキュアな保管場所(HashiCorp Vaultやオフラインの暗号化アーカイブなど)に保存する。Kubernetes公式のSecretグッドプラクティスでも、etcd上のSecretデータ自体の暗号化設定とあわせて、この種の鍵管理を「root権限のパスワードと同じ扱い」で管理することが推奨されている - ローテーションの挙動を理解しておく: Sealed Secretsのコントローラはデフォルトで30日ごとに新しい鍵ペアを生成するが、既存のSealedSecretを復号できるよう古い鍵も保持し続ける仕組みになっている。鍵のローテーションと、実際のシークレット値(パスワードなど)のローテーションは別物であることを運用チーム内で認識しておく
- 復元経路そのものを見直す: そもそも「新規構築+GitOps流し込み」という運用を避け、ArgoCDの公式ドキュメントが推奨する「デスティネーションクラスタでのシークレット管理」の考え方に沿って、External Secrets OperatorでHashiCorp Vaultのような外部シークレットストアと連携する構成に寄せるのも有効な選択肢だ。この構成なら、クラスタを作り直してもシークレットの実体は外部にあり、コントローラの鍵消失というリスク自体を切り離せる
Red Hatのガイドが指摘する通り、GitOpsにおけるシークレット管理には「暗号化してGitに置く」方式と「外部ストアへの参照だけをGitに置く」方式の二系統がある。どちらを選ぶにせよ、「クラスタを作り直したときに何が引き継がれ、何が引き継がれないか」を事前に図示しておくことが、今回のような障害を防ぐ最も実効性のある一手になる。
こうした「バックアップは取っていたつもりが、実際に必要だった要素が欠けていた」という構造は、以前VeleroでK3sのPVCを復元したら中身が空だった事例でも触れた。GitOpsやバックアップの「仕組みがある」ことと、「実際に復元できる」ことの間には、常に確認すべきギャップが存在する。
K3sクラスタの運用設計そのものを自前で積み上げていくと、こうした鍵管理・バックアップ運用の落とし穴は増えていく。Prometheus+Grafanaのモニタリングやcert-managerの証明書自動更新を標準搭載しているKuboのように、こうした「地味だが致命的な運用設計」をあらかじめ組み込んだマネージドK3s環境を使うという選択肢も、検討する価値がある。
6. まとめ
K3sクラスタをGitOpsで再構築したときにSealed Secretsが全滅する原因は、秘密鍵がGitで管理されない「コード化の盲点」にあった。etcdスナップショットからの復元であれば鍵も引き継がれるが、新規構築+GitOps流し込みの運用では、コントローラが新しい鍵を自動生成してしまう。
対策は明確だ。鍵のバックアップをセキュアな場所に保管し、ローテーションの挙動を理解し、可能であればArgoCDが推奨する外部シークレットストア連携への移行も視野に入れる。
もし、こうした鍵運用の設計・テストまで含めた本番運用を自前で抱えるのが負担に感じるなら、KuboのようなマネージドK3s環境で、運用設計ごと任せてしまうという手もある。お問い合わせから、現在の運用で抱えている課題を相談してみてほしい。