ハイブリッド k3s · 第5回

ハイブリッド k3s #5: kubectl を手放すことにした — GitOps 1/3

この記事の原文は Zenn で公開しています。


ホームラボ全体構成図

0. このシリーズについて

このシリーズは、上の図にある「今この瞬間も動き続けているホームラボ」をどう作ってきたのかを、一編ずつ記録していく連載です。

「これ、本当に動くのか?」という素朴な疑問から始めたお遊びプロジェクトが、満足できる性能と、壊しては組み直すことの繰り返しを経て、仕事で溜まったストレスを解きほぐしてくれる本物のおもちゃになってしまいました。リソースが潤沢なクラスタではありませんが、Kubernetes をしっかり味わうには十分すぎるほどで、次に試したいことを次々と投げかけてくれます。

  • 6ノード — クラウド(AWS 東京)の Lightsail サーバー 2台(コントロールプレーン + etcd) + 自宅(札幌)の iMac 上に立てた Lima VM エージェント 4台
  • 合計 19 vCPU / 61 GiB49 ネームスペース248 Pod(うち 150 running)
  • デプロイは ArgoCD、認証は Keycloak OIDC、その上で CloudNativePG・Vault・CrowdSec・Prometheus/Grafana などが動いている

第4回まではクラスタと、その上の CloudNativePG を 命令的(helmkubectl apply) に立ててきました。今回のテーマは、これらを ArgoCD で GitOps 化 することです。ただ扱う範囲が、ツール選定からクラスタのブートストラップ、シークレット管理まで一編に収めるには広いため、三編に分けて 進めていきます。

  • 第5回(本記事)・設計 — なぜ GitOps なのか、何で実現するか(ツール・構造)を比較して決めます。
  • 第6回・ブートストラップ — ArgoCD をインストールし、app-of-apps・ApplicationSet でクラスタの骨格を立てます。
  • 第7回・適用 — 最初の対象として CloudNativePG を GitOps へ移し、シークレット(パスワード)管理まで仕上げます。

その最初の一編である本記事は、ツールと構造を決めるところまで 進めます。

1. 背景 — 命令的に立てたものが積み上がり始めた

第4回で CloudNativePG を載せたときは、二種類のコマンドで十分でした。オペレーターは helm で入れ、データベースクラスタは kubectl apply で立てました。

# 第4回 — オペレーターのインストール
helm upgrade --install cnpg cnpg/cloudnative-pg \
  --namespace cnpg-system --create-namespace --wait

# 第4回 — demo-db.yaml(Cluster CRD)の適用
kubectl apply -f demo-db.yaml

Kubernetes はオブジェクトの扱い方を大きく三つに分けています — 命令的コマンド(kubectl create ...)、命令的オブジェクト設定、そして 宣言的オブジェクト設定(kubectl apply -f) です(Kubernetes — Object Management)。

問題は、その YAML が どこにあって、誰が、いつ適用するのか でした。私の場合、そのファイルはノートPCのどこかにあり、適用するのは私自身、タイミングは「思い出したとき」でした。

これは宣言的なマニフェストを 手で運用している だけで、載せるものが一つ二つと増えるにつれ、ほどなく限界にぶつかりました。

宣言的マニフェストでも手で運用すればドリフトが起きる

1-1. 手で運用しながら突き当たった壁

DB の上にはすぐにアプリケーションが、その前段にはイングレスが、隣にはシークレットやバックアップが付いてきます。そうして複数のサービスを載せて運用していくうちに、今やこのクラスタは 49 ネームスペース・248 Pod まで増えました。この規模を手で apply しながら回していく中で、次のような壁に突き当たりました。

手で運用すると崩れる四つのこと

  • ドリフト(drift) — クラスタだけが唯一の真実になる。
    • 急ぎのときに kubectl editkubectl scale でクラスタを直接いじった瞬間、元の YAML と実際の状態がズレます。その変更はどのファイルにも残らないので、後で元の YAML を再び apply すると、静かに上書きされたり衝突したりします。
    • Kubernetes 自体は、コントローラーが 現在の状態を望ましい状態(desired state)に絶えず合わせ込む しくみで動いているのに(Kubernetes — Controllers)、肝心のその「望ましい状態」が自分の頭の中と散らばったファイルにしかなければ、合わせ込みの基準点そのものが存在しないことになります。
    • 「今動いているものが本物」なのに、その本物がコードに無いのです。
  • 履歴の不在 — 「なぜこうなっているのか」に答えられない。
    • replicas がなぜ 3 なのか、この環境変数は誰がいつなぜ入れたのか — 手作業の運用には、その記録がありません。シェルの履歴と記憶に頼るしかありません。
  • 再現不可 — クラスタを建て直せない。
    • ノードを入れ替えたりクラスタを作り直したりすると、これまでの無数の apply を順序と依存関係まで合わせて打ち直さなければなりません。第1・2回でクラスタを何度も壊しては建て直したことを思えば、これは他人事ではありませんでした。
  • 監査・協業の不在 — 立ち止まってレビューするポイントが無い。
    • 変更にレビューも、承認も、巻き戻し(revert)もありません。Enter を押せばそのまま本番に反映されます。
    • 正直、一人で使うホームラボなら、この項目は一番痛みが小さい部類です。ですが私がこのホームラボを回している本当の目的は、業務環境 — 複数人で運用する企業クラスタ — にそのまま持ち込める進め方を体に染み込ませること です。
      • チームが同じクラスタを触る瞬間、‘who / when / why’ とレビュー・承認・ロールバックは選択肢ではなく 必須 になります。
      • 変更ひとつが障害に直結しうるし、その変更をたどる記録が無ければ、復旧も責任追跡も不可能だからです。
    • GitOps はすべての変更を Git を経由させる ことで、この問題を構造的に無くします。
      • GitLab は、メイン(トランク)ブランチへのマージコミットそのものが監査ログ(audit trail)になり、Merge/Pull Request がレビュー・承認・協業の行われる地点になる、と説明しています(GitLab — What is GitOps)。
      • これは CNCF OpenGitOps が挙げる中核原則のひとつ “Versioned and Immutable”(バージョン管理され、不変な状態の履歴)とも重なります(OpenGitOps Principles)。

