ハイブリッド k3s · 第4回

ハイブリッド k3s #4: k3s の上に統合データベースを構築する

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


ホームラボ全体構成図

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

このシリーズは、上の構成図のように、今この瞬間も動いているホームラボをどう作ったのかを一つずつ記録していく記事です。

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

  • ノード 6台 — クラウド(AWS 東京)の Lightsail server 2台(コントロールプレーン + etcd) + 自宅(札幌)iMac 上の Lima VM agent 4台
  • 合計 19 vCPU / 61 GiB、ネームスペース 49個、Pod 248個(稼働 150)
  • デプロイは ArgoCD、認証は Keycloak OIDC、その上に CloudNativePG・Vault・CrowdSec・Prometheus/Grafana などが稼働中

今回は、3回目までに築いた6ノードのハイブリッドクラスタの上に、PostgreSQL を安定して運用するために5つの Operator を掘り下げ、最終的に CloudNativePG で HA クラスタとバックアップまで構築する 話です。

1. Kubernetes と DB、そして Operator

“Kubernetes supports stateful workloads; I do not.” — Kelsey Hightower (2018)

Kubernetes の伝道師として知られる Kelsey Hightower が 2018年に X(Twitter)へ残した一言です。「Kubernetes はステートフルなワークロードをサポートする。でも私はしない」 — つまり「自分なら、そこにデータベースは載せない」という意味ですね。彼だけの意見ではなく、データベースを Kubernetes に載せることは、長らくインフラ業界では敬遠されがちでした。

理由ははっきりしています。Pod はいつでも落ちうるし(Ephemeral)、ノードは予告なく入れ替わります。ステートレスなアプリケーションなら落ちても立て直せば済みますが、データを抱えた DB は、Pod が一つ消えた瞬間にそのデータの命運が左右されます。だから「DB だけは RDS や Cloud SQL のようなマネージドサービスに任せろ」という助言が、長く定説でした。

この通念を覆したのは二つでした。一つは StatefulSet と PersistentVolume の安定化です。Pod が再起動したり別のノードへ移っても、同じボリュームと安定したネットワーク ID を保てるようになり、ステートフルなワークロードの土台が整いました。もう一つが Operator パターン です。

Operator は2016年に CoreOS が 発表した概念で、ひとことで言えば「運用知識をソフトウェアに込める(Operational Knowledge into Software)」という発想です。管理者の頭の中やシェルスクリプトに散らばっていた運用作業 — プロビジョニング、バージョンアップグレード、フェイルオーバー、バックアップ、特定時点復旧(PITR) — を、ワークロードと同じ場所で動く コントローラ(Controller) のコードへ移したものです。運用者が YAML で「望む状態」だけを宣言すれば、Operator が現在の状態と絶えず突き合わせ、その状態へ収束させます。最初の事例が CoreOS の etcd Operator と Prometheus Operator でした。

DB のように運用難度の高いソフトウェアほど、このパターンの効果は大きくなります。なかでも PostgreSQL のエコシステムは、Operator が最も激しく競い合う分野です。それぞれ異なるアーキテクチャ哲学を掲げた各ソリューションを、次の章で一つずつ比べていきます。


2. 主要な5つの Postgres Operator アーキテクチャ比較

現在 CNCF エコシステムやエンタープライズ環境で最も広く使われている主要な PostgreSQL Operator は、次のとおりです。各ソリューションは固有のアーキテクチャと長所・短所を持ち、インフラの要件に応じて適した選択肢が変わります。

なお、この5つの中から一つを選んだ理由は、ホームラボに適した Operator を選ぶためと、個人的にもっと経験してみたいという思いが大きな割合を占めています。実際に企業で活用する観点とは別物だという点を踏まえて読んでいただけると幸いです。

① Zalando Postgres Operator

