インフラエンジニアの羅針盤

インフラエンジニア1〜3年目のための技術ガイド

CKA編⑭ ArgoCD GitOps+Velero

公開更新

新卒インフラエンジニア向け「Kubernetes 実践教科書 ② CKA クラスタ構築・運用編」(全16回)の第14回です。前回で監視が整い、「おかしい」に気づけるようになりました。ですが、ここまで私たちは変更のたびに helm upgradekubectl apply手で叩いてきました。本番運用では、この「手で当てる」こと自体がリスクです——誰が・いつ・何を変えたのかが残らず、環境ごとの差分も頭の中にしかありません。本回は運用の仕上げとして、Kustomize で環境差分を管理し、ArgoCD で「Git に書いた状態へクラスタを自動で寄せる」GitOps を導入し、Velero でバックアップまで揃えます。CKA では D1 の「Use Helm and Kustomize to install cluster components」に直結する回です。

操作は作業端末 k8s-ops192.168.1.122)から行います。プロンプトの $ は一般ユーザー developer第4回で配布した admin.conf(cluster-admin)を使います。本回で作るファイルは developer のホーム配下の ~/gitops にまとめます(所有者は developersudo はツール導入時のみ)。

目次
  1. 今ここマップ(全 16 回中の現在地)
  2. この回のゴール
  3. Helm と Kustomize の使い分け
  4. 道具を揃える(git・argocd・velero)
  5. Kustomize で環境差分を管理する
  6. ArgoCD を導入して GitOps 化する
  7. fanclub-api を ArgoCD に載せる
  8. Velero でバックアップ・リストアする
  9. やってみよう
  10. まとめ
  11. 理解度チェック(○×形式・全 9 問)
  12. 次回予告

今ここマップ(全 16 回中の現在地)