こうした壁に突き当たった一番大きな原因は、マニフェストが有るか無いかではなく、そのマニフェストが 真実の源(single source of truth)として一か所に集まっていて、適用が継続的に自動化されているか という点でした。

宣言的なマニフェストでも手で適用すれば、ドリフト・履歴の不在・再現不可・監査の不在が付いてきます。肝心なのは 宣言を Git の一か所に集め、適用を継続的に自動化する こと — 特に複数人で触る企業環境では、監査・協業が必須になります。

2. GitOps

2-1. GitOps とは何か

GitOps は一言でいえば、「望ましい状態(desired state)を Git に宣言しておき、クラスタを常にその宣言へ合わせ続ける運用方式」 です。

  • 一つめ、システム(インフラとアプリ)の望ましい状態を、Git を唯一の基準(single source of truth)として 宣言しておきます。
  • 二つめ、クラスタ内の ソフトウェアエージェント が、その宣言を自動的に取得して、実際の状態をそこへ収束させます。
    • 人は Git に「こうあるべき」を書くだけで、それをクラスタに反映し維持する作業はエージェントが代わりにやってくれます。

単に 「Git に YAML を置く」こと自体が GitOps なのではありません。

肝心なのは、Git を通さなければクラスタが変わらないように Git を唯一の基準として固定することです。手で kubectl を打つ道が一緒に開いていれば、Git はただのファイル置き場にすぎません。

GitOps の概念 — Git に宣言 → エージェントが自動収束

この方式は 2017 年、Weaveworks の Alexis Richardson が Operations by Pull Request という記事で初めて名付け、今では CNCF 傘下の OpenGitOps プロジェクトが、四つの原則としてその定義を標準化・管理しています(OpenGitOps)。

  • ① 宣言的(Declarative)
    • 望ましい状態を「こうしろ」(命令)ではなく「こういう状態であるべき」(宣言)として書く。
    • 命令は順序とタイミングに依存するが、宣言はいつ適用しても同じ結果へ収束する。
  • ② バージョン管理・不変(Versioned and Immutable)
    • その宣言を、Git のようにバージョンが残り、勝手に書き換わらない場所に置く。
    • すべての変更がコミットとして刻まれ、誰が・いつ・なぜ が残り、巻き戻し(revert)がそのままロールバックになる。
  • ③ 自動でプル(Pulled Automatically)
    • 人が押し込むのではなく、エージェントがその宣言を 自ら取得する
  • ④ 継続的な調整(Continuously Reconciled)
    • エージェントが実際の状態を絶えず観測し、宣言とズレれば再び合わせ込む。

2-2. なぜ GitOps なのか

  • ドリフト → ③ 自動プル + ④ 継続的調整が是正する。
    • エージェントが Git と実際の状態を絶えず突き合わせるので、誰かが kubectl edit で直接いじってズレても、次の調整で元に戻す(self-heal)。
    • 「今動いているものが本物」ではなく「Git が本物」になる。
  • 履歴・監査の不在 → ② バージョン管理が解決する。
    • すべての変更がコミット・Pull Request として残るので、誰が・いつ・なぜ が追跡でき、レビュー・承認・ロールバックの地点ができる。
    • 複数人が同じクラスタを触る企業環境で特に重要。
  • 再現不可 → ① 宣言 + ② 単一ソースが解決する。
    • クラスタ全体の望ましい状態が Git の一か所に宣言されているので、クラスタを作り直しても、その宣言を再び適用すれば同じ姿に復元される。

要するに GitOps の効用は「楽だから」ではなく、手作業の運用が構造的に抱えていた問題を、構造的に無くす からです。そしてその効用は、③・④を 誰が、どの向きで 行うか — つまり Push か Pull か — で安全性がもう一段分かれます。

2-3. Push デプロイと Pull デプロイ

変更を実際のクラスタへ届ける方式は、二つに分かれます。

Push デプロイ vs Pull デプロイ

Push デプロイ は、クラスタの外側にある CI/CD パイプラインが変更をクラスタへ押し込む方式です。パイプラインがビルド後にマニフェストを適用するのですが、gitops.tech はこの方式について「環境リポジトリが変わったときだけトリガーされ、(クラスタの)逸脱は自分では気づけない」という限界を指摘しています(gitops.tech)。

