現場のリアルな課題:停電後のクラスタドリフトと「ClickOps」の限界
昨年の冬、ブレーカーが落ちてしまい、K3sを動かしていた3台のリファービッシュ品Lenovo ThinkCentre M720qノードの電源が一斉に落ちました。ノードが再起動したものの、12個ある主要サービスのうち4つが起動しませんでした。
Longhornボリュームの再マウントが失敗し、TraefikのIngressルートは消失、さらにPi-holeのDNSインスタンスはCrashLoopBackOff状態でスタックしました。2週間前の深夜1時に場当たり的に実行したkubectl editや、記録に残っていないHelmフラグを再現するため、ノートPC2台のbash履歴をひたすら漁る羽目になり、4時間もの時間を浪費しました。
自宅サーバー(おうちラボ)を運用していると、誰もがこの壁にぶつかります。最初は綺麗な設計を維持しようと意気込んでクラスタを立ち上げるものの、次第に「手軽さ」に流されてしまいます。稼働中のPodで直接環境変数を書き換えたり、メディアサーバーを試すために記録も取らずhelm installを実行したり、ワークステーションの適当な~/k8s-testフォルダにマニフェストを放置したりしがちです。しかし、いざハードウェア障害が発生すると、クラスタの実環境の状態はディスク上のどのファイルとも一致しなくなり、復旧にはゼロからの再構築が必要になります。
根本原因の分析:命令型デプロイが破綻する理由
命令型(Imperative)のアプローチでクラスタを管理することは、各ノードを壊れやすい「ペット」のように特別扱いすることと同じです。ソース管理を介さない直接の変更は、主に3つのアーキテクチャ上のリスクを生み出します:
- 気付かない設定ドリフト(乖離):稼働中のDeploymentに直接適用したホットフィックスには監査ログが残りません。3週間も経てば、なぜメモリ制限が256Miから1Giへ引き上げられたのか、誰も覚えていない状態になります。
- ワークステーションの単一障害点(SPOF):作業用YAMLファイルがメインデスクトップのNVMeドライブにしか保存されていない場合、クラスタ全体の構成情報はそのワークステーション1台の生存に依存することになります。
- Push型CI/CDによるセキュリティリスク:GitHub Actionsなどのパイプラインから内部へマニフェストをプッシュするには、ホームルーターの6443ポートを開放するか、Dynamic DNSを設定するか、あるいは壊れやすいCloudflareやWireGuardトンネルをやりくりしなければなりません。
時間が経つにつれ、クラスタは誰も再起動を恐れて触れない「ブラックボックス」と化してしまいます。
ベアメタルK3s向けデプロイモデルの比較
専用のGitOpsコントローラーの導入を決める前に、個人開発者がマニフェストをデプロイする標準的な3つの手法を比較検証しました:
1. ターミナルからのKubectlおよびHelm
ターミナルからkubectl apply -f ./manifestsを実行すると、ローカル環境でのテスト時に即座にフィードバックが得られます。しかし、運用開始から数日経つと破綻し始めます。ターミナルではドリフト検出が一切行われないため、更新スクリプトやバグのあるOperatorによってサービス定義が勝手に変更されても、ローカルディレクトリからはそれを検知する術がありません。
2. 外部Push型パイプライン(GitHub Actions / GitLab CI)
AWS EKSのようなクラウド環境であれば、マネージドラナーからVPC内へ直接デプロイできます。しかし自宅環境の場合、Kubernetesのコントロールプレーンに対する外部からのインバウンドアクセスを開放しなければなりません。さらに厄介なことに、パイプライン自体が成功しても、その10分後にOOM(Out of Memory)キルによってPodがクラッシュした場合、外部CIツールは「成功(グリーン)」と判定したまま放置してしまいます。
3. クラスタ内GitOps(Argo CD)
Argo CDはこのネットワークモデルを逆転させます。クラスタ内部でエージェントが動作し、リモートのGitリポジトリを監視します。3分ごと(またはWebhook経由の即時通知)に、コミットされたYAMLマニフェストとetcd内の実環境の状態を比較します。変更をコミットすると、Argo CDが内側からプルして適用するため、ファイアウォールの6443ポートを外部に開放する必要は一切ありません。
ステップ・バイ・ステップ:K3sへのArgo CDセットアップ手順
Argo CDのコアコンポーネント(APIサーバー、repoサーバー、コントローラー)が消費するメモリは約350Mi程度です。搭載メモリが8GB程度の環境でも余裕を持って動作します。
ステップ1:Argo CDのインストール
専用のNamespaceを作成し、公式のマニフェストを適用します:
# 専用のNamespaceを作成
kubectl create namespace argocd
# 安定版のデプロイマニフェストを適用
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# 5つの主要コントローラーPodがRunning状態になるまで待機
kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=argocd-server -n argocd --timeout=120s
自動生成された管理者パスワードを取得します:
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d; echo
ポート8080をフォワードし、ローカルマシンからWebダッシュボードへ安全にアクセスできるようにします:
kubectl port-forward svc/argocd-server -n argocd 8080:443
ブラウザでhttps://localhost:8080にアクセスし、自己署名証明書の警告を許可した上で、adminとしてログインします。
ステップ2:Gitリポジトリの構成
GitHub、GitLab、またはローカルのGiteaインスタンス上にクリーンなリポジトリを作成します。以下のようにアプリケーションごとにサブディレクトリを分割して管理することで、依存関係を適切に分離できます:
homelab-gitops/
├── apps/
│ ├── whoami/
│ │ ├── deployment.yaml
│ │ └── service.yaml
│ └── monitoring/
│ ├── prometheus.yaml
│ └── grafana.yaml
└── bootstrap/
└── whoami-app.yaml
動作確認用の軽量なWebサーバーとして、apps/whoami/deployment.yamlを作成します:
apiVersion: apps/v1
kind: Deployment
metadata:
name: whoami
namespace: default
labels:
app: whoami
spec:
replicas: 2
selector:
matchLabels:
app: whoami
template:
metadata:
labels:
app: whoami
spec:
containers:
- name: whoami
image: traefik/whoami:v1.10.1
ports:
- containerPort: 80
resources:
limits:
memory: 64Mi
cpu: 100m
---
apiVersion: v1
kind: Service
metadata:
name: whoami-service
namespace: default
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
selector:
app: whoami
この変更をコミットし、mainブランチへプッシュします。
ステップ3:宣言的Applicationマニフェストの作成
UI上での手動クリックによるアプリケーション作成は避けましょう。Argo CD自体の構成もYAMLで宣言的に管理することで、ディザスタリカバリ時の再現性を確保できます。以下のマニフェストをbootstrap/whoami-app.yamlとして保存します:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: whoami-service
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: 'https://github.com/your-username/homelab-gitops.git'
targetRevision: HEAD
path: apps/whoami
destination:
server: 'https://kubernetes.default.svc'
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
この構成を自律的に維持するために、以下の2つのディレクティブが重要となります:
prune: true: Git上からYAMLファイルを削除した際、クラスタ内の実リソースも自動的に削除するようArgo CDに指示します。selfHeal: true: 誰かがkubectl等でリソースを手動変更した場合でも、自動的にGitの定義通りの状態へ強制修復します。
マニフェストを適用します:
kubectl apply -f bootstrap/whoami-app.yaml
ステップ4:自動ドリフト修復の動作確認
数秒以内に、Argo CDのUI上でwhoami-serviceがSyncedおよびHealthyステータスに変化します。
ここで、手動による変更をシミュレートしてみましょう。ターミナルから直接Deploymentをスケールさせてみます:
kubectl scale deployment whoami --replicas=6
すかさずkubectl get pods -l app=whoamiを実行してPodの状態を確認します。一瞬だけ追加の4つのPodが起動しかける様子が確認できるはずです。しかし直後にArgo CDが介入し、実環境のPod数(6)とGitリポジトリで定義された設定値(2)の乖離を検知して余分なレプリカを即座に破棄、元の2台へと復元します。常にGitリポジトリが唯一の信頼できる情報源(Single Source of Truth)として機能し続けます。