本シリーズは 6 部構成です。現在地は第5部「監視・運用」の第14回、第5部の締めくくりです。

  • 第1部 クラスタ構築(第1〜5回)
  • 第2部 ワークロード管理(第6〜8回)
  • 第3部 ネットワーク(第9〜11回)
  • 第4部 ストレージ(第12回
  • 第5部 監視・運用(第13〜14回)← 今ここ
  • 第6部 トラブルシュート(第15〜16回)
  • Kubernetes v1.36.2
  • kubectl v1.35.6(作業端末・kustomize 内蔵)
  • Helm v4.1.4
  • ArgoCD v3.4.5(argo-cd chart)
  • Velero v1.18.2(plugin-for-aws v1.13.0)
  • Gitea 1.22
  • MinIO (RELEASE 系)
  • AlmaLinux 10.2
  • 確認日 2026-07-18

この回のゴール

本回を終えると、次のことができるようになります。到達できたかは記事末の「やってみよう」と「理解度チェック」で確認します。

  • Helm と Kustomize の使いどころの違いを説明できる(CKA D1 必須)
  • Kustomize の overlays で staging/prod の環境差分(レプリカ数・リソース上限)を管理できる
  • ArgoCD を導入し、fanclub-api を GitOps 化できる(Git に push すると自動で反映される)
  • Velero でクラスタのバックアップとリストアを実施できる

Helm と Kustomize の使い分け

本回では新しく Kustomize が登場します。ですが Helm はもう第4回から何度も使ってきました。「両方ともマニフェストを管理する道具なら、何が違うのか」——ここを最初にはっきりさせます。

  • Helmテンプレート+values で「パラメータ化されたパッケージ」を配布する道具。{{ .Values.image.tag }} のような穴あきテンプレートに値を流し込みます。再配布・再利用が得意で、fanclub-api や Traefik のように「誰かが作った chart を受け取って使う」場面に向きます。
  • Kustomizebase(素の YAML)+ overlays(差分パッチ)で管理する道具。テンプレート言語を持たず、完成した YAML に「ここだけ書き換える」パッチを重ねますkubectl内蔵されており(kubectl apply -k)、追加インストールが要りません。同じアプリを環境ごとに少しだけ変える(本番はレプリカ 3、検証は 1 など)場面に向きます。

つまり Helm は「配布のためのパッケージング」、Kustomize は「環境ごとの差分管理」です。対立する道具ではなく、「Helm chart で配られたものを、環境差分は Kustomize で当てる」と併用できます。本回は、これまで Helm で入れてきた fanclub-api を題材に、環境差分の管理を Kustomize に、デプロイの自動化を ArgoCD に移していきます。CKA では両方を「クラスタコンポーネントの導入手段」として問われます。

Helm と Kustomize の対比図。Helm はテンプレートに values を流し込んでパッケージを配布する仕組み、Kustomize は完成した base の YAML に overlays の差分パッチを重ねて環境ごとの違いを出す仕組みであることを左右に並べ、両者は併用でき Helm は配布・Kustomize は環境差分に向くという使い分けを示した図
図1:Helm と Kustomize の役割の違い

道具を揃える(git・argocd・velero)

本回は複数の CLI を使います。helmkubectl は導入済み、Kustomize は kubectl 内蔵なので、追加で入れるのは gitargocdvelero の 3 つです。git は OS のパッケージ、残り 2 つは GitHub のリリースから取得します(配信元は第2回の whitelist に登録済み)。

実行コマンド(k8s-ops・root 権限で導入):

# dnf install -y git
# curl -sSL -o /usr/local/bin/argocd https://github.com/argoproj/argo-cd/releases/download/v3.4.5/argocd-linux-amd64
# chmod +x /usr/local/bin/argocd
# curl -sSL -o /tmp/velero.tar.gz https://github.com/vmware-tanzu/velero/releases/download/v1.18.2/velero-v1.18.2-linux-amd64.tar.gz
# tar xzf /tmp/velero.tar.gz -C /tmp
# install /tmp/velero-v1.18.2-linux-amd64/velero /usr/local/bin/velero

実行コマンド(k8s-ops・developer・バージョン確認):

$ git --version
$ argocd version --client --short
$ velero version --client-only
$ kubectl kustomize --help | head -1

実行結果(抜粋):

git version 2.52.0
argocd: v3.4.5+564b949
Client:
	Version: v1.18.2
	Git commit: c253c7fe37d78c9b7e55c68544f7c5b2608712d8
Build a set of KRM resources using a 'kustomization.yaml' file. ...

最後の 1 行は長いので途中で省略しています(kubectl のバージョンによって文言が変わります。ヘルプが表示されれば内蔵されている証拠です)。kustomize を単体で入れていないことに注目してください。kubectl kustomizekubectl apply -k が同じ機能を内蔵しているので、CKA でも本番でも kubectl だけで足ります。

Kustomize で環境差分を管理する

まず、これまで Helm chart(テンプレート)で管理してきた fanclub-api を、Kustomize の base(素の YAML)に移します。ここで思想の違いがはっきりします——chart は「値を流し込む前のテンプレート」ですが、base は「もう値が入りきった完成品の YAML」です。完成品は helm template で書き出せます。GitOps 化するのはアプリの中心(backend/frontend の Deployment・Service・ConfigMap と、環境で変える ResourceQuota)に絞り、DB・NetworkPolicy・Gateway といったインフラ寄りの部分は既存の Helm 管理のまま活かします。

実行コマンド(k8s-ops・developer):

$ mkdir -p ~/gitops/gitops-fanclub/base ~/gitops/gitops-fanclub/overlays/staging ~/gitops/gitops-fanclub/overlays/prod
$ cd ~/gitops/gitops-fanclub
$ helm template fanclub ~/fanclub-chart --namespace fanclub \
    --set gateway.enabled=true --set frontend.image.tag=1.0.0 \
    --show-only templates/backend-deployment.yaml > base/backend-deployment.yaml
$ helm template fanclub ~/fanclub-chart --namespace fanclub \
    --set gateway.enabled=true --set frontend.image.tag=1.0.0 \
    --show-only templates/frontend-deployment.yaml > base/frontend-deployment.yaml

残りも --show-only を切り替えて書き出します。configmap.yamlresourcequota.yaml はそれぞれ base/ へ 1 対 1 で。Service だけは backend と frontend の 2 つを 1 ファイル(base/services.yaml)にまとめるので、2 回の出力を --- でつないで書き込みます。

実行コマンド(k8s-ops・developer):

$ HT="helm template fanclub ~/fanclub-chart --namespace fanclub --set gateway.enabled=true --set frontend.image.tag=1.0.0 --show-only"
$ $HT templates/configmap.yaml     > base/configmap.yaml
$ $HT templates/resourcequota.yaml > base/resourcequota.yaml
$ { $HT templates/backend-service.yaml; echo "---"; $HT templates/frontend-service.yaml; } > base/services.yaml

ここでひとつ手を入れますhelm template--namespace fanclub を渡した分、各リソースに namespace: fanclub焼き込んで出力します。ところが後で当てる差分パッチには namespace を書きません。すると Kustomize が「namespace 無しのパッチ」と「namespace 付きのリソース」を別物とみなしてパッチが当たりませんno resource matches strategic merge patch というエラーになります)。base はnamespace を持たない素の形にしておき、namespace は kustomization 側で一律に付けるのが定石です。焼き込まれた namespace 行を消します。

実行コマンド(k8s-ops・developer):