Pull デプロイ は、クラスタ のエージェント(operator)が Git を直接観測して変更を引き寄せる方式です。gitops.tech の説明どおり、operator が「環境リポジトリの望ましい状態と、デプロイ済みの実際の状態を絶えず比較し、差があればインフラを合わせる」のです。この比較と是正が止まらない点が、Push との決定的な違いです。

2-4. なぜ Pull のほうが安全で堅牢なのか

Pull が GitOps の推奨方式に挙げられるのには、二つの明確な理由があります。

第一に、セキュリティ — 認証情報がクラスタの外に出ません。

  • Push は、外部の CI がクラスタへ接続するために privileged な認証情報を握っていなければなりません。
  • 一方 Pull は、デプロイの主体がクラスタの中にあるため「外部サービスが認証情報を知る必要が無く」(gitops.tech)、接続もクラスタから外へ出ていくアウトバウンド(egress)だけで済みます。
  • CNCF も 2025 年の記事で、pull モデルは「外部からの push トラフィックにクラスタをさらさない」点をセキュリティ上の利点として整理しています(CNCF, 2025)。

第二に、self-heal — 逸脱を自分で元に戻します。

  • Pull エージェントは実際の状態と Git を比較し続けるので、誰かが kubectl edit で直接いじったドリフトも、次の調整のときに元どおりに戻します。
  • 第1節で「人が戻さなければならなかった」ドリフトを、これからはコントローラーが戻します。

3. 何で GitOps をやるか — ArgoCD vs Flux vs Fleet

第2節で、GitOps とは何か(望ましい状態を Git に宣言 → エージェントが自動で収束)、なぜそれが手作業の運用の四つの限界を埋めるのか、そしてなぜ Pull 方式のほうが安全で推奨されているのかを説明しました。

ここからは、その Pull を実際に回してくれるツール を調べていきます。

Kubernetes の GitOps ツールは数多くありますが、実務で真剣に比較対象として私が選んだのは三つです — ArgoCD・Flux CD・Rancher Fleet

三つとも「Git を単一ソースとしてクラスタをその宣言に合わせる」という本質は同じですが、コントローラーをどう構成するか、CRD をどう分けるか、UI を内蔵するか、何個のクラスタを念頭に置くか で違いがはっきり出ます。

一つずつ、概念とアーキテクチャから押さえていきます。

3-1. ArgoCD — アプリ中心の GitOps コントローラー

ArgoCD は Intuit が作って寄贈した Argo プロジェクト の一部で、公式ドキュメントは自らを「Kubernetes のための宣言的 GitOps 継続的デリバリーツール」と定義しています(Argo CD Docs)。reconcile(今の状態「actual」を望ましい状態「desired」へ合わせていく動作)を担う application-controller、Git をキャッシュ・レンダーする repo-server、API・UI を提供する server が、一つのまとまりとして動きます。

下は、最新の安定版(v3.4.3 — 私のクラスタも同じバージョン)を基準に描いた ArgoCD の詳細アーキテクチャです。API・Repo・Application の 3 コアに ApplicationSet・Redis・Dex が加わり、Repository Server が Git を pull してレンダーすると、Application Controller が live と比較してクラスタへ sync します。

ArgoCD アーキテクチャ v3.4.3

ArgoCD の個性は二つあります。

  • すべてが Application という CRD を中心に「アプリ」単位でまとまる 点。
    • 「この Git パスを、このクラスタのこのネームスペースへ同期せよ」を Application 一枚で宣言し、複数のアプリを一つが束ねる app-of-apps パターンや、アプリをテンプレートから自動生成する ApplicationSet で規模を広げます。
  • 充実した Web UI を標準で内蔵 する点。
    • 公式ドキュメントが「アプリケーションの活動をリアルタイムに見せる Web UI」を中核機能に挙げ、同期状態・diff・ロールバックはもちろん、SSO(OIDC)・RBAC まで画面で扱えます(Argo CD Docs)。成熟度も堅実です。Argo は 2020 年に CNCF Incubating に入り、2022 年 12 月 6 日に Graduated(卒業) 段階へ上がりました(CNCF — Argo Graduated)。

3-2. Flux CD — 組み合わせ型の GitOps ツールキット

Flux はもともと Weaveworks が作ったツールで、v2 で Kubernetes の controller-runtime と独自の GitOps Toolkit の上に書き直されました。公式サイトは Flux を「Kubernetes のための、オープンで拡張可能な継続的・漸進的デリバリーソリューションの集合(set)」と紹介しています(Flux)。

この「集合」という表現が、Flux の特徴をよく表しています。

ArgoCD が一まとまりのコントローラー群だとすれば、Flux は目的ごとに分割した複数のコントローラーの組み合わせ です。

下は、最新版(v2.8)の公式ドキュメントを基準にした Flux の詳細アーキテクチャです。

六つのコントローラーがそれぞれ CRD を持ち、source-controller がソースを pull してアーティファクトとして公開すると、kustomize/helm コントローラーが SSA で適用し、image コントローラーは新しいイメージを Git へコミットし返してループを閉じます。

Flux CD アーキテクチャ v2.8

