クラスタ内Gitの落とし穴|k3s GitOpsを全損から復旧する設計
2026-07-11
クラスタ内Gitの落とし穴|k3s GitOpsを全損から復旧する設計のカバー画像

個人 k3s / VPS で GitOps をやっている人向けの話です。この記事は、既公開の「個人 k3s クラスタを Git と Kustomize で育てる」の続編にあたります。前の記事では「Git を中心に据えると運用が楽になる」と書きました。今回はその裏側、「その Git をクラスタの中に置いたとき、何が人質に取られるのか」 を正直に書きます。

断っておくと、これは「クラスタ内 Git はやめろ」という記事ではありません。自分は今もクラスタ内に Forgejo を置いて運用しています。その上で、この構成が抱えている循環依存を直視して、壊れたときにどう戻すかを整理した、という話です。

何に困っていたか

きっかけは、ある日ふと考えた素朴な疑問でした。

「今このクラスタが丸ごと消えたら、自分は何を頼りに復旧するんだ?」

自分の個人クラスタは GitOps で回しています。Argo CD が Git リポジトリを見て、書いてある通りにクラスタを作る。だから復旧も簡単なはず——Git さえあれば全部戻る、と思っていました。

ところが、その Git サーバー(Forgejo)はクラスタの中で動いている。Argo CD もクラスタの中。Argo CD が読む Git も、クラスタの中。

つまり「クラスタが死んだら Git も死ぬ」。復旧の拠り所にしていたものが、復旧すべき対象そのものの中にいた。この循環に気づいたときの、足元が抜ける感じが、この記事の出発点です。

この構成の何が良いのか(なぜクラスタ内 Git なのか)

先に、なぜこんな危ういことをしているのかを書きます。クラスタ内 Git には、個人運用では捨てがたい良さがあります。

  • プライバシー / データ主権: コードも Issue も CI ログも、他社の SaaS に預けない。全部自分の VPS の中にある。
  • 自律性: GitHub が落ちても、料金体系が変わっても、自分のクラスタは自分の Git で回り続ける。脱・外部依存。
  • 費用: プライベートリポジトリも CI も runner も、追加課金なしで好きなだけ持てる。VPS 一台の費用に含まれている。
  • 近さ: Forgejo runner → BuildKit → Harbor → Argo CD が同じクラスタ内で完結する。ビルドしたイメージが数秒でクラスタに届く。

要するに「全部自分の手の中にある」快適さです。これは本物のメリットで、だから自分はこの構成を選び続けています。問題は、その「全部が手の中」が、裏返すと「全部が同じ籠の中」だということです。

repo での実装 —— 依存の輪がどう閉じているか

<prod-overlay> の manifest を読むと、依存関係が具体的な YAML として見えます。要点だけ拾います(値は伏せ、ホスト名・リソース名はプレースホルダに置き換えています)。

1. アプリの Git ソースは、ほとんどがクラスタ内 Forgejo

各 Application の repoURL を数えると、その大部分が同じクラスタ内 Forgejo を指しています。

# Application(抜粋・構造のみ)
source:
  path: <prod-overlay>/forgejo
  repoURL: ssh://[email protected]:<SSH_PORT>/<org>/<infra-repo>.git
  targetRevision: main

2. Argo CD は、自分自身もその Forgejo から同期している

Argo CD 自身をデプロイする Application の source も、同じクラスタ内 Forgejo・path: <prod-overlay>/argocd です。Argo CD が Argo CD をデプロイし、その定義は Forgejo にある。

3. そしてその Forgejo は、Argo CD がデプロイしている

Forgejo をデプロイする Application が、<prod-overlay>/forgejo を同期して Forgejo 本体を立てています。ここで輪が閉じます。

Argo CD → Forgejo(を立てる)→ Forgejo(が Argo CD の定義を持つ)→ Argo CD

4. 鍵も secret も、さらに内側の OpenBao から来ている

Argo CD が Forgejo に SSH で繋ぐための鍵(<repository-credentials>)、Argo CD 自身の secret(OIDC の client secret や署名鍵、Redis 認証)——これらは全部 ExternalSecret 経由で External Secrets Operator → OpenBao から注入されています。

# ExternalSecret(抜粋・構造のみ)
kind: ExternalSecret
metadata:
  name: <repository-credentials>   # Argo CD が Forgejo に繋ぐための資格情報
spec:
  secretStoreRef:
    name: <openbao-store>            # ← これもクラスタ内で動いている
    kind: ClusterSecretStore

依存を内側に向かって並べると、こうなります。