$ sed -i '/^  namespace: fanclub$/d' base/*.yaml

次に、base をひとまとめにする kustomization.yaml を作ります。namespace: fanclub をここに書くことで、base の全リソースへ一律に付きます。~/gitops/gitops-fanclub/base/kustomization.yamlvi などで作成します。

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: fanclub
resources:
  - backend-deployment.yaml
  - frontend-deployment.yaml
  - services.yaml
  - configmap.yaml
  - resourcequota.yaml

これで base は「fanclub のアプリ中心部を、値が入りきった素の YAML で一式にしたもの」になりました。ここに 環境ごとの差分(overlay)を重ねます。まず staging——検証環境はレプリカを最小にします。overlays/staging/patch-replicas.yaml を作成します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: fanclub-backend
spec:
  replicas: 1
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: fanclub-frontend
spec:
  replicas: 1

そして staging の kustomization.yamloverlays/staging/kustomization.yaml)で「base を土台に、このパッチを当てろ」と宣言します。

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: fanclub
resources:
  - ../../base
patches:
  - path: patch-replicas.yaml

次が prod です。ここに本回のいちばん実務的な壁があります。本番はレプリカを増やしたい——backend を 3 にしたい。ところが第8回で入れた fanclub-quotalimits.cpu: 3)が邪魔をします。backend は 1 レプリカで CPU 上限を 2300m 使うので、3 レプリカ(約 6900m)は quota を超えて作れません。実際、レプリカだけ増やしても新しい Pod は exceeded quota で弾かれます。

ここで Kustomize の値打ちが出ます。「本番では quota も引き上げる」という差分も、overlay で表現できるのです。prod overlay には、レプリカのパッチともう 1 枚、ResourceQuota を引き上げるパッチを置きます。まず overlays/prod/patch-replicas.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: fanclub-backend
spec:
  replicas: 3
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: fanclub-frontend
spec:
  replicas: 2

次に overlays/prod/patch-quota.yaml——backend 3 レプリカが収まるよう CPU・メモリの上限を広げます。

apiVersion: v1
kind: ResourceQuota
metadata:
  name: fanclub-quota
spec:
  hard:
    limits.cpu: "8"
    limits.memory: 8Gi
    requests.cpu: "4"
    requests.memory: 4Gi
    pods: "20"
    persistentvolumeclaims: "4"

prod の kustomization.yaml で 2 枚のパッチを当てます。

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: fanclub
resources:
  - ../../base
patches:
  - path: patch-replicas.yaml
  - path: patch-quota.yaml

できあがりを確認します。まだクラスタには何も当てません——kubectl kustomize は「当てたらどうなるか」を手元で組み立てて表示するだけのコマンドです。staging と prod で出力がどう違うかを見比べます。

実行コマンド(k8s-ops・developer):

$ kubectl kustomize overlays/staging | grep -E "kind: (Deployment|ResourceQuota)|replicas:|limits.cpu:"
$ echo "----"
$ kubectl kustomize overlays/prod | grep -E "kind: (Deployment|ResourceQuota)|replicas:|limits.cpu:"

実行結果(同じ base から別の値が出る):

kind: ResourceQuota
    limits.cpu: "3"
kind: Deployment
  replicas: 1
kind: Deployment
  replicas: 1
----
kind: ResourceQuota
    limits.cpu: "8"
kind: Deployment
  replicas: 3
kind: Deployment
  replicas: 2

同じ base から、staging は最小・prod は増強という別々の完成形が出ました。テンプレート言語を一切書かず、「素の YAML + 差分」だけでこれができるのが Kustomize です。この gitops-fanclub ディレクトリを、次に Git に載せて ArgoCD に見張らせます

ArgoCD を導入して GitOps 化する

ここからが本回の中心、GitOps です。考え方はひとつだけ——「Git に書いた状態を唯一の正とし、クラスタをそれに自動で合わせ続ける」。手で kubectl apply するのをやめ、Git に push することが唯一の変更手段になります。これを担うのが ArgoCD です。まず、push 先となる Git サーバが要ります。企業ネットワークの再現として、外部の GitHub ではなくクラスタと同じ LAN 上の k8s-registry に Gitea(軽量な Git サーバ)を立てて使います。

Gitea を k8s-registry 上のコンテナとして起動します。イメージは Docker Hub から取得します(第2回の whitelist に登録済み)。k8s-registry に SSH して、docker で立てます。

実行コマンド(k8s-registry・root 権限):

# docker run -d --name gitea --restart unless-stopped -p 3000:3000 \
    -e GITEA__server__ROOT_URL=http://k8s-registry:3000/ \
    -e GITEA__security__INSTALL_LOCK=true \
    -e GITEA__service__DISABLE_REGISTRATION=true \
    -v gitea-data:/data \
    gitea/gitea:1.22

INSTALL_LOCK=true で初回セットアップ画面を飛ばし、SQLite の内蔵 DB で動きます。起動したら、管理ユーザー developer を作り、空の gitops-fanclub リポジトリを API で作成します。

実行コマンド(k8s-registry・root 権限):

# docker exec -u git gitea gitea admin user create \
    --username developer --password giteaPass123 \
    --email dev@example.local --admin --must-change-password=false
# curl -s -u developer:giteaPass123 -X POST http://localhost:3000/api/v1/user/repos \
    -H "Content-Type: application/json" \
    -d '{"name":"gitops-fanclub","private":false,"auto_init":false}'

これで http://k8s-registry:3000/developer/gitops-fanclub.git が使えます。作業端末(k8s-ops)に戻り、Kustomize 一式を push します。ここで 2 つ手当てが要ります。① k8s-registryno_proxy に入っていないので、そのまま git push するとプロキシへ回されて弾かれます(第11回fanclub.local と同型)——no_proxyk8s-registry を足します。② HTTP での push には認証が要るので、リモート URL にユーザーとパスワードを埋め込みます。

実行コマンド(k8s-ops・developer):

$ export no_proxy="$no_proxy,k8s-registry"
$ export NO_PROXY="$no_proxy"
$ cd ~/gitops/gitops-fanclub
$ git init -b main
$ git add .
$ git -c user.email=dev@example.local -c user.name=developer commit -m "fanclub kustomize base + overlays"
$ git remote add origin http://developer:giteaPass123@k8s-registry:3000/developer/gitops-fanclub.git
$ git push -u origin main

* [new branch] main -> main と出れば push 成功です。ブラウザで http://k8s-registry:3000/developer/gitops-fanclub を開くと、base と overlays が並んでいます。ここが GitOps の「唯一の正」になります。なお URL にパスワードを埋め込むのは学習環境の簡略化です(.git/config に平文で残ります)。本番ではアクセストークンや SSH 鍵、Git の資格情報ヘルパーを使います。

次に ArgoCD 本体をクラスタに導入します。その前に alma-proxy の whitelist に chart の配信元 argoproj.github.io を足します第9回の MetalLB・第12回の Longhorn・第13回の Prometheus と同じ手当てです)。

実行コマンド(alma-proxy・root):

# echo 'argoproj.github.io' >> /etc/squid/whitelist.txt
# systemctl reload squid

k8s-ops に戻り、専用の argocd 名前空間に Helm で入れます。ここで --set redis.image.repository=redis を付けるのがポイントです。この chart は既定で redis を ecr-public.aws.com(AWS の公開レジストリ)から取りますが、その本体データ(blob)が毎回変わる CloudFront のホスト名で配られるため、whitelist で許可しきれず ImagePullBackOff になります。redis は Docker Hub にもあり、そちらは第2回で許可済みなので、取得元を Docker Hub の redis に差し替えます(argocd 本体や dex は quay.ioghcr.io から取れるので手当て不要)。

実行コマンド(k8s-ops・developer):

$ helm repo add argo https://argoproj.github.io/argo-helm
$ helm repo update
$ helm install argocd argo/argo-cd --namespace argocd --create-namespace \
    --set redis.image.repository=redis

ArgoCD が operator(controller)パターンで動くことを確認しておきます。第11回の Traefik(Gateway API の CRD を見張る)と同じ形です——ArgoCD は Application という CRD を監視し、そこに書かれた「Git のこの場所」と「クラスタの実状態」の差分を、絶えず埋めようとします。CKA D1 の「Understand CRDs, install and configure operators」がそのまま出てくる場面です。

実行コマンド(k8s-ops・developer):

$ kubectl get crds | grep argoproj
$ kubectl get pods -n argocd

実行結果(Application 等の CRD 3 つと ArgoCD の Pod 群・抜粋):

applications.argoproj.io                     2026-07-18...
applicationsets.argoproj.io                  2026-07-18...
appprojects.argoproj.io                      2026-07-18...

NAME                                    READY   STATUS    RESTARTS   AGE
argocd-application-controller-0         1/1     Running   0          3m
argocd-applicationset-controller-...    1/1     Running   0          3m
argocd-dex-server-...                   1/1     Running   0          3m
argocd-notifications-controller-...     1/1     Running   0          3m
argocd-redis-...                        1/1     Running   0          3m
argocd-repo-server-...                  1/1     Running   0          3m
argocd-server-...                       1/1     Running   0          3m

管理 UI は前回の Grafana と同じく port-forward で覗きます(外部公開はしません)。初期パスワードは Secret から取り出します。

実行コマンド(k8s-ops・developer):

$ kubectl get secret argocd-initial-admin-secret -n argocd -o jsonpath='{.data.password}' | base64 -d; echo
$ kubectl port-forward -n argocd svc/argocd-server --address 0.0.0.0 8080:443

port-forward は動かしっぱなしにしますCtrl+C で止まると UI も切れます)。第13回の Grafana と同じく、この先のコマンドは別のシェルを開いて実行してください。手元のブラウザで https://192.168.1.122:8080 を開き、ユーザー admin と取り出したパスワードでログインします(自己署名証明書の警告は許可して進みます)。まだ何も登録していないので、画面は空です。ここに fanclub-api を「見張る対象」として登録します。

fanclub-api を ArgoCD に載せる

ArgoCD に「この Git のこの場所を、このクラスタのこの名前空間へ、自動で反映し続けろ」と伝えるのが Application リソースです。~/gitops/application-prod.yaml を作成します。

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: fanclub-api-prod
  namespace: argocd
spec:
  project: default
  source:
    repoURL: http://k8s-registry:3000/developer/gitops-fanclub.git
    targetRevision: main
    path: overlays/prod
  destination:
    server: https://kubernetes.default.svc
    namespace: fanclub
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

要点は syncPolicy.automated の 2 行です。prune: true は「Git から消したものはクラスタからも消す」、selfHeal: true は「クラスタを手で変えても Git の状態へ引き戻す」。この 2 つで「Git = 唯一の正」が成立します。path: overlays/prod なので、ArgoCD は先ほどの prod overlay(backend 3・quota 拡大)を適用します。

ここで所有権の引き継ぎを押さえておきます。fanclub-api はこれまで Helm(リリース fanclub)が管理してきました。この Application を適用すると、base に入れた backend・frontend の Deployment/Service/ConfigMap/ResourceQuota は、ここから ArgoCD が管理します——以後これらを変えるときは helm upgrade ではなく Git に push します(selfHeal があるので、手で kubectl edit しても Git の状態へ引き戻されます)。一方、base に入れなかった DB・NetworkPolicy・Gateway・Secret は引き続き Helm 管理のままです。「アプリの中心は GitOps で、インフラ寄りは Helm で」という住み分けになります。この二重管理をなくすには、いずれ全リソースを base に移して Helm リリースを退役させますが、本回はアプリの中心を GitOps 化するところまでを扱います。

実行コマンド(k8s-ops・developer):

$ kubectl apply -f ~/gitops/application-prod.yaml
$ kubectl get application -n argocd

実行結果(同期には少し時間がかかり、まず Progressing→やがて Healthy):

NAME               SYNC STATUS   HEALTH STATUS
fanclub-api-prod   Synced        Healthy

ArgoCD の UI を見ると、fanclub-api-prod が backend・frontend・Service・ConfigMap・ResourceQuota を束ねた 1 枚のツリーとして表示され、Synced(Git と一致)/Healthy(正常稼働)になっているはずです。fanclub はこれまで Helm が管理していましたが、ここで ArgoCD がこれらのリソースを引き取りました——衝突は起きず、そのまま Synced になります。backend が 3 レプリカに増えるには JVM の起動で数分かかります。増えたレプリカと広がった quota を確認します。

実行コマンド(k8s-ops・developer):

$ kubectl get deploy -n fanclub
$ kubectl get resourcequota fanclub-quota -n fanclub

実行結果(backend 3/3・quota が 8 に広がっている):

NAME               READY   UP-TO-DATE   AVAILABLE
fanclub-backend    3/3     3            3
fanclub-frontend   2/2     2            2

NAME            AGE   REQUEST                LIMIT
fanclub-quota   ...   ...                    limits.cpu: 6900m/8, limits.memory: .../8Gi

ここからが GitOps の本番です。もう kubectl でレプリカを変えませんGit を変えると、ArgoCD がそれを検知してクラスタへ反映します。試しに prod のレプリカを 3 から 2 に減らして push します。

実行コマンド(k8s-ops・developer):

$ cd ~/gitops/gitops-fanclub
$ sed -i 's/replicas: 3/replicas: 2/' overlays/prod/patch-replicas.yaml
$ git -c user.email=dev@example.local -c user.name=developer commit -am "prod: backend replicas 3 -> 2"
$ git push

最大 3 分ほど待って kubectl get deploy fanclub-backend -n fanclub を見ると、手で apply していないのに backend が 2 レプリカへ変わります。ArgoCD が Git の変更を検知し、差分を当てたのです。「最大 3 分」なのは、ArgoCD が既定で Git を約 3 分ごとに見に行く(ポーリングする)からで、待ちきれなければ UI の REFRESH/SYNC ボタンから手動で反映もできます(本番では Git の Webhook を使って push 直後に反映させます)。これが GitOps——変更の記録が Git にすべて残り、クラスタは Git に追従する。「誰が・いつ・何を変えたか」は Git のコミット履歴が答えます。

デモで挙動を確認できたら、prod 本来の狙い(backend 3・frontend 2)へ戻しておきます。本巻はこの後 prod=backend 3 レプリカを前提に進むためです。backend だけを 3 に戻し frontend は 2 のままにしたいので、両方に一致してしまう sed 's/replicas: 2/replicas: 3/' は使わず、ファイルを本来の内容へ書き戻して push します。

実行コマンド(k8s-ops・developer):

$ cat > overlays/prod/patch-replicas.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: fanclub-backend
spec:
  replicas: 3
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: fanclub-frontend
spec:
  replicas: 2
EOF
$ git -c user.email=dev@example.local -c user.name=developer commit -am "prod: backend replicas 2 -> 3 に戻す"
$ git push

数分後、kubectl get deploy fanclub-backend -n fanclub が再び 3 レプリカに戻ります。以降の回、とくに第16回のトラブルシュートは、この backend 3・frontend 2 の状態を前提に進みます。

GitOps のループ図。開発者が Git リポジトリに push した宣言的マニフェストを ArgoCD の Application コントローラが監視し、Git の状態とクラスタの実状態の差分を検知して自動で同期し、手で変更してもセルフヒールで Git の状態へ引き戻すという循環を示した図
図2:GitOps のループ(push → 差分検知 → 同期 → セルフヒール)

Velero でバックアップ・リストアする

運用の最後のピースはバックアップです。第5回で etcd のスナップショットを取りましたが、あれはクラスタ全体の状態のバックアップでした。ここで入れる Velero は粒度が違います——名前空間やアプリ単位で、リソース定義と(対応していれば)ボリュームの中身をまとめて退避します。「fanclub だけ丸ごとバックアップ・復元したい」に応えるのが Velero です。

  • etcd スナップショット(第5回):クラスタの全状態を丸ごと。復元はクラスタ全体を巻き戻す、重い操作。
  • Velero(本回)名前空間/アプリ単位で選んで退避・復元。日常運用の「この名前空間だけ戻したい」に向く。

Velero はバックアップの保存先に S3 互換のオブジェクトストレージを要求します。本環境ではクラウドの S3 が使えないので、S3 互換の MinIO を、Gitea と同じく k8s-registry 上のコンテナとして立てます(イメージは Docker Hub・whitelist 済み)。

実行コマンド(k8s-registry・root 権限):

# docker run -d --name minio --restart unless-stopped -p 9000:9000 -p 9001:9001 \
    -e MINIO_ROOT_USER=minioadmin -e MINIO_ROOT_PASSWORD=minioadmin123 \
    -v minio-data:/data \
    minio/minio server /data --console-address ":9001"
# docker run --rm --network host --entrypoint sh minio/mc -c \
    "mc alias set local http://localhost:9000 minioadmin minioadmin123 && mc mb local/velero"

これで k8s-registry:9000 に MinIO が立ち、バックアップ用のバケット velero ができました。次に、Velero が MinIO へアクセスするための認証情報ファイルを作業端末(k8s-ops)で作ります。AWS 認証情報の形式で ~/gitops/minio-credentialsvi などで作成します(値は先ほど MinIO に設定したもの)。

[default]
aws_access_key_id = minioadmin
aws_secret_access_key = minioadmin123

この認証情報ファイルを指定して Velero を導入します。ここでボリュームの中身をどう退避するかを決めるのが重要です。クラウドなら「ボリューム スナップショット」機能が使えますが、本環境の MinIO にはそれがありません。そこで --use-node-agent--default-volumes-to-fs-backup を付け、各ノードのエージェントが PVC の中身をファイルとして MinIO へ退避する(File System Backup)方式にします。これを付けないと マニフェスト(YAML)だけが保存され、fanclub-db の Longhorn ボリュームの中身は退避されません——復元しても DB が空になり、会員データが失われます。

実行コマンド(k8s-ops・developer):

$ velero install \
    --provider aws \
    --plugins velero/velero-plugin-for-aws:v1.13.0 \
    --bucket velero \
    --secret-file ~/gitops/minio-credentials \
    --use-volume-snapshots=false \
    --use-node-agent \
    --default-volumes-to-fs-backup \
    --backup-location-config region=minio,s3ForcePathStyle=true,s3Url=http://192.168.1.123:9000

Velero 本体と、PVC の中身を退避する node-agent(DaemonSet)が立ちます。node-agent は既定で Control Plane の taint を許容しないため、Workload Node の 2 台に載ります(fanclub-db もそこにいるので、DB の退避はこれで足ります)。保存先(MinIO)に届いているかは velero backup-location get で確認でき、PHASEAvailable なら準備完了です。

後で「本当にデータが戻ったか」を確かめられるよう、目印になる会員を 1 件足しておきます。seed の 2 名(鈴木花子・佐藤一郎)に無い名前を入れておけば、復元後にそれが残っているかでDB の中身まで戻ったかを判定できます(もし DB データが退避されていなければ、復元後に会員は seed の 2 名だけに戻り、この目印は消えます)。

実行コマンド(k8s-ops・developer):

$ export no_proxy="$no_proxy,fanclub.local"
$ curl -sk -X POST --resolve fanclub.local:443:192.168.1.124 https://fanclub.local/api/members \
    -H "Content-Type: application/json" \
    -d '{"name":"復元テスト太郎","email":"restore-test@example.com","plan":"premium"}'

これで会員は 3 名になりました。fanclub 名前空間を丸ごとバックアップします(--wait で完了まで待ちます)。

実行コマンド(k8s-ops・developer):

$ velero backup create fanclub-backup --include-namespaces fanclub --wait
$ velero backup describe fanclub-backup | grep -E "Phase:|backed up"
$ kubectl get podvolumebackups -n velero | grep fanclub-db

実行結果(CompletedDB ボリュームの中身も退避されている)。リソース件数はクラスタの状態で大きく変わります(本書の再検証時は 532 件でした)ので、数字そのものより Phase: Completed と PodVolumeBackup の Completed を見てください:

Phase:  Completed
Total items to be backed up:  532
Items backed up:              532

NAME                    ...   POD            VOLUME   STATUS      ...
<pvb-name>               ...   fanclub-db-0   data     Completed   ...

fanclub-db-0data ボリュームが Completed になっています——これが File System Backup が効いている証拠で、DB の中身が MinIO へ退避されました。これが無ければ、後の復元で会員データは戻りません。

いよいよ復元の確認です。ここからは破壊操作——fanclub 名前空間を丸ごと削除してから、バックアップで戻します。スナップショットからロールバックできる状態でのみ実施してください

ここでひとつ手当てが要ります。いま fanclub-api-prod は selfHeal 付きで ArgoCD に見張られているので、名前空間を消すと ArgoCD が即座に app 層を Git から作り直そうとし、Velero の復元とぶつかります(消したそばから作られては、Velero が何を戻したのか分かりません)。Velero の復元を単独で観察するため、先に ArgoCD の自動 sync を止めます。Application の syncPolicy.automated を空にすれば止まります。

実行コマンド(k8s-ops・developer):

$ kubectl patch application fanclub-api-prod -n argocd --type merge \
    -p '{"spec":{"syncPolicy":{"automated":null}}}'

これで自動 sync が止まりました。名前空間を削除し、Velero で戻します。

実行コマンド(k8s-ops・developer・破壊操作):

$ kubectl delete namespace fanclub
$ velero restore create --from-backup fanclub-backup --wait
$ kubectl get podvolumerestores -n velero | grep fanclub-db

実行結果(Completed・DB ボリュームの中身も復元):

Restore completed with status: Completed.

NAME                    ...   POD            VOLUME   STATUS      ...
<pvr-name>              ...   fanclub-db-0   data     Completed   ...

復元されると Pod が戻ってきます。ただし backend は 1/2 のままのことがあります——DB(fanclub-db-0)が作り直されたので、backend が古い接続を掴んだまま再接続できていないためです(第12回でも出た現象です)。backend の Pod を入れ替えて再接続させます。

実行コマンド(k8s-ops・developer):

$ kubectl delete pod -n fanclub -l app=fanclub-backend

backend が 2/2 に戻ったら、いよいよデータの生存確認です。ブラウザから https://fanclub.local が開け、会員一覧に目印の「復元テスト太郎」が残っていることを確認します。

実行コマンド(k8s-ops・developer):

$ curl -sk --resolve fanclub.local:443:192.168.1.124 https://fanclub.local/api/members

実行結果(会員 3 名・目印が生存=DB の中身まで復元された):

[{"id":1,"name":"鈴木花子",...},{"id":2,"name":"佐藤一郎",...},{"id":11,"name":"復元テスト太郎","email":"restore-test@example.com","plan":"premium",...}]

目印が残っていました——名前空間を丸ごと消しても、会員データまで戻せたということです。もし File System Backup を有効にしていなければ、ここは seed の 2 名だけに戻り、目印は消えていました。確認できたら、止めておいた自動 sync を元に戻します

実行コマンド(k8s-ops・developer):

$ kubectl patch application fanclub-api-prod -n argocd --type merge \
    -p '{"spec":{"syncPolicy":{"automated":{"prune":true,"selfHeal":true}}}}'

Velero と GitOps は役割が違います。GitOps(ArgoCD)が戻せるのは Git にある宣言的マニフェスト——backend や frontend の「あるべき形」です。しかし DB の中身(会員データ)は Git にありません。そこを埋めるのが Velero の File System Backup です。「アプリの構成は Git、データは Velero」と補い合うのが実務の形です。

バックアップの二層を表す図。上段は etcd スナップショットがクラスタ全体の状態を丸ごと退避する重い操作であること、下段は Velero が名前空間やアプリ単位で選んで S3 互換の MinIO へ退避・復元する日常運用向けの操作であることを対比し、粒度と用途の違いを示した図
図3:バックアップの二層(etcd スナップショットと Velero)

やってみよう

本回で入れた ArgoCD・Velero は第15回以降もそのまま使いますので、後始末で削除しないでください。演習 3 は破壊操作を含むため、スナップショットから戻せる状態で行ってください。

staging と prod の差分を出力で確かめる

kubectl kustomize overlays/stagingkubectl kustomize overlays/prod の出力を比べ、同じ base から replicas と limits.cpu だけが変わっていることを確認してください。テンプレート言語を使わずに環境差分が表現できていることを、自分の目で確かめられれば十分です。

Git を変えて自動 sync を見る

overlays/prod/patch-replicas.yaml の frontend の replicas を 2 から 3 に変えて git push し、手で apply せずに kubectl get deploy fanclub-frontend -n fanclub が 3 レプリカへ変わるのを確認してください。ArgoCD UI で Sync のイベントが記録されることも確認してください。

Velero で名前空間を復元する

本文と同じ流れで、先に kubectl patch application で自動 sync を止めてから(syncPolicy.automatednull に)、目印の会員を 1 件足し、velero backup create --wait でバックアップし、kubectl delete namespace fanclub で消し、velero restore create --from-backup --wait で戻してください。復元後に backend が 1/2 なら kubectl delete pod -l app=fanclub-backend で入れ替え、https://fanclub.local が開けることと目印の会員が残っていることを確認できれば成功です。終わったら syncPolicy.automated を戻して sync を再開します。必ずスナップショットから戻せる状態で実施してください。考えてみよう:もし自動 sync を止めなかったら、名前空間を消した瞬間に ArgoCD は何をしようとするか——本文の説明と結びつけて考えてみてください。

まとめ

本回では運用の仕組みを 3 つ揃えました。①Helm と Kustomize の使い分け——Helm は配布のパッケージング、Kustomize は環境差分(base + overlays)。②ArgoCD で GitOps 化——Application という CRD で「Git = 唯一の正」を宣言し、push すればクラスタが自動で追従する。pruneselfHeal が「Git に寄せ続ける」を支える。③Velero でバックアップ・リストア——etcd スナップショット(クラスタ全体)とは粒度が違い、名前空間/アプリ単位で退避・復元できる。加えて、本番の増強(backend 3 レプリカ)を quota も含めて overlay で表現し、環境差分の管理が現実の制約(第8回の fanclub-quota)にどう効くかも確認しました。これで fanclub-api は、HA クラスタ・分散ストレージ・監視・GitOps・バックアップを備えた運用体制に到達しました。

理解度チェック(○×形式・全 9 問)

次の各文が正しいか(○)誤りか(×)を判断してください。下の「解答と解説」を開くと答え合わせができます。

  1. Kustomize はテンプレート言語で YAML を生成する。
  2. Helm は values でパラメータ化されたパッケージを配布する道具である。
  3. kubectl apply -k は Kustomize を適用する。
  4. GitOps では、変更は手動の kubectl apply ではなく Git 経由で行う。
  5. ArgoCD の selfHeal: true は、クラスタを手で変えても Git の状態へ引き戻す。
  6. ArgoCD は Application という CRD を監視する operator(controller)である。
  7. Velero は etcd 全体だけをバックアップする。
  8. Velero は名前空間やアプリ単位でバックアップ・リストアできる。
  9. Kustomize の overlay では、Deployment のレプリカ数は変えられるが ResourceQuota は変えられない。
解答と解説

1=×(テンプレートを持たず、素の YAML に差分パッチを当てる)/2=○/3=○(-k が Kustomize 適用)/4=○(Git が唯一の変更手段)/5=○/6=○(第11回 Traefik と同じ operator パターン)/7=×(etcd 全体は第5回のスナップショット。Velero は名前空間/アプリ単位)/8=○/9=×(本回のとおり ResourceQuota も overlay のパッチで変えられる)

次回予告

次回・第15回からは第6部「トラブルシュート」に入ります。CKA の配点で最大(30%)の領域です。まずは Control Plane と etcd のトラブルシュート——Static Pod の異常、etcd のバックアップ・リストア、kubelet 停止からの復旧を、実際に障害を仕込んで診断・復旧します。ここまで積み上げてきた監視とログが、原因究明の土台になります。

前の記事
次の記事