個人 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 だ。
次にやること(優先順):
- secret 金庫のバックアップ自動化(CronJob 化)とオフサイト保管、unseal 手段の管理を固める
- 全 repo の GitHub push mirror 化(ミラーの取りこぼしをなくす)
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 がどこを指しているか、一度確認してみると面白いはずです。