[ 何かを sync したい ]
        │ 必要
        ▼
   Argo CD ──── git source ────► Forgejo(クラスタ内)
        │                            ▲
        │ repo鍵/自身のsecret        │ を立てているのは
        ▼                            │
   External Secrets ──► OpenBao      └──── Argo CD(自分自身)
        (どちらもクラスタ内)

一番外側の「アプリを直す」を実行するために、Forgejo・OpenBao・ESO・Argo CD がすべて生きている必要がある。平常時はこれで全く問題なく回ります。問題が出るのは、この輪のどこか——特に土台のノード——が崩れたときだけです。

この repo ならではの深掘り —— 単一障害点は Git ではなく「ノード」だった

もう少し内側を見ると、本当の急所が見えてきます。

Forgejo のデータは local PV(ノード固定のホストパス)に載っていて、OpenBao も同じ性格の永続化をしています。個人 VPS 構成では、こうした永続データは実質的に同じ障害ドメイン(同じノード / ストレージ)に集まりがちです。

すると、「クラスタが死ぬ」の最悪ケースは分散障害ではなく、その障害ドメインごと失うことです。そのとき同時に失われるのは:

  • Forgejo = Git 本体(コミット履歴・Issue・CI 設定を含む)
  • OpenBao = secret の金庫
  • 各アプリの永続データ

GitOps の心臓(Git)と、それを復元するための鍵(secret)が、同じ障害ドメインの中で一緒に失われうる。 これがこの構成の一番怖いところで、「クラスタが死んだ日に Git も死ぬ」というタイトルの正体です。ここは local PV / nodeAffinity の罠の話(別記事)とも地続きです。

設計判断 —— なぜ「全部クラスタ内」にしなかったか(実は、していない)

この repo を読んで少し安心したのは、過去の自分が完全な自己完結を避けていた痕跡があったことです。全部を輪の中に入れてはいませんでした。

  • GitHub ミラーが部分的に存在する。 一部の Application は repoURL を GitHub 側([email protected]:<org>/<infra-repo>.git)にしています。全部ではなく一部だけですが、クラスタの外にコードのコピーがあるという事実は、全損時の意味が大きい。Argo CD 側にもそのための repository 資格情報が用意してあります。
  • 緊急用のローカル認証経路を確保してある。 ログインは通常 IdP(OIDC) 経由ですが、IdP が落ちても管理者として入れる非常口を別に残してあります(IdP を巻き込んだ全断を一度経験しているので、この非常口の価値は身に染みています)。
  • 手元の local clone がある。 そもそも自分はこの repo をローカルにも clone して持っています。これが最後の命綱です。

つまり設計判断としては、「利便性のためにクラスタ内 Git を選ぶ。ただし唯一のコピーにはしない」。ここを踏み外すと、本当に retrieval 不能になります。

運用と切り分け —— 全損からの復旧を「思考実験」する

実際に壊す勇気はないので、手順書として順序を整理しておきます。ポイントは 「外側から内側へ」ではなく「内側の土台から外側へ」積み直す ことです。

手順 0: 手元の Git を確定する

  • ローカル clone(または GitHub ミラー)を最新に。ここが世界の正本になる。origin が死んだ Forgejo を指していないか確認する。

手順 1: 素の k3s を立て直す

  • ノード / k3s を再構築。この時点では Argo CD も Forgejo もいない。

手順 2: kubectl で「土台」だけ直接ブートストラップする

  • ここが肝。GitOps ではなく、手元の clone から kubectl で直接叩く。repo の Readme にこの逃げ道のコマンドが残っています。
# Forgejo を GitOps を介さず直接立てる
kubectl apply -k ./<prod-overlay>/forgejo

# Argo CD 自身も、手元マニフェストから直接立てる
kubectl kustomize ./<prod-overlay>/argocd --enable-helm | kubectl apply -f -

手順 3: secret の金庫(OpenBao)を復元する

  • OpenBao を戻し、unseal する。External Secrets が動き始めれば、repo 資格情報や Argo CD 自身の secret が再注入される。ここが復旧の一番の関門で、secret 金庫のバックアップと unseal 手段が確保できていることが前提になる。secret はコードと違って Git には無い。

手順 4: 正本を Forgejo に戻し、Argo CD に舵を渡す

  • ローカル clone を Forgejo に push。Argo CD の repo 資格情報が揃えば、あとは Argo CD が残り全アプリを自動で sync して積み上げていく。

手順 5: 輪が閉じたことを確認する