具体的には source-controller(Git・Helm・OCI・S3 などソース取得)、kustomize-controller(Kustomize 適用)、helm-controller(Helm リリース)、notification-controller(通知)、そしてイメージ自動更新のコントローラー群が、それぞれ自分の CRD(GitRepositoryKustomizationHelmRelease など)を持って協調します(Flux)。

このように細かく分かれた構造は、組み合わせ・拡張が自由でクラスタのフットプリントが軽いという利点がある一方、公式に内蔵された Web UI が無い という違いがあります。

状態確認は主に CLI(flux)で行い、画面が必要ならエコシステムの別 UI やベンダーのホスティング製品を付けます(Flux)。

成熟度は ArgoCD と互角です。Flux も 2022 年 11 月 30 日に CNCF Graduated 段階へ上がっており(CNCF — Flux Graduated)、二つのツールは卒業の時期すら一週間と離れていない、肩を並べる成熟度を持っています。

3-3. Rancher Fleet — 数百クラスタのための GitOps

三つめは SUSE の Rancher Fleet です。前の二つとは出発点が違います。

ArgoCD・Flux が「一つのクラスタの GitOps」から出発してマルチクラスタへ広げていく側だとすれば、Fleet は最初から「大規模なマルチクラスタ」を狙って 設計されています。

AWS のガイド文書も Fleet を「単一クラスタから数千個まで拡張できるよう作られた GitOps-at-scale」ツールとして紹介しています(AWS — Rancher Fleet)。

動作モデルもその目的に合わせてあります。管理(アップストリーム)クラスタの Fleet Manager が、GitRepo で指したリポジトリの内容を Bundle にパッケージし、グループ・ターゲットの設定に応じて多数の ダウンストリームクラスタへファンアウト(fan-out) します(Fleet — Mapping to Downstream Clusters)。

運用はたいてい Rancher の Continuous Delivery 画面で一元管理します。データセンター・リージョン・顧客ごとにクラスタが数十・数百個と散らばる MSP や大企業には強力なモデルです。

ただし Fleet は ArgoCD・Flux と違い、CNCF プロジェクトではなく SUSE Rancher エコシステムの一部 である点も併せて見ておく必要があります。

下は、最新版(v0.15.0)の公式ドキュメントを基準にした Fleet の詳細アーキテクチャです。

アップストリームの gitjob・fleet-controller が Git を Bundle に加工してターゲットごとの BundleDeployment を作り、各ダウンストリームの fleet-agent がアウトバウンドで引き寄せて適用します(コントローラーがダウンストリームへ先に接続しないので、NAT・ファイアウォールの背後でも動作します)。

Rancher Fleet アーキテクチャ v0.15.0

3-4. 一つの表で比較

三つのツールを同じ軸に並べると、違いがはっきりします。

GitOps コントローラー比較 — ArgoCD vs Flux vs Fleet

項目ArgoCDFlux CDRancher Fleet
一言での定義アプリ中心の GitOps コントローラー組み合わせ型 GitOps ツールキット大規模マルチクラスタ GitOps
コントローラー構成一まとまり(controller・repo・server)目的別コントローラーの組み合わせ(GitOps Toolkit)Fleet Manager → ダウンストリーム
主要 CRDApplicationApplicationSetGitRepositoryKustomizationHelmReleaseGitRepoBundle
Web UI内蔵(状態・diff・ロールバック・OIDC/RBAC)公式の内蔵なし(CLI + エコシステム UI)Rancher UI に統合
主なターゲット規模単一~複数クラスタ単一~複数クラスタ数百~数千クラスタ
ガバナンスCNCF Graduated(2022-12)CNCF Graduated(2022-11)SUSE Rancher(非 CNCF)
強みアプリ単位の可視性・UI軽量・組み合わせ/拡張性大規模クラスタへのファンアウト

成熟度(どちらも CNCF Graduated)や Pull・self-heal といった中核動作は、ArgoCD と Flux で実質的に互角です。本当の分かれ道は 「アプリを画面で見ながら扱うか(ArgoCD) vs コントローラーを組み合わせて CLI で扱うか(Flux)」、そして Fleet の場合は 「クラスタが何個か」 でした。

3-5. では、なぜ ArgoCD だったのか

私の選択は ArgoCD でした。ただしこれは「三つのうちどれが優れているか」ではなく、私のホームラボの文脈 に沿った決定でした。

なぜ ArgoCD だったのか — ホームラボ基準の決定

基準は三つでした。

  • 第一に、私の環境はクラスタが一つ です(6 ノードですが単一クラスタ)。
    • 数百クラスタへファンアウトする Fleet の強みは私には使い道が無く、むしろその管理モデルは過剰です。
  • 第二に、私は 「何がズレたのかを”目で”見たい」 という要望が強くありました。
    • ドリフトや sync の状態、diff やロールバックを画面で即座に確認できる ArgoCD の内蔵 UI は、CLI 中心の Flux より学習でも運用でも直感的でした。
  • 第三に、このホームラボの目的が いつか業務環境へ持ち込む進め方を身につけること である以上、資料・事例が豊富で寿命の保証されたツールが必要でした。
    • 実際、プロジェクト参加時に使って馴染みのあるツールでもあり、もっと深く掘り下げたいツールでもありました。
    • ArgoCD は CNCF Graduated である上、CNCF の 2025 年 ArgoCD エンドユーザー調査 で、回答者のクラスタの約 60% が ArgoCD でアプリケーションをデプロイしていると答えるほど採用率が圧倒的でした(CNCF, 2025)。

