はじめに
前編では、Raspberry Pi 5 ×6 で k3s の HA クラスタ(server3 + agent3、embedded etcd) を SSD 起動で組み、kubectl get nodes で6台 Ready になるところまでを作りました。
本記事(後編)は、その立ち上がったクラスタを実際に使える監視基盤にするところです。
■この記事でできること(後編=GitOps・監視)
- クラスタに ArgoCD を入れて app-of-apps で GitOps 化
- MetalLB(LoadBalancer の払い出し)と Longhorn(分散ストレージ)を土台として敷く
- VictoriaMetrics スタック / Grafana を載せ、「Git に push すれば監視基盤が反映される」状態にする
前提: 前編の手順で 6台 Ready の k3s HA クラスタが立っていること。手元PCで
kubectl get nodesが通り、k3s 起動時に--disable servicelb済み(MetalLB と競合させないため)であること。注: 本記事の IP・ホスト名・トークンはすべてダミー(
10.0.0.x/<TOKEN>/<deploy-token>等)です。自分の環境を公開するときは伏字を必ず確認してください。
完成構成
「手元PCで Git に push → ArgoCD が検知 → クラスタへ反映」この一方通行を作るのが本記事のゴールです。
1. STEP 2: ArgoCD の導入
kubectl create namespace argocd
kubectl apply -n argocd \
-f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
stable は最新安定版を指します(私の環境は v3.2.5 で固定運用中)。再現性を重視するなら stable をタグ(例 v3.2.5)に置き換えてください。ARM64 マルチアーキ対応イメージなので RPi5 でそのまま動きます。初期管理者パスワードを取得し、ポートフォワードでUIへ(この時点では MetalLB 未導入でも port-forward で UI に入れます)
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath="{.data.password}" | base64 -d; echo
kubectl -n argocd port-forward svc/argocd-server 8080:443
# https://localhost:8080 (user: admin)
ログインするとこの画面。次の STEP 3 で app-of-apps を流すと、ここにアプリが並んでいきます(下の画面は私の実環境で、最終的に14アプリが Synced / Healthy になった状態)
2. STEP 3: GitOps 化(app-of-apps)
Git リポジトリを「クラスタの状態の唯一の正」にします。ここではまずリポジトリの「器」だけ作り、各ファイルの中身は §3 で1つずつ作りながら push していきます。推奨ディレクトリ構成:
本記事の
repoURLは、クラスタとは別の Raspberry Pi 5 上に立てた自宅セルフホスト GitLab(例では10.0.0.30)を指しています。GitHub など外部の Git でも手順は同じで、repoURLを差し替えるだけです。「GitOps の Git そのものを別Pi5の GitLab で完結させる」話は1本書けるので、構築の詳細は別記事にします。
正直な所・・・ この GitLab は1台構成なので単一障害点です。しかも GitLab は GitOps の Git ソースであると同時に、自作アプリのコンテナレジストリも兼ねています。つまり GitLab が落ちると、ArgoCD の同期が止まるだけでなく、Pod が再起動したときにイメージを pull できず ImagePullBackOff になります(k3s 側は HA でも、ここが弱点)。この GitLab 自体も冗長化する予定で、その話は別記事で扱います。
homelab-gitops/
├─ apps/ # 各アプリのArgoCD Application定義
│ ├─ metallb.yaml # ネットワーク基盤(sync-wave -1:最初に)
│ ├─ metallb-config.yaml # MetalLBのIPプールCRを適用(wave 0)
│ ├─ longhorn.yaml # ストレージ基盤(StatefulSetより先に)
│ ├─ victoria-metrics.yaml # k8s-stack(Grafana同梱)
│ ├─ victoria-logs.yaml
│ ├─ victoria-traces.yaml
│ └─ myapp.yaml # (任意) 自作アプリ用。無ければ削って進めてOK
├─ root-app.yaml # app-of-apps(apps/配下を束ねる)
└─ manifests/ # 各アプリのHelm values / kustomize
└─ metallb/ # IPAddressPool / L2Advertisement のCR
myapp.yaml は自作アプリ用の枠です。無ければ無視して進めてOK(自作アプリは別記事で詳しく書きます)。
以降のコード内の
<you>(GitLab のユーザー/グループ名)・<user>/<deploy-token>・10.0.0.30(GitLab の IP)などのプレースホルダは、すべて自分の環境の値に書き換えてください。
リポジトリを用意して push するまでの流れ(初回だけ):
1. GitLab に空のプロジェクトを作る(例: homelab-gitops)。GitHub など外部の Git でも同じ。
2. 手元PCでローカルリポジトリを作り、上の構成でファイルを置いて push する(apps/*.yaml と root-app.yaml の中身は §3 で作っていきます)。
git init homelab-gitops && cd homelab-gitops
mkdir -p apps manifests/metallb
# apps/ に metallb.yaml / longhorn.yaml / victoria-metrics.yaml … を作成(§3参照)
# root-app.yaml も配置(下記)
git add -A && git commit -m "init gitops"
git branch -M main # targetRevision を main に合わせる
git remote add origin http://10.0.0.30/<you>/homelab-gitops.git # 自宅GitLabのIPに置き換え
git push -u origin main
3.(リポジトリが private なら)ArgoCD にアクセス情報を登録する。LAN内の GitLab でも private なら認証が要ります。
# ArgoCD CLI の例(Web UI の Settings → Repositories でも同じことができる)
argocd repo add http://10.0.0.30/<you>/homelab-gitops.git \
--username <user> --password <deploy-token>
ハマったところ これを飛ばすと、後述の root-app が ComparisonError(認証エラー)でずっと同期できません。「プロジェクトを public(LAN内限定)にする」か「上の repo 登録を済ませる」かのどちらかを必ずやってください。
4. 親となる root-app を一度だけ kubectl apply する。これ以降は Git へ push するだけで全アプリが同期されます(root-app が apps/ を監視し、子 Application を自動生成)。
📄 作成するファイル: root-app.yaml(リポジトリのルート)
# root-app.yaml (リポジトリのルートに置く)
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root-app
namespace: argocd
spec:
project: default
source:
repoURL: http://10.0.0.30/<you>/homelab-gitops.git # 自宅GitLabのIP。GitHub等なら差し替えるだけ
targetRevision: main
path: apps
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
kubectl apply -f root-app.yaml
3. STEP 4: MetalLB・Longhorn・監視スタック・自作アプリをデプロイ
apps/ 配下に Application を置くと、ArgoCD が拾って同期します。
3-1. まず MetalLB(LoadBalancer の払い出し基盤)
servicelb を切ったので、type: LoadBalancer に EXTERNAL-IP を振るのは MetalLB の役目です。Helm で導入し、L2 モードで LAN の空きIP帯を払い出します。
用語:
sync-waveは ArgoCD の同期順序の指定。数字が小さいほど先に同期されます(-1は他より先)。MetalLB やストレージのような土台を先に立てたいときに使います。
📄 作成するファイル: apps/metallb.yaml
# apps/metallb.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: metallb
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "-1" # 他アプリより先に同期
spec:
project: default
source:
repoURL: https://metallb.github.io/metallb
chart: metallb
targetRevision: 0.14.8
destination:
server: https://kubernetes.default.svc
namespace: metallb-system
syncPolicy:
automated: { prune: true, selfHeal: true }
syncOptions: [ CreateNamespace=true ]
コントローラ起動後に、払い出すIPプールを CR(CustomResource=MetalLB が k8s に追加したリソース種別。ここでは IPAddressPool と L2Advertisement)で渡します。まず CR を manifests/metallb/ に置きます。
📄 作成するファイル: manifests/metallb/pool.yaml
# manifests/metallb/pool.yaml
apiVersion: metallb.io/v1beta1
kind: IPAddressPool
metadata:
name: lan-pool
namespace: metallb-system
spec:
addresses:
- 10.0.0.240-10.0.0.250 # ルータのDHCP範囲とかぶらない空き帯を指定
---
apiVersion: metallb.io/v1beta1
kind: L2Advertisement
metadata:
name: lan-l2
namespace: metallb-system
spec:
ipAddressPools: [ lan-pool ]
この CR の適用も ArgoCD に任せます。manifests/metallb/ を指す Application を1つ置き、MetalLB 本体(sync-wave -1)より後(0) に同期させます(コントローラが起動してから CR を入れる順番にするため)。
📄 作成するファイル: apps/metallb-config.yaml
# apps/metallb-config.yaml(IPプールCRを適用するApplication)
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: metallb-config
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "0" # MetalLB本体(-1)の後に
spec:
project: default
source:
repoURL: http://10.0.0.30/<you>/homelab-gitops.git # 自宅GitLab(§2と同じ)
targetRevision: main
path: manifests/metallb # pool.yaml を置いたディレクトリ
destination:
server: https://kubernetes.default.svc
namespace: metallb-system
syncPolicy:
automated: { prune: true, selfHeal: true }
ハマりどころ: MetalLB の IP プールは、ルータの DHCP 払い出し範囲と必ず分けること。重複するとIP衝突で通信が不安定になる。L2 モードは同一サブネット前提なので、VLAN を切っている場合は L2Advertisement の interface 指定も確認する。
補足: 私の環境は Ingress を立てず「1サービス=1 IP」で公開している
--disable traefik で Ingress コントローラを切ったあと、結局そのまま Ingress を入れていません。Grafana も ArgoCD も VictoriaMetrics の各エンドポイントも、すべて type: LoadBalancer で MetalLB から 1サービス1個ずつIPを払い出して直公開しています(私の環境では20個強のLB IPが出ている)。
- メリット: L7 ルーティングやTLS終端の設定が要らない。サービス追加=IPが1つ増えるだけで、設定がフラットで事故りにくい
- デメリット: IPを食う。
https://<IP>:<port>直アクセスなので、ホスト名でまとめたり証明書を集約したりはできない - L7 が欲しくなったら(パスベースの集約、Let's Encrypt のTLS集約など)、そのとき初めて ingress-nginx などを1つ立てて、その Service だけ LoadBalancer にするのが素直
ホームラボは LAN のIPに余裕があるので、まずは LB 直公開がいちばんシンプルです。「とりあえず Ingress」を急がない、というのが今の私の割り切りです。
確認コマンド: 現状 Ingress を使っているかは
kubectl get ingress,ingressclass -Aで分かる(私の環境は両方ゼロ)。LBで何が公開されているかはkubectl get svc -A | grep LoadBalancer。
3-2. ストレージ(Longhorn)— StatefulSet を載せる前に
監視スタックは vmstorage / VictoriaLogs などステートフルな Pod が多く、PVC を要求します。k3s 標準の local-path はそのノードのローカルディスクに固定されるため、ノードが落ちるとデータごと巻き添えになります(HA の意味が薄れる)。そこで私の環境では分散ブロックストレージの Longhorn を入れ、レプリカを複数ノードに分散させています。
📄 作成するファイル: apps/longhorn.yaml
# apps/longhorn.yaml(MetalLBと同じ Application 形式。監視スタックより先に同期)
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: longhorn
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "-1" # MetalLB と同じく先行させる
spec:
project: default
source:
repoURL: https://charts.longhorn.io
chart: longhorn
targetRevision: 1.7.2 # 自分のクラスタの実値で確認
destination:
server: https://kubernetes.default.svc
namespace: longhorn-system
syncPolicy:
automated: { prune: true, selfHeal: true }
syncOptions: [ CreateNamespace=true ]
ハマりどころ①: Longhorn は各ノードに
open-iscsi(apt install open-iscsi)が必須。入れ忘れると Volume がAttachingで固まる(前編 STEP0 で導入済みのはず)。
ハマりどころ②: 既定 StorageClass を 1つに絞ること。local-pathとlonghornの両方が(default)になっていると、PVC がどちらに行くか不定になる。kubectl get scで確認し、local-pathの default を外す(↓)。監視スタックを載せる前にやっておくこと(既存PVCは後から自動移動しないため)。kubectl patch storageclass local-path \ -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'ハマりどころ③: RPi の SSD/USB ストレージは個体差が出やすい。Longhorn のレプリカ数とディスク割り当ては、実ディスク容量を見て決める。
Longhorn の Dashboard。6ノードに Volume が分散して全部 Healthy、というのが一目で分かります(私の環境は18ボリューム・スケジュール可能2.75TiB)。
3-3. 監視スタックと自作アプリ
VictoriaMetrics は単体チャートでも入りますが、私の環境では victoria-metrics-k8s-stack を使い、cluster モード(vminsert/vmselect/vmstorage)で運用しています。これは VictoriaMetrics Operator・vmagent・vmalert・alertmanager・Grafana・node-exporter・kube-state-metrics を**一括で入れてくれる「傘チャート(umbrella chart=複数のサブチャートを束ねた親チャート。1つ入れれば関連一式が揃う)」**です。
📄 作成するファイル: apps/victoria-metrics.yaml
# apps/victoria-metrics.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: victoria-metrics
namespace: argocd
spec:
project: default
source:
repoURL: https://victoriametrics.github.io/helm-charts/
chart: victoria-metrics-k8s-stack
targetRevision: 0.72.5 # app version v1.138.0。傘チャート版は更新が速いので実値を確認
helm:
values: |
# Operator が VMCluster CR を作り、vminsert/vmselect/vmstorage を生成する
vmcluster:
enabled: true
spec:
retentionPeriod: "1" # 月単位。RPiの容量に合わせて短めから
vmstorage:
storage:
volumeClaimTemplate:
spec:
storageClassName: longhorn # 3-2 のLonghornに載せる
resources:
requests:
storage: 30Gi
grafana:
enabled: true
persistence:
enabled: true
storageClassName: longhorn
destination:
server: https://kubernetes.default.svc
namespace: monitoring
syncPolicy:
automated: { prune: true, selfHeal: true }
syncOptions: [ CreateNamespace=true ]
ここがポイントです。ログ(VictoriaLogs)とトレース(VictoriaTraces)は、専用の Helm チャートを別途入れているわけではありません。 上の傘チャートに同梱されている VM Operator がもう動いているので、あとは VLCluster / VTCluster という CR を1枚置くだけで、Operator が vlinsert/vlselect/vlstorage(トレースは insert/select/storage)一式を生成してくれます。これが Operator 運用のうま味です。私の環境も両方 cluster mode・各2レプリカ・ストレージは Longhorn で動いています。
・・・というのも、ただやってみたかっただけです(笑)
CR は manifests/ に置き、MetalLB の CR と同じ要領で ArgoCD の Application から同期させます。
📄 作成するファイル: manifests/victoria-logs/vlcluster.yaml
# VictoriaLogs を cluster mode で。Operator がこの CR を見て Pod 一式を作る
apiVersion: operator.victoriametrics.com/v1
kind: VLCluster
metadata:
name: vm-vlogs
namespace: monitoring
spec:
vlinsert:
replicaCount: 2
image: { repository: victoriametrics/victoria-logs, tag: v1.47.0 }
vlselect:
replicaCount: 2
image: { repository: victoriametrics/victoria-logs, tag: v1.47.0 }
vlstorage:
replicaCount: 2
image: { repository: victoriametrics/victoria-logs, tag: v1.47.0 }
storage:
volumeClaimTemplate:
spec:
storageClassName: longhorn
resources: { requests: { storage: 10Gi } }
📄 作成するファイル: manifests/victoria-traces/vtcluster.yaml
# VictoriaTraces を cluster mode で。OTLP(gRPC) を 4317 で受ける
apiVersion: operator.victoriametrics.com/v1
kind: VTCluster
metadata:
name: vm-traces
namespace: monitoring
spec:
insert:
replicaCount: 2
image: { repository: victoriametrics/victoria-traces, tag: v0.7.0 }
extraArgs: { otlpGRPCListenAddr: ":4317", "otlpGRPC.tls": "false" }
select:
replicaCount: 2
image: { repository: victoriametrics/victoria-traces, tag: v0.7.0 }
storage:
replicaCount: 2
image: { repository: victoriametrics/victoria-traces, tag: v0.7.0 }
storage:
volumeClaimTemplate:
spec:
storageClassName: longhorn
resources: { requests: { storage: 10Gi } }
あとはこの2ディレクトリを指す ArgoCD Application を置けば(§7-1 の metallb-config と同じ形)、Git に push するだけで Operator が反映します。
CR では image.tag を必ず固定してください。Operator 運用には Helm の targetRevision がない代わりに、CR に書いた image tag がそのまま使われます。latest のような可変タグにすると Pod 再作成時に予期せず上がることがあるので、本記事の v1.47.0 / v0.7.0 のように版を固定するのが安全です。
あとは Grafana のデータソースをそれぞれに向けるだけ。以降は Git に push するだけで ArgoCD が差分を検知して反映します。これが GitOps の気持ちよさです。
Grafana 側のデータソース。メトリクス(VictoriaMetrics)・ログ(VictoriaLogs)・トレース(VictoriaTraces)・アラート(Alertmanager)が、すべてクラスタ内の *.svc.cluster.local で繋がっています。
実際に流れているメトリクスはこんな具合(cluster 版なので datapoints・ingestion rate が見えます)。
バージョンメモ: 私の環境は傘チャート
victoria-metrics-k8s-stack0.72.5 / app v1.138.0(vminsert/vmselect/vmstorage の-clusterイメージ)。同梱の VM Operator は imagev0.68.3、Grafana サブチャートは app12.4.1。VictoriaMetrics は更新が速いので、傘チャートのtargetRevisionは artifacthub で都度確認してください。
自作アプリ(私の環境では監視まわりの自作コンポーネント SPICA / ALTAIR など)は別記事で詳しく書く予定です。ここではコンテナイメージを ARM64 でビルドしておく点だけ注意(
docker buildx --platform linux/arm64)。
4. ハマりポイントと回避策(GitOps・ネットワーク・ストレージ編)
まず動かないときのデバッグの基本(3コマンド)
Application が Synced にならない、Pod が Pending / ContainerCreating から進まない——そんなときは、まずこの3つで原因を覗いてください。原因の9割はここに書いてあります。
# 1. クラスタ全体の状況(どこで何がコケているか)を一覧で確認
kubectl get pods -A
# 2. 止まっているPodの詳細なエラー(末尾の Events が特に重要)を見る
kubectl describe pod <Pod名> -n <名前空間>
# 3. アプリ自体が吐いている生ログを見る
kubectl logs <Pod名> -n <名前空間>
ArgoCD 側の同期エラーは、Web UI の各 Application を開くか
kubectl -n argocd get applicationsでComparisonError等が見えます。下の表は、GitOps 化・MetalLB・Longhorn でよく出る症状の早見表です(クラスタ構築そのものの落とし穴は前編にまとめています)。
| 症状 | 原因 | 回避策 |
|---|---|---|
root-app が ComparisonError で同期できない |
private リポジトリの認証未登録 |
argocd repo add(または UI の Settings → Repositories)で登録。LAN内なら public 化でも可 |
Pod 再起動で ImagePullBackOff
|
コンテナレジストリ(GitLab)が落ちている / 認証切れ | レジストリ(本構成は GitLab 兼用)を冗長化 or 復旧。imagePullSecret を確認 |
LoadBalancer の EXTERNAL-IP が <pending>
|
MetalLB 未導入 or IPプール未設定 | MetalLB を入れ、IPAddressPool/L2Advertisement を適用 |
| EXTERNAL-IP は付くが繋がらない | servicelb と MetalLB の競合 / DHCP帯と重複 |
--disable servicelb を確認、IPプールをDHCP範囲外に |
PVC が Pending、Volume が Attaching で固まる |
Longhorn の open-iscsi 未導入 / 既定SC重複 |
全ノードに apt install open-iscsi。既定SCを1つに絞る |
| チャートが勝手にアップグレードされた |
targetRevision: "*" × automated
|
安定運用は chart 版を固定する |
5. 運用の注意点
-
kubeconfig とトークンの管理: これがあれば誰でもクラスタを掌握できる。Git に絶対コミットしない(
.gitignoreと Secret は SealedSecrets / SOPS などで暗号化) -
GitLab(Git ソース兼レジストリ)の単一障害点: 前述のとおり、ここが落ちると同期停止+
ImagePullBackOffとなります -
automatedの暴走に注意:prune: trueは Git から消したリソースをクラスタからも消す。意図しない削除を防ぐため、まずは検証環境で挙動を確認してから本番に
6. まとめ
- 立ち上げ済みの k3s HA クラスタに ArgoCD + app-of-apps を入れ、「Git に push すれば反映」される GitOps 運用を実現
- ネットワーク基盤(MetalLB)とストレージ基盤(Longhorn)を sync-wave で先に敷き、その上に VictoriaMetrics スタック・Grafana を載せた
- 最終的に、6ノードのリソースを1枚のダッシュボードで俯瞰できるところまで到達
クラウドを使わず手元で、冗長化された本番に近い監視基盤を回せるのがホームラボの醍醐味です。次回は VictoriaMetrics(cluster版)を RPi5 で運用してみた実測、その先で GitOps の Git そのものを自宅セルフホスト GitLab(別Pi5)で完結させる話と、ここに載せた自作コンポーネントの話を書く予定です。
前提となる k3s HA クラスタの組み方は前編 にまとめています(まだの方はそちらから)。
同じくホームラボで k3s / 監視基盤をいじっている方、構成の突っ込みや「うちはこうしてる」コメントぜひください。
環境: Raspberry Pi 5 (16GB) ×6 / Ubuntu Server 24.04 LTS (arm64) / k3s v1.34.3+k3s1 / ArgoCD v3.2.5 / MetalLB v0.14.8 / Longhorn v1.7.2 / victoria-metrics-k8s-stack chart 0.72.5(app v1.138.0, VM Operator v0.68.3)/ VictoriaLogs v1.47.0 / VictoriaTraces v0.7.0 / Grafana 12.4.1