最初に見たのは、この分野の元祖格である Zalando Postgres Operator です。ドイツの EC 企業 Zalando が自社の PostgreSQL 運用のために作り、今も社内で数百のクラスタを回しながら鍛えてきた、最も歴史の長い部類の Operator です。

The Postgres Operator delivers an easy to run highly-available PostgreSQL clusters on Kubernetes (K8s) powered by Patroni. — Zalando postgres-operator 公式 README

核となるのは Spilo という Docker イメージです。Spilo は PostgreSQL と HA 管理ツールの Patroni、そして S3 バックアップ・復旧ツールの WAL-G までを一つのイメージに束ねたパッケージで、Operator 自体はその上で「望むクラスタを CRD で宣言すれば Spilo Pod を立ててくれる」比較的薄い制御層として動きます。実際の高可用性 — リーダー選出と自動フェイルオーバー — は各 Pod の中の Patroni が担い、アプリケーション接続にコネクションプーリングが要るなら PgBouncer を別途 connection pooler として一緒に立てられます。

ここで、よくある誤解を一つ解いておきます。「Patroni を使うと etcd や ZooKeeper のような外部の合意ストア(DCS)が別途必要だ」という認識ですが、Kubernetes の上ではそうではありません。 Zalando Operator の既定設定では、Patroni が Kubernetes のリソースそのもの(Endpoints、または ConfigMaps)を DCS として 使い、外部 etcd への接続は空のままにしておくのが既定の任意オプションです。Patroni はこの K8s オブジェクトに TTL(既定 30秒)付きのリーダー lock を取って定期的に更新し、リーダーが消えれば、残ったノードが WAL の位置を比べて新しいリーダーを決めます。

長所は、圧倒的な事例・情報の多さです。最も古く、大規模な本番で揉まれてきたので、問題に出会っても検索すれば前例が出てきます。ライセンスも寛容な MIT です。ただしこの Operator は本質的に Zalando が自社のニーズのために作ったものなので公式の商用サポートがなく、2026年現在も保守は続いていますが、リリースの速度は後発の CloudNativePG に比べて目に見えて遅くなっています。

Zalando Postgres Operator アーキテクチャ

② CrunchyData PGO

次は CrunchyData PGO です。GitHub リポジトリの紹介が “Production PostgreSQL for Kubernetes, from high availability Postgres clusters to full-scale database-as-a-service” であることからも分かるとおり、データベース専門の会社が作っただけあって 「データ保護とバックアップ」に最も重きを置いた Operator です。

HA 自体は Zalando と同じ系統です。各 Postgres Pod の database コンテナの中で PostgreSQL と Patroni が一緒に回って自動フェイルオーバーを担い、合意ストア(DCS)には Kubernetes API(Endpoints lease) を使います — こちらも外部 etcd は不要です。アプリケーションは PgBouncer(コネクションプーラー)と Service(rw/ro) を経由して Primary・Replica に接続します。

PGO の本当の個性はバックアップにあります。PostgreSQL バックアップツールの事実上の標準である pgBackRest を、各 Postgres Pod の サイドカーコンテナ専用の repo host Pod として統合しました。spec.backups.pgbackrest の設定だけで、すべてのトランザクションログ(WAL)を 最大4か所のストレージ(S3・MinIO・GCS・Azure Blob)へアーカイブし、ノードが丸ごと飛んでも特定時点復旧(PITR)と災害復旧(DR)が保証されます。バックアップ・復旧を本気で扱うなら、最も心強い選択肢です。

ただし、ホームラボに入れるには ライセンスの落とし穴 がありました。PGO のソースコードは Apache 2.0 ですが、本番用のコンテナイメージは Crunchy Data Developer Program の規約 に縛られていて、そのイメージを本番で使うには事実上の商用契約が必要です。個人の学習・ホームラボ程度なら使えますが、「いつでも社内・商用環境へ広げられるか」を基準に見ると、心穏やかでない制約でした。

CrunchyData PGO アーキテクチャ