Flux も優れたツールです。同等の成熟度(CNCF Graduated)でより軽く、組み合わせが自由なので、CLI・コード中心で運用を自動化したいチームには、むしろこちらのほうが合うかもしれません。私自身も UI 操作より CLI での操作を好みますが、「アプリを画面で見ながら扱いたい、単一クラスタを長く回しながら業務への移行を練習する」 という私の条件には、ArgoCD のほうがもう一歩ぴったり合っただけです。今このクラスタでは、ArgoCD が 79 個のアプリを reconcile しながら動いています。

ArgoCD reconcile ループ

$ kubectl get statefulset,deploy -n argocd
NAME                                             READY
statefulset.apps/argocd-application-controller    1/1     # reconcile エンジン(pull・比較・sync)
deployment.apps/argocd-repo-server                1/1     # Git マニフェストのキャッシュ・レンダー
deployment.apps/argocd-server                     1/1     # API / UI
deployment.apps/argocd-applicationset-controller  1/1     # 後の回で扱う自動生成器
# … dex / redis / notifications / image-updater

$ kubectl get applications -n argocd --no-headers | wc -l
79

そしてこの reconcile には selfHeal も有効になっているのが確認できます。

$ kubectl get application root -n argocd -o jsonpath='{.spec.syncPolicy.automated}'
{"prune":true,"selfHeal":true}

4. どんな構造で ArgoCD を立てるか — レンダリング・組織化・リポジトリ・アクセス

ArgoCD に決めたからといって、すぐに第4回の CloudNativePG を Git に載せられるわけではありません。GitOps の流れをたどってみると —

Git に 何を・どう 書いておき → ArgoCD がそれを どう取得し → クラスタへ適用するか — その経路の上に、決めるべきポイントが順に現れます。整理すると四つです。

  1. マニフェストを どんな形式 で書くか — レンダリング
  2. そうして書いたアプリを ArgoCD に どう登録・管理 するか — 組織化
  3. それらのファイルを どこに(どんなリポジトリ構造で) 置くか — リポジトリ
  4. ArgoCD がそのリポジトリに どうアクセス するか — アクセス(認証)

この四つを決めて初めて、CNPG を GitOps へ移す「構造」が立ちます。ここからは軸ごとに、それが何で、なぜ決める必要があり、どんな候補があり、何を基準に選ぶのか を見ていきます。

4-1. マニフェストのレンダリング — Kustomize vs Helm vs plain

まず「マニフェストのレンダリング」とは何かから押さえます。

Kubernetes に何かを立てるには、結局その対象を書いた YAML(マニフェスト) が必要です — Deployment 一つ、Service 一つ、ConfigMap 一つ、といった具合に。

ところが同じアプリでも環境ごと(dev/prod)に replicas やイメージタグが変わり、似たアプリが何セットも増えていきます。このとき「元をどう書いておき、クラスタに実際に入る 最終的な YAML をどう作り出すか」 — この過程を レンダリング と呼びます。

ArgoCD はこのレンダリング方式を一つに強制せず、リポジトリのパスにあるファイルを見て自動で判別します。

kustomization.yaml があれば Kustomize、Chart.yaml があれば Helm、どちらも無ければ素の YAML(plain)として処理します(Argo CD Docs)。ですから私たちが決めるのは「自分のマニフェストを、この三つのどれで書くか」です。同じ目標(同じアプリを dev は replicas 1、prod は 3 に)を題材に、三つがどう違うかを一つずつ見ます。

plain YAML — 加工なしでそのまま

plain は文字どおり加工がありません。deployment.yamlservice.yaml のように値がすべて埋まった完成 YAML をディレクトリに置けば、ArgoCD の repo-server がそれを 変形せずそのまま クラスタへ適用します。

レンダリングの段階がそもそも無いので、Git に書かれた文字がそのままクラスタの状態 であり、何がデプロイされるか迷いなく読めて、別に覚える文法もありません。第1節で見た「宣言的オブジェクト設定(kubectl apply -f)」そのものです(Kubernetes — Object Management)。

問題は 繰り返し です。

dev と prod のように一、二行だけ違う環境を持つには、ファイルを丸ごとコピーして(別ディレクトリに分けるなどして)その行だけ直さなければならず、共通のイメージタグ一つを上げるときも、コピーしておいたすべてのファイルを一つずつ手で直さなければなりません。

対象が増えるほど、重複と漏れ(片方だけ直してしまうミス)が急速に溜まります。下の図がその限界を見せます — ほとんど同じ二つのファイルが、replicas 一行のために別々に存在しています。

plain YAML のレンダリング

Kustomize — base + overlay で重ねて書く(テンプレートなし)

Kustomize はその繰り返しを コピペなしで 解く道具です。