kubectl get applications -n argocd          # 全 App が Synced/Healthy に向かうか
kubectl get externalsecret -A               # secret 注入が回復しているか
argocd app list                             # Forgejo を source にする App が緑か

思考実験して分かったのは、復旧の成否を分けるのは Git ではなく「クラスタの外にある 2 つのもの」 だということ。すなわち (a) コードのコピー(local clone / GitHub ミラー)と、(b) OpenBao のバックアップと unseal key。この 2 つが外に無ければ、どれだけ GitOps がきれいでも戻せません。

罠と改善余地

正直に、今の自分の構成に残っている穴を挙げます。

  • GitHub ミラーが一部だけで、しかも手動同期。 全 App を push mirror で常時ミラーしていない。「全損時にコードが揃わない App がある」状態になりうる。→ Forgejo 側の push mirror を全 repo に効かせるか、CI で定期ミラーを回すのが宿題。
  • secret 金庫のバックアップは、コードほど自動化の余地がある。 ここは復旧の最大の関門になるので、バックアップの自動化(CronJob 化)とオフサイト保管、unseal 手段の管理は、一般論としても最優先で固めておきたい領域(バックアップを CronJob にする話は別記事の主題)。
  • 循環依存の「起動順序」が暗黙知になりやすい。 上の手順 0〜5 は、放っておくと頭の中にしか残らない。→ DISASTER_RECOVERY.md として手順・コマンドまで含めて明文化する価値がある。手順書自体も Git(=失うもの)に入れる矛盾があるので、ローカル / 外部にも複製しておく。
  • 同一障害ドメインへの集中。 Git と secret 金庫が同じ障害ドメインに寄りやすい。ここを Longhorn 等でノードから剥がすか、障害ドメインを分けるかは、費用とのトレードオフで検討する余地がある。

公開時に伏せたもの(値は出さず、理由だけ)

  • Forgejo の実ホスト名と SSH ポート: forgejo-ssh.example.com:<SSH_PORT> に置き換え。公開 Git エンドポイントを晒すと攻撃対象を教えることになるため。
  • リポジトリ名・org 名・リソース名・overlay のパス: <infra-repo> / <org> / <repository-credentials> / <prod-overlay> などに置換。個人アカウントや repo 構成の特定を避けるため。App の本数やアプリ名も、技術的に必要な範囲を超えて具体的にしないようにしています。
  • secret の中身(repo 鍵、OIDC の client secret、署名鍵、OpenBao の値): 一切引用していません。ExternalSecret は「どのキーを、どこから引くか」という構造だけを載せています。復旧手順で本当に効くのはこの構造の理解であって、値そのものではありません。

まとめ(次に改善したいこと + 関連記事リンク)

クラスタ内 Git は、個人運用の快適さとしては本物です。自分はやめるつもりはありません。ただ、GitOps の「Git さえあれば戻る」という安心感は、その Git がクラスタの外にもう一部あって初めて成立する——この一点を、輪が閉じる前に知っておきたかった。

自分にとっての結論はシンプルで、こうです。

クラスタ内に Git を置くのはいい。ただしそれを「唯一のコピー」にしてはいけない。そして本当に守るべきは Git ではなく、secret の金庫のバックアップと unseal key だ。

次にやること(優先順):

  1. secret 金庫のバックアップ自動化(CronJob 化)とオフサイト保管、unseal 手段の管理を固める
  2. 全 repo の GitHub push mirror 化(ミラーの取りこぼしをなくす)
  3. DISASTER_RECOVERY.md を手順・コマンドまで含めて起こす

関連記事(予定):

  • 「GitHub に依存しない: Forgejo を GitOps の中心に置く」——この記事の“建設版”。なぜ Forgejo を中心に据えたか。
  • 「Secret を Git から追い出す: OpenBao + External Secrets Operator」——この記事で最大の関門だった secret 層の詳細。
  • 「local PV と nodeAffinity で身動きが取れなくなる前に読む話」——単一ノードにデータが集中する構造の話。
  • 「バックアップと logrotate を CronJob にする」——上の宿題 1・2 の実装編。

この記事は個人 k3s / VPS 運用の連載の一部です。「これが正解」ではなく「自分の個人 k3s ではこう考えている」という記録として読んでください。あなたのクラスタでも、kubectl get applications -n argocd の source がどこを指しているか、一度確認してみると面白いはずです。

クラスタ内Gitの落とし穴|k3s GitOpsを全損から復旧する設計
http://notes.midnightstops.com/posts/30/
作者
Author
公開日
2026-07-11