③ Percona Operator for PostgreSQL

3つめは Percona Operator for PostgreSQL です。18年以上オープンソース DB のビジネスを続けてきた Percona が作っただけあって、「完全なオープンソース」という色が最も鮮明な選択肢です。

アーキテクチャの根は、さきほど見た CrunchyData PGO です。Percona は PGO を ハードフォーク(hard fork) して(3.0.0 から完全に独立したプロジェクトとして)発展させました。だから高可用性は同じく Patroni が、合意ストアは Kubernetes API(Endpoints lease) が、バックアップは pgBackRest が、コネクションプーリングは PgBouncer が担います — PGO の実績ある骨格をそのまま受け継いでいます。

Percona ならではの特徴は二つです。一つは PMM(Percona Monitoring and Management)統合 です。各 Postgres Pod に PMM Client サイドカー が付き、クエリ分析(QAN)・システムメトリクスはもちろん Patroni のメトリクスまで PMM Server へ送ります。複雑な設定なしに運用レベルの可観測性が付いてきます。

もう一つが決め手でした。コンテナイメージまで完全なオープンソース(Apache 2.0)で、利用の制約がありません。 さきほどの CrunchyData が本番イメージに商用契約を求めていたのと正反対です。Percona 自身も “Migrate to Freedom” という表現でこの点を打ち出しています。ホームラボから始めて、いつか社内・商用環境まで広げる可能性を考えると、この「制約なし」は大きな魅力でした。

それでも最終選考で外れた理由は、ただ一つ、重さ でした。PGO の系譜を継いでいるので Pod ごとにコンテナが複数(database・pgBackRest・PMM)付き、さらに PMM Server まで別に立てる必要があります。エンタープライズには妥当な構成ですが、19 vCPU を分け合う私のホームラボには、いささか重いものでした。

Percona Operator for PostgreSQL アーキテクチャ

④ StackGres

4つめは StackGres です。スペインの OnGres が作ったこの Operator は、単なる HA ツールを超えて 「Kubernetes 上の完全な PostgreSQL プラットフォーム(DBaaS)」 を掲げています。

高可用性の土台はやはり Patroni、合意ストアは Kubernetes API です(外部 etcd は不要)— ここまでは前述と同じです。StackGres の色は 「一つの Pod にすべてを詰め込む(batteries-included)」 という設計にあります。一つの Postgres Pod の中に、PostgreSQL+Patroni はもちろん、すべてのトラフィックを受け持つ Envoy プロキシ(必須)、コネクションプーラーの PgBouncer、メトリクスを取り出す postgres-exporter、ログを送る fluent-bit、ローカルの状態を調整する cluster-controller まで、複数のコンテナが一緒に回ります。ここでの Envoy は単なるプロキシではなく、Postgres のワイヤープロトコルを解釈して接続統計まで作ってくれます。

運用体験もよく練られています。Web コンソールと REST API が標準で組み込まれていて、kubectl でできるほぼすべての操作を画面から処理できます。バックアップは SGBackupSGObjectStorage の CRD で連続アーカイブ(ベースバックアップ + WAL)を S3・MinIO・GCS・Azure へ保存します。「バッテリー込み」という言葉どおり、始めたばかりの人には最も親切なオールインワンです。

問題は ライセンス でした。StackGres の中核コードは AGPL 3.0 です。単に使う分には構いませんが、この上に何かを載せてサービスとして提供した瞬間、ソース公開の義務が波及しかねません。今すぐは問題なくても、「このクラスタの上に自分のサービスを載せて外部へ提供するかもしれない」というホームラボの拡張シナリオを思うと、AGPL は前もって避けておきたい懸念材料でした。

StackGres アーキテクチャ

⑤ CloudNativePG (CNPG)