Kubernetes 公式ドキュメントはこれを「kustomization ファイルを通じて Kubernetes オブジェクトをカスタマイズする独立したツール」と定義し、1.14 から kubectl に内蔵されていて kubectl apply -k ですぐに使えます(Kubernetes — Kustomize)。中核となる概念は base と overlay です。

共通のマニフェストを base/ に一セットだけ置き(例:replicas 1 の Deployment)、環境ごとに変わる部分だけを overlays/prod/kustomization.yamlpatch として書きます(例:「replicas を 3 に」「名前の前に prod- を付ける」)。すると kustomize build が base を読み、overlay の patch を上書きして最終的な YAML を作ります。

決定的な特徴は テンプレート言語が無い 点です — {{ }} のような変数の置換ではなく、YAML の上に YAML をマージ して、また別の素の YAML を作るので、出来上がった YAML がそのまま読め、「何がどう変わったか」が目に見えます。

共通値(イメージタグなど)は base の一か所だけ直せばすべての overlay に反映されるので、plain のコピペ問題が消えます。代わりに、条件分岐や繰り返し生成のような複雑な表現は苦手です。

下の図は、base 一セット + prod overlay の patch が、どう最終的な YAML へ合わさるかを実際のファイルで見せます。

Kustomize のレンダリング

Helm — チャートと values で変数化(テンプレートエンジン)

Helm はアプローチが違います。

自らを「Kubernetes のパッケージマネージャー」と名乗り(Helm)、アプリケーションを チャート(chart) というパッケージにまとめます。チャート内 templates/ のマニフェストには値を直接書かず、{{ .Values.replicas }} のような Go テンプレートの変数の置き場所 を置き、実際の値は values.yaml に分けて書きます。

デプロイ時に Helm が values をその置き場所へ差し込んで(置換して)最終的なマニフェストをレンダーします。

なので チャートはそのままで values だけ差し替えれば、dev・prod はもちろん別のクラスタまで、同じチャートでデプロイできます。

さらに条件文(if)・繰り返し(range)・共通ヘルパー(_helpers.tpl)・他チャートへの依存(subchart)まで対応し、表現力が最も強いです。

そのため CloudNativePG・Vault のように 他人が公開した複雑なソフトウェアをチャートごと引っ張ってきて、values だけ自分の環境に合わせて変えて使う のに特に向いています(公開チャートはたいてい公式のチャートリポジトリから取得します)。

テンプレート言語が一枚挟まるので「Git に書かれたテキスト」と「実際に適用される YAML」が一段離れます。なので helm template でレンダー結果を事前に展開して確認する習慣が要ります。

下の図は、変数の置き場所({{ }})が values の値に置換されて最終的な YAML になる過程を見せます。

Helm のレンダリング

項目plain YAMLKustomizeHelm
処理方式なし(そのまま適用)base + overlay のパッチ・マージGo テンプレートの変数置換
テンプレート言語なしなしあり({{ }})
変数化・表現力なし中(パッチ・フィールド注入)高(条件・繰り返し・依存)
結果の透明性最も高い(元=結果)高い(YAML→YAML)低い(レンダーしないと見えない)
kubectl 内蔵適用のみ(-f)はい(-k)いいえ(別ツール)
再利用・配布低い(コピペ)中(base の再利用)高い(チャート・リポジトリで共有)
学習・複雑さ最低低い中~高
向いている所少量の固定リソース自分で直接宣言するマニフェスト外部の複雑な公開チャート

整理すると、三つは優劣ではなく 用途 が違います — 静的で少数のリソースなら plain、自分で直接宣言するマニフェストならテンプレートなしで読みやすい Kustomize、外部の複雑な公開チャートを取り込むなら Helm が自然です。

4-2. アプリの組織化 — app-of-apps vs ApplicationSet

次は「そうして書いたマニフェストを ArgoCD にどう登録するか」です。

ArgoCD はデプロイの単位を Application という CRD で扱います。

「この Git パスを、このクラスタのこのネームスペースへ同期せよ」を一枚に書いたものですね。アプリが一、二個なら、この Application を手で一枚ずつ書けば済みます。ですが載せるものが数十個に増えると、Application を作る作業そのもの が仕事になり、抜けたりバラバラになったりしがちです。

そこで「Application をどう体系的に(できれば自動で)作るか」を決めなければなりません。ArgoCD には二つの道があります。

app-of-apps は、公式ドキュメントの表現そのまま「他のアプリだけで構成される ArgoCD アプリを一つ宣言する」パターンです(Argo CD — Cluster Bootstrapping)。親となる root Application 一つに子の Application のリストを書いておき、その root だけ同期すれば、子が次々と作られます。

「クラスタの骨格を一度に立てる」ブートストラップの入口に向いています。ただし子のリストを 直接書いておく 方式なので、新しいアプリができるたびに子 Application を手で一つ追加しなければなりません。

ApplicationSet はそこから一歩進み、公式ドキュメントの表現で「多数のクラスタ・アプリにまたがって Application を自動化し、柔軟に管理する」コントローラーです(Argo CD — ApplicationSet)。

中核は generator です。

generator がパラメータを生み出し、そのパラメータを一つのテンプレートに差し込んで Application を 刷り出します。generator には、リストを直接与える list、登録済みのクラスタをなめる cluster、リポジトリのフォルダ・ファイルをなめる git、二つを掛け合わせる matrix などがあります。

特に git generator は、決めておいたパス(例:apps/*)のフォルダごとに Application を自動で作ってくれるので、新しいフォルダを追加するだけでアプリが勝手にできます

人が Application を直接作る必要がありません。

アプリの組織化 — app-of-apps vs ApplicationSet

二つは競合するというより、レイヤーが違います。 app-of-apps は「最初の入口を一つ」作るのに、ApplicationSet は「その下でアプリを大量に量産する」のに強いです。

なので、よく root(app-of-apps)が ApplicationSet 群を立て、各 ApplicationSet がフォルダをなめて実際のアプリを刷り出す、という形で一緒に使います。

4-3. リポジトリ構造 — monorepo vs polyrepo

三つめは「その宣言を どのリポジトリに 置くか」です。

ここには順序があります。先に押さえる 原則 が一つあり、その後で そのリポジトリを一つにするか複数に分けるか を決めます。

原則は、config(マニフェスト)をアプリのソースコードと分離する ことです。

ArgoCD 公式ガイドは 「Kubernetes マニフェストは、アプリケーションのソースコードと分離した別の Git リポジトリに置くことを強く推奨」 すると明言しています(Argo CD — Best Practices)。理由は次のとおりです。

  • ① ソースと config が同じリポジトリにあると、config を変えただけでもアプリのビルド CI が回り直す無限ループが起きやすい。
  • ② デプロイ履歴(config のコミット)と開発履歴(ソースのコミット)が混ざり、監査ログが散らかる。
  • ③ 「コードを触る人」と「本番へデプロイする人」の権限を分けにくい。

その「config リポジトリ」を、さらにどう置くかが残ります。

  • monorepo は、クラスタ全体の宣言を一つのリポジトリに集め、フォルダ(platform/workloads/…)で区切る方式
    • 変更の全体像が一目で見渡せ、ApplicationSet の git generator も一つのリポジトリだけなめれば済むのでシンプル。
    • 代わりに組織が非常に大きくなると、「このフォルダはこのチームだけ」のように細かく権限を分けるには限界がある。
  • polyrepo は、チーム・ドメインごとに config リポジトリを分ける方式
    • リポジトリ単位でアクセス権限をきれいに分けられますが、クラスタ全体を一目で見たり、複数のリポジトリにまたがる変更をしたりは煩雑になります。(この二つの優劣は、正解があるというより組織の規模・権限要件によるトレードオフです。)

リポジトリ構造 — ソース分離・monorepo vs polyrepo

4-4. リポジトリアクセス — HTTPS vs SSH デプロイキー vs GitHub App

最後は ArgoCD がその(たいてい非公開の)リポジトリをどう読むか です。

第2節で見た Pull モデルどおり、読む主体である ArgoCD の repo-server はクラスタの中にあり、外へ出ていくアウトバウンド接続だけを使います。それでも非公開リポジトリを読むには認証情報が要りますが、方式ごとに 権限が及ぶ範囲読み取り専用に絞れるか が違います。公式ドキュメントは HTTPS(ユーザー/トークン)、SSH 秘密鍵(デプロイキー)、GitHub App、TLS クライアント証明書などに対応しています(Argo CD — Private Repositories)。

  • HTTPS · Personal Access Token
    • トークンをパスワードのように使って繋ぎます。
    • 最も手軽ですが、トークンがアカウント単位で複数リポジトリに届くよう広がりやすく、たいてい読み/書きの権限を併せ持ちます。
    • 漏れると影響範囲が大きいです。
  • SSH · デプロイキー(Deploy Key)
    • GitHub 公式ドキュメントはデプロイキーを 「単一のリポジトリへのアクセスを与える SSH キー」 と定義し、「デフォルトは読み取り専用で、追加時に書き込み権限を与えられる」 と明記しています(GitHub — Deploy keys)。
    • つまり そのリポジトリ一つに範囲が限定 され、読み取り専用で発行 できるので、最小権限の原則に最もよく合います。
    • 公開鍵をリポジトリの deploy key として登録し、秘密鍵を ArgoCD に入れておけば済みます。
  • GitHub App
    • インストール単位で 選んだリポジトリ・権限にだけ 届く、きめ細かな方式です。
    • インストールトークンは約 1 時間で失効する短命トークンなので自動更新しながら使い(GitHub — App installation auth)、監査・失効の管理がきれいです。
    • 組織・多数リポジトリの規模に向きますが、初期設定がやや複雑です。

リポジトリアクセス — HTTPS・SSH デプロイキー・GitHub App

どの方式でも、認証情報はクラスタの中だけに置き、外へ出ていく接続だけを使う点は同じです。分かれるのは「権限をどれだけ絞れるか」で、リポジトリが一つなら 読み取り専用でそのリポジトリだけに限定される デプロイキーが、最もシンプルで安全です。

4-5. 私のホームラボの構成は? — 何を、なぜ、そして何を得るか

私の構成の性格と拡張 — シンプル・安全・再現、軸ごとの拡張

  • レンダリング
    • Kustomize を基本 + Helm を併用。
    • 自分で直接宣言するリソースは環境ごとの分岐がほとんど無いので、テンプレート言語なしで 結果の YAML がそのまま見える Kustomize がシンプルでデバッグしやすかったです。
    • 逆にオペレーターのように 他人がよく作って公開したチャート は、わざわざほどいて移さず Helm でそのまま取り込みます。
    • 自分が書くものは透明に、他人が作ったものは再利用で — 管理の負担が両方とも最小になります。
  • 組織化
    • app-of-apps + ApplicationSet。
    • root(app-of-apps)をブートストラップの入口に据え、その下の ApplicationSet 群が git generator でフォルダをなめてアプリを量産します。
    • アプリを増やすとき Application を手で作らなくてよく(フォルダを足すだけ)、クラスタを建て直しても root 一つで全体が復元されます。
    • 再現性と拡張性が一緒に付いてきます。
  • リポジトリ
    • ソースと分離した config monorepo。
    • マニフェストはアプリのソースと分離しつつ(公式推奨)、単一クラスタのホームラボでは、その config を一つのリポジトリに集めました。
    • CI ループ・履歴の混在・権限の問題を分離で避けつつ、変更の全体を一目で見て、generator もシンプルに回します。
  • アクセス
    • 読み取り専用 SSH デプロイキー。
    • 認証情報をクラスタの中だけに置く Pull モデルに加えて、そのリポジトリ一つにだけ、読み取り専用で 権限を絞りました。
    • 鍵が漏れても、そのリポジトリを読む以上のことはできないので、被害範囲が構造的に閉じ込められます。

この構成の 性格 は一言でいえば 「単一クラスタ・一つの config monorepo・読み取り専用 pull」 — シンプルで、安全で、丸ごと再現できます。

同時に、軸を一つずつ変えるだけで企業環境へ拡張できる土台でもあります(リポジトリを polyrepo へ、アクセスを GitHub App へ、対象をマルチクラスタへ)。

# ArgoCD のリポジトリ接続シークレット — キー構成を見るだけで方式が分かる
$ kubectl get secret repo-seonology-k3s -n argocd -o jsonpath='{.data}' | jq 'keys'
[
  "sshPrivateKey",   # SSH デプロイキーで接続 — 認証情報はクラスタの中だけに
  "type",            # git
  "url"              # リポジトリはただ一つ = monorepo
]

私の GitOps 構成 — monorepo からクラスタまで

これらの決定が積み重なって、第4回で命令的に立てた CloudNativePG を GitOps へ移す 準備 が整いました。次回(第6回)では、この設計を実際に手で立てていきます。

config リポジトリを作る → ArgoCD をインストール → 読み取り専用デプロイキーでリポジトリを接続 → root(app-of-apps)と ApplicationSet の骨格を立てる まで。

そして第7回で、その上に CNPG を載せ、オペレーターは Helm で・Cluster CR は Kustomize で宣言して GitOps で回すところまで書いていきます。

ArgoCD を使うと決めた後も、四つを決める必要があります — レンダリング(Kustomize を基本 + Helm を併用)、組織化(app-of-apps でブートストラップ + ApplicationSet で量産)、リポジトリ(ソースと分離した config monorepo)、アクセス(読み取り専用 SSH デプロイキー)。それぞれの選択の理由は「シンプルで、安全で、再現できるように」へ一つに集まり、同時に軸を一つずつ変えるだけで企業環境へ拡張できる土台になります。

5. おわりに — そして次へ

今回は、コマンド(kubectl apply)を一行も増やしませんでした。

代わりに、そのコマンドを手放すための 設計 を終えました。手で運用するときに崩れていた四つ(ドリフト・履歴・再現・監査)を GitOps の四原則がそのまま埋めることを確かめ(第1・2節)、その Pull を回してくれるツールに ArgoCD を選び(第3節)、その上にマニフェストを どう書き・束ね・置き・読むか を四つの軸で決めました(第4節)。

ばらして見ると決定が多く見えますが、すべて一つの方向に集まります — シンプルで、安全で、再現できるように。

レンダリングは結果がそのまま見える Kustomize を基本に、他人が作った複雑なチャートは Helm で取り込み、組織化は root 一つで全体が復元される app-of-apps + ApplicationSet、リポジトリはソースと分離した config monorepo、アクセスはそのリポジトリ一つだけ読む read-only デプロイキー。

単一クラスタを最もシンプルかつ安全に回す出発点であり、同時に軸を一つずつ変えるだけで企業環境へ拡張できる、という作戦です。

設計が終わったので、次回からはこれを実際に手で立てていきます。

  • 第6回・ブートストラップ — ArgoCD をインストールし、読み取り専用デプロイキーで config リポジトリを接続したうえで、root(app-of-apps)と ApplicationSet でクラスタの骨格を立てます。そして誰かが kubectl edit で直接いじった変更を、ArgoCD がどう元へ戻すか(self-heal)を目で確かめます。
  • 第7回・適用 — 第4回で命令的に立てた CloudNativePG を GitOps へ移します。オペレーターは Helm で、Cluster CR は Kustomize で宣言し、最後に残った宿題であるシークレット(パスワード)管理まで仕上げます。

kubectl apply を手で打っていた場所を、これからはコミット一つが肩代わりするようにする番です。

参考 / 出典