最後は CloudNativePG(CNPG)です。2022年にようやく登場した最も遅い後発組ですが、わずか2年で GitHub スターで Zalando・CrunchyData を追い抜き、今や最も人気のある PostgreSQL Operator になりました。EDB(EnterpriseDB)が作り、2025年1月に CNCF Sandbox へ寄贈、ライセンスは Apache 2.0 です。

CNPG が一気に先頭へ出た秘訣は、逆説的にも 「引き算」 でした。前の4つが共有していた Patroni を丸ごと取り払い、外部 DCS も使いません。 代わりに各 Pod の Instance Manager(PID 1 として動く Go プロセス)が PostgreSQL のネイティブバイナリを直接制御し、Kubernetes API(Endpoint Leases) を状態の唯一の真実(Source of Truth)とします。リーダー(primary)の決定とフェイルオーバーは、Operator(controller-manager) が reconciliation で直接さばきます。

結果は 極端なシンプルさ です。一つの Pod の中に Patroni も、Envoy も、いくつものサイドカーもありません — Instance Manager と PostgreSQL の二つだけです。その分オーバーヘッドが小さく、デバッグも容易です。バックアップは Barman Cloud を内蔵し、WAL を S3 互換ストレージへ連続アーカイブして PITR を提供し、接続は rwror の Service できれいに振り分けられます。

ガバナンスも安心です。CNCF プロジェクトなので特定の会社が勝手に終わらせることができず、複数のベンダーが商用サポートを提供し、この分野で開発の速度が最も速いです。次の章でこの5つを一つの表で比べたうえで、なぜ CNPG だったのかを整理します。

CloudNativePG アーキテクチャ

表でざっくり整理すると

Operator高可用性エンジン合意ストア(DCS)Pod 構成バックアップライセンスガバナンス · 活発さ
ZalandoPatroniK8s API (Endpoints/ConfigMaps)Spilo (PG+Patroni+WAL-G)、Pooler 別途WAL-G → S3MIT社内向け · リリース鈍化
CrunchyDataPatroniK8s API (Endpoints lease)database + pgBackRest サイドカー + repo hostpgBackRest(最大4 repo)Apache 2.0 · 本番イメージに制約OSS が事実上の商用化
PerconaPatroniK8s API (Endpoints lease)database + pgBackRest + PMM サイドカーpgBackRestApache 2.0 · 完全オープン(制約なし)安定した企業 · 活発
StackGresPatroniK8s APIbatteries(Envoy·PgBouncer·exporter·fluent-bit·controller)SGBackup · 連続アーカイブAGPL 3.0(コピーレフト性)OnGres 主力 · 活発
CloudNativePG独自の Instance Manager(Patroni なし)K8s API (Endpoint Leases)Instance Manager + PostgreSQL(2つ)Barman CloudApache 2.0CNCF · 現在 #1 · 最も活発

5つを並べると違いがはっきりします。①〜④はすべて Patroni を使います — 本当の違いは「その Pod に何を上乗せするか(バックアップ・モニタリング・プロキシ)」と「ライセンス」です。そして ⑤ CNPG だけが Patroni を外して 独自の Instance Manager を採用した、という点です。合意ストアは5つとも Kubernetes API です — 「Patroni を使うと外部 etcd が要る」というよくある誤解は、Kubernetes の上ではもう事実ではありません。

私のホームラボの基準は、はっきりしていました。19 vCPU を分け合うので 軽くなければ ならず、いつか社内・商用へ広げても ライセンスに縛られない こと、長く運用するので 廃れないガバナンス が要りました。


3. なぜ CloudNativePG を選んだのか

数多くの Operator を比べた結果、現在の私のホームラボ環境(k3s ベース)と今後の運用方針に最もよく合致したソリューションが CloudNativePG (CNPG) でした。その決め手は、大きく三つです。

① 極端なシンプルさと軽さ (Kubernetes Native)

ホームラボの性質上、リソース(CPU/メモリ)はエンタープライズ環境ほど潤沢ではありません。Zalando や他の Operator のように Patroni や外部 DCS(etcd、ZooKeeper)を立てるとなると、そのコンポーネント自体の状態(State)まで追加で管理する大きな負担が生まれます。CNPG は外部依存を完全に取り除き、Kubernetes API Server そのものを DCS として活用 します。Patroni のような別の HA プロセスを挟まず、PostgreSQL と、それを PID 1 として包んで管理する Instance Manager(Go プロセス) だけが動くので、オーバーヘッドが極めて小さく、構造が直感的でデバッグもずっと楽です。

② Barman による万全のバックアップ/復旧 (PITR)

データベースで最も大切なのは、何よりバックアップです。CNPG は PostgreSQL バックアップの事実上の標準である Barman(Backup and Recovery Manager) のクラウド版を内蔵しています。わずか数行の設定で、すべてのトランザクションログ(WAL)を S3 互換ストレージ(私の場合はホームラボ内に構築した MinIO、または Cloudflare R2)へ連続アーカイブします。ノードやディスクが物理的に壊れても、最後にアーカイブされた時点までのデータを、過去の特定時点(Point-In-Time Recovery)へ巻き戻せます。CNPG は既定の archive_timeout を5分に取り、5分単位の明確な RPO(目標復旧時点) を保証します。同期レプリケーションやより短いアーカイブ間隔で、この間隔をさらに縮めることもできます。

③ 宣言的な CRD — 次回 GitOps の完璧な素材

CNPG の Cluster CRD は徹底して宣言的(Declarative)です。PostgreSQL のバージョン、インスタンス数、リソース上限、ストレージ、バックアップポリシーまで、クラスタのすべての状態を一枚の YAML に収めます。

今回は Operator を helm でインストールし、クラスタを kubectl apply で直接立てます。

クラスタの状態が丸ごと YAML に収まるということは、その定義をそのまま Git に載せて宣言的に回せるという意味です。まさに 次回、このクラスタを含むホームラボ全体を ArgoCD で GitOps 化 する予定で、すべてを一つの CRD で表現する CNPG は、その移行に最も向いた素材です。

Git に載せた YAML がそのままクラスタの状態になり、変更をコミットすれば ArgoCD が同期して無停止のローリングアップデートで反映します。データベースを Kubernetes の中へ迎え入れると決めた以上、その定義までコードで管理されてこそ、ようやく中途半端でなくなります。

ホームラボ CloudNativePG アーキテクチャ


4. 実践:CNPG Operator のインストール

CNPG は Helm で Operator をクラスタ全体(cluster-wide)に一度デプロイしておけば、以降はどのネームスペースでも Cluster を作れます。公式チャートリポジトリを追加して cnpg-system ネームスペースにインストールします。

helm repo add cnpg https://cloudnative-pg.github.io/charts
helm repo update
helm upgrade --install cnpg cnpg/cloudnative-pg \
  --namespace cnpg-system \
  --create-namespace \
  --wait

CNPG チャートは「常に最新の point リリース」だけを提供します。特定のバージョンに固定するなら --version <chart-version> を付けてください。(この記事は Operator v1.24 基準です。)

インストールが終わったら、Operator Pod(Deployment 名は cnpg-controller-manager)が Running かを確認します。

kubectl get pods -n cnpg-system
# NAME                                       READY   STATUS    RESTARTS   AGE
# cnpg-controller-manager-8d447b4b6-xxxxx    1/1     Running   0          40s

CNPG のすべての操作はカスタムリソース(CRD)で行います。主要な CRD がきちんと登録されたかを確認します。

kubectl get crd | grep postgresql.cnpg.io
# backups.postgresql.cnpg.io
# clusterimagecatalogs.postgresql.cnpg.io
# clusters.postgresql.cnpg.io
# imagecatalogs.postgresql.cnpg.io
# poolers.postgresql.cnpg.io
# scheduledbackups.postgresql.cnpg.io

最後に、あとでクラスタの状態を見たり接続を検証したりするときに使う cnpg kubectl プラグイン を入れておきます。krew があれば一行です。

kubectl krew install cnpg
kubectl cnpg version
# Build: {Version:1.24.1 ...}

krew がなければ、CNPG の 公式インストールスクリプトでも入れられます。

Operator とプラグインが整いました。いよいよ実際のデータベースクラスタを立てる番です。


5. 実践:最初の高可用性 PostgreSQL クラスタをデプロイ

Operator が整ったので、実際にデータを入れる PostgreSQL クラスタを立てます。CNPG は Cluster という単一の CRD 一枚に、クラスタのすべての状態 — インスタンス数、PostgreSQL バージョン、リソース、ストレージ — を収めます。

以下は検証用に作った3ノードの高可用性クラスタのマニフェスト(demo-db.yaml)です。検証用なのでリソースは小さめにし(必要に応じて調整できます)、ストレージは k3s 既定の local-path を使います。

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
  name: demo-db
  namespace: cnpg-demo
spec:
  instances: 3                       # 1 Primary + 2 Replica
  imageName: ghcr.io/cloudnative-pg/postgresql:16.4

  storage:
    size: 1Gi
    storageClass: local-path         # k3s 既定 (WaitForFirstConsumer)

  resources:
    requests:
      memory: "256Mi"
      cpu: "100m"
    limits:
      memory: "512Mi"
      cpu: "500m"

ネームスペースを作って適用します。

kubectl create namespace cnpg-demo
kubectl apply -f demo-db.yaml

Operator はまず Primary を initdb でブートストラップし、そのあと Replica を一つずつ join させます。(自宅ノードで PostgreSQL イメージを初めて取得する場合、初回の起動に数分かかることがあります。)状態が Cluster in healthy state になれば完了です。

kubectl get cluster demo-db -n cnpg-demo
# NAME      AGE   INSTANCES   READY   STATUS                     PRIMARY
# demo-db   24m   3           3       Cluster in healthy state   demo-db-1

Pod は1つの Primary と2つの Replica で構成され、CNPG 既定のアンチアフィニティのおかげで、可能な限り別々のノードへ散らばります。実際、私のホームラボでは3つの Pod がそれぞれ別の Lima ノード(agent-2/3/4)に配置されました。

kubectl get pods -n cnpg-demo
# NAME        READY   STATUS    RESTARTS   AGE
# demo-db-1   1/1     Running   0          13m
# demo-db-2   1/1     Running   0          6m
# demo-db-3   1/1     Running   0          3m

Operator はあわせて、接続用の Service も三つ作ってくれます。書き込みは常に Primary へ向かう *-rw、読み取りは Replica へ分散する *-ro、そして Primary・Replica をすべて含む *-r です。

kubectl get svc -n cnpg-demo
# NAME         TYPE        CLUSTER-IP      PORT(S)    AGE
# demo-db-r    ClusterIP   10.43.89.62     5432/TCP   27m
# demo-db-ro   ClusterIP   10.43.198.255   5432/TCP   27m
# demo-db-rw   ClusterIP   10.43.55.136    5432/TCP   27m

6. 実践:接続の検証とレプリケーション状態の確認

クラスタが立ったので、実際に接続して動作を確認します。CNPG はクラスタ作成時に アプリケーション用 Secret(<cluster>-app) を自動で生成します。ここにはユーザー名・DB 名・パスワードと、すぐ使える接続 URI が入っています。(管理者/superuser アクセスは既定で 無効 で、必要なときに spec.enableSuperuserAccess: true で有効にします。だから既定の構成に *-superuser Secret はありません。)

kubectl get secret demo-db-app -n cnpg-demo -o jsonpath='{.data.username}' | base64 -d; echo
# app
kubectl get secret demo-db-app -n cnpg-demo -o jsonpath='{.data.dbname}' | base64 -d; echo
# app

接続は、§4 で入れた cnpg プラグイン が一番手軽です。Primary に直接 psql セッションを開きます。

kubectl cnpg psql demo-db -n cnpg-demo

セッションでレプリケーション状態を確認します。Primary が二つの Replica へ WAL をストリーミングしているはずです。

postgres=# SELECT application_name, client_addr, state, sync_state
           FROM pg_stat_replication ORDER BY application_name;

 application_name | client_addr |   state   | sync_state
------------------+-------------+-----------+------------
 demo-db-2        | 10.42.3.111 | streaming | async
 demo-db-3        | 10.42.6.236 | streaming | async
(2 rows)

二つの Replica がどちらも streaming 状態です。Patroni も、外部 DCS もなしに — ただ Kubernetes API と各 Pod の Instance Manager だけで — リーダーが選出され、ストリーミングレプリケーションが構成されました。既定のレプリケーションは非同期(async)なので書き込み遅延がほとんどなく、より強い保証が必要なら spec に同期レプリケーション(minSyncReplicas/maxSyncReplicas)を有効にできます。

ここまでで、ホームラボの k3s 上に、軽くてシンプルな PostgreSQL の高可用性クラスタが立ちました。あとは最後の一つ — このデータをどう安全に守るか、バックアップを付ける番です。


7. データを守る最後の一枚 — MinIO へのバックアップ

クラスタが立っても、バックアップがなければ道半ばです。ノードが丸ごと飛んだり、うっかりテーブルを消したりしたとき、ベースバックアップ + WAL アーカイブ があってはじめて、過去のある時点へ巻き戻せます(PITR)。

S3 互換 API — バックエンドは何でも

CNPG のバックアップエンジン Barman Cloud は、S3 互換 API でオブジェクトストレージへバックアップを上げます。ここで大事なのは、「S3 互換」が一つの標準インターフェースだという点です。だからバックエンドは AWS S3 でも、Google Cloud Storage・Azure Blob・Cloudflare R2 でも、セルフホストの MinIO でも 何でも構いません — 設定のエンドポイントと認証情報を変えるだけで、そのまま動きます。

CNPG バックアップ — S3 互換オブジェクトストレージ (MinIO)

今回選んだのは MinIO です。MinIO は S3 API をそのまま実装したオープンソースのオブジェクトストレージで、クラスタの中に直接立てて使えます。マネージドの S3 ではなく MinIO を選んだ理由は三つです。

  • データ主権:バックアップが外へ一歩も出ず、ホームラボの中にとどまります。
  • 費用ゼロ:クラウドストレージ・転送の料金がかかりません。
  • アクセスが単純:同じ k3s の中の minio.minio:9000 へ直接つながります。

いつか社外バックアップが必要になれば、同じ設定でエンドポイントを S3・R2 に変え、認証情報を差し替えるだけで済みます。バックエンドを入れ替えるコストがほぼないのが、S3 互換の強みです。

バックアップ設定を付ける

まず MinIO 接続の認証情報を、クラスタと同じネームスペース に Secret として置きます(キー名は自由、ここでは ACCESS_KEY/SECRET_KEY)。

kubectl -n cnpg-demo create secret generic minio-backup-creds \
  --from-literal=ACCESS_KEY='<minio-access-key>' \
  --from-literal=SECRET_KEY='<minio-secret-key>'

そして Clusterspec.backup.barmanObjectStore を足します。バケットのパス(destinationPath)、MinIO のエンドポイント(endpointURL)、そして今作った Secret を指定すれば完了です。

spec:
  backup:
    barmanObjectStore:
      destinationPath: s3://cnpg-demo/          # MinIO バケット
      endpointURL: http://minio.minio:9000      # クラスタ内部の MinIO
      s3Credentials:
        accessKeyId:
          name: minio-backup-creds
          key: ACCESS_KEY
        secretAccessKey:
          name: minio-backup-creds
          key: SECRET_KEY
      wal:
        compression: gzip

適用すると、Operator が PostgreSQL の archive_command を引き受けて WAL の連続アーカイブ を始めます。クラスタの ContinuousArchiving コンディションが立てば準備完了です。

kubectl get cluster demo-db -n cnpg-demo \
  -o jsonpath='{.status.conditions[?(@.type=="ContinuousArchiving")].status}'; echo
# True

最初のバックアップと確認

WAL は流れ続けますが、復旧の基準点になる ベースバックアップ は一度取っておく必要があります。Backup リソース一つで済みます。

apiVersion: postgresql.cnpg.io/v1
kind: Backup
metadata:
  name: demo-db-backup-1
  namespace: cnpg-demo
spec:
  method: barmanObjectStore
  cluster:
    name: demo-db
kubectl apply -f backup.yaml
kubectl get backup -n cnpg-demo
# NAME               CLUSTER   METHOD              PHASE       ERROR
# demo-db-backup-1   demo-db   barmanObjectStore   completed

completed です。実際に MinIO バケットを覗くと、ベースバックアップがタイムスタンプのディレクトリに積まれています。

# MinIO バケット cnpg-demo の中
demo-db/
└── base/
    └── 20260609T094230/      # ベースバックアップ(以降 WAL は wals/ に蓄積)

これでこのクラスタは、ノードが飛んでも — MinIO に積まれたベースバックアップと WAL で — 望む時点へ巻き戻せます。データを Kubernetes の中へ迎え入れつつ、そのデータを守る安全網まで、同じ宣言的なやり方で備えたことになります。


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

CNPG クラスタを、Tailscale を越えるノードにまたがって構成するのは避けたほうが無難です。

CNPG はノード間のネットワーク品質に敏感です。Primary は Replica へ WAL を絶えずストリーミングし、各 Pod の Instance Manager は Kubernetes API へ短い周期で状態(lease)を更新します。ところがこのホームラボのノード間通信は、Tailscale(WireGuard)メッシュの上に flannel VXLAN をもう一度重ねた二重カプセル化の構造で、クラウド(東京)–自宅(札幌)の区間は事実上 WAN です。この区間をまたいで Primary・Replica を配置すると、増えた RTT と揺らぎに、縮んだ MTU まで重なって、WAL のストリーミングが切れ、lease が失効します。実際、Lightsail ノードと Lima ノードにまたがってクラスタを構成したときは、レイテンシによる接続のドロップと Pod の再起動が果てしなく繰り返されました。

だから CNPG クラスタは、Tailscale を越えない同じ低レイテンシ区間の中に束ねることを強くおすすめします。nodeSelectoraffinity でクラスタを一つのサイト(クラウドノード同士、または自宅ノード同士)に固定すれば、ノード間通信が LAN 並みに安定します。サイトをまたぐ冗長化が必要なら、単一クラスタを WAN に分散させるより、サイトごとのクラスタ + 非同期の Replica Cluster という構成のほうが安全です。

振り返ると、CNPG の魅力は結局 シンプルさ でした。Patroni も外部 DCS もなしに Kubernetes API 一つでリーダーを決め、バックアップは S3 互換ストレージに任せる。軽くて、自由で(Apache 2.0・CNCF)、何よりクラスタからバックアップポリシーまで、すべてが一枚の YAML で宣言される — これが 19 vCPU の私のホームラボにも、その先にも、ぴたりと合いました。

まさにその「一枚の YAML」が次回の出発点です。今回は helmkubectl apply命令的に 立てましたが、クラスタもバックアップも、結局は宣言的なマニフェストです。次回は、このクラスタを含むホームラボ全体を ArgoCD で GitOps 化 します — Git に載せた YAML がそのままクラスタの状態になり、コミット一つがそのままデプロイになる、そうした形へ移っていきます。


参考 / 出典