1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ZFSのCopy-on-WriteでPRごとのDBブランチを実現するKubernetes Operator

1
Last updated at Posted at 2026-05-31

image.png

はじめに

スキーマ変更の PR レビューで、こう言われたことはないでしょうか。

「これ、手元の本番相当データで動かして確認したいんだけど、DBのリストアに30分かかるんだよね」

「本番に近いデータが入ったDBをどう用意するか」って、地味だけどずっと付きまとう悩みですよね。共有のテストDBはテスト同士がぶつかるし、フィクスチャを保守するのもしんどい。かといって本番データをリストアするのは遅い。どれも一長一短です。

これを Git のブランチみたいな感覚でDBを払い出すことで解決できないか、と考えて作ったのが OSS「BranchDB」です。

この記事ではまず「Database Branching」って何?というところから始めて、既存サービスとの違いを整理し、そのうえで BranchDB の中身に入っていきます。

GitHub → https://github.com/MaSuCcHI/branchdb-operator


Database Branching とは何か

Git がコードをブランチで管理するように、データベースの「状態(スキーマ+データ)」をブランチで管理しよう、という考え方を Database Branching と呼びます。

開発環境のDBは、たいてい「全員で1台を共有」か「毎回フィクスチャを流す」のどちらかに落ち着きます。Database Branching はこの構図をひっくり返します。本番データのスナップショットを起点に、PRや機能ごとに独立したDBをサッと作って、使い終わったら捨てる。git checkout -b するのと同じノリで、自分専用の隔離されたDBが手に入るイメージです。

これで何が嬉しいかというと、こんな感じです。

  • テストの干渉がなくなる:ブランチごとに独立したDBなので、並列に走るテストがお互いのデータを壊さない
  • 本番が再現できる:本番データのスナップショットから作るので、フィクスチャでは再現しにくいバグも検証できる
  • マイグレーションを事前検証できる:実データに対して migration を流して、想定どおりか、どれくらい時間がかかるかを安全に確認できる
  • 気軽に使い捨てられる:PRをマージしたら捨てるだけ。コストはコピーオンライトの差分だけ

既存のサービスと、その「隙間」

この考え方、実はクラウド系のDBサービスでもどんどん採用が進んでいます。

サービス DB エンジン ブランチ対象 提供形態
Neon PostgreSQL スキーマ+データ SaaS(独自 Page Server / CoW)
PlanetScale MySQL(Vitess) スキーマのみ SaaS(DDL ブランチ)
TiDB Cloud Serverless TiDB スキーマのみ SaaS(クラスタ分岐)

この中だと Neon が「データごとブランチ」という意味でいちばん近くて、PostgreSQL のデータブランチをマネージドで提供しています。PlanetScale や TiDB Cloud はどちらかというとスキーマ(DDL)の管理が主目的で、本番データを丸ごと持ち込むような使い方はあまり想定されていません。

ここで、ぽっかり空いた「隙間」が見えてきます。

本番データごとブランチしたい。でも、データを外部の SaaS には預けたくない。しかも使ってるのは MySQL。

GDPR や個人情報、社内ポリシーの都合で「データはクラウドに出せない」というチーム、わりと多いんですよね。それに、すでにオンプレやプライベートクラウドで Kubernetes を回しているなら、できればそこに乗せたい。でも Neon は PostgreSQL 専用& SaaS のみなので、ここには手が届きません。

この隙間を埋めたくて作ったのが BranchDB です。


そこで「BranchDB」を作った

BranchDB は、セルフホストで・本番データごとの Database Branching を実現する Kubernetes Operator です。DatabaseBranch というカスタムリソース(CR)を Kubernetes に作るだけで、ZFS のコピーオンライトクローンでDBをパッと複製して、NodePort 経由で繋がる MySQL を払い出してくれます。

※ メインで検証しているのは MySQL です。PostgreSQL も同じ仕組みで動く想定で実装してありますが、こちらはまだ実験的な位置づけ(検証中)だと思ってください。

git push origin feature/payment-v2
→ DatabaseBranch CR を作成
→ Operator が ZFS スナップショットをクローン → MySQL Pod 起動
→ NodePort が割り当てられる
→ テストが mysql://192.168.1.100:31234/ に対して実行される
→ PR マージ → CR 削除 → リソースが自動クリーンアップ

ZFS のクローンはコピーオンライトなので、100GBのDBでもブランチ作成は数秒で終わります。スナップショット時点のデータはそのまま見えて、書き込みは差分だけ記録されるので、容量もほとんど食いません。

ほかのやり方と並べてみると、BranchDB の立ち位置がはっきりします。

アプローチ データ 速度 セルフホスト 本番データ コスト
Docker Compose + dump restore フルコピー 遅い(数十分〜) ✅(リストア必要)
Testcontainers フィクスチャ 中(起動+流し込み) △(フィクスチャ管理)
Neon Branching フル(CoW) 速い ❌ SaaS のみ ✅(PG のみ) SaaS 課金
PlanetScale Branching スキーマのみ 速い ❌ SaaS のみ SaaS 課金
BranchDB フル(ZFS CoW) 速い(〜数秒) ✅(MySQL / PG は実験的) OSS

Testcontainers はフィクスチャをコードで管理できるのが強みで、用途が合うならまずこっちが候補です。BranchDB が刺さるのは、「本番っぽいデータで再現テストしたい」「スキーマ変更が既存データに与える影響を見たい」みたいに、フィクスチャ維持の手間が割に合わなくなってくる場面です。

なぜ Kubernetes Operator にしたのか

最初から Kubernetes だったわけではありません。最初は1台のVMの上で、ZFS のクローンと MySQL プロセスをまとめて動かしていました。これはこれで普通に動きます(要望があればVM版も公開します)。

ただ、ブランチを増やすほど、1台のVMの上で何本もの MySQL が同時に走ることになります。そうなると、性能を上げる手段がインスタンスのスケールアップ(垂直スケール)しかありません。ブランチが増えるたびに、もっと大きいVMに載せ替え続ける。そういう未来が見えてきて、これはしんどいなと。

そこで踏み込んだのが、ストレージとコンピュートの分離です。Neon や Snowflake と同じ発想で、データを置く層(ストレージ)と、それを処理する層(コンピュート)をスパッと切り離します。

  • ストレージ側:ZFS サーバー上の ZFS Agent が1か所で集中管理する。スナップショットとクローン(=データの実体)はぜんぶここに集まる
  • コンピュート側:各ブランチDBは Kubernetes の Pod として動く。複数ノードに分散できるので、ノードを足せばそのまま水平にスケールする

クローンしたボリュームは NFS で Pod 側にマウントするので、データの実体をコピーせずに計算リソースだけ横に広げられます。これで「VMをデカくし続けるしかない」状態から、「ブランチが増えたらノードを足せばいい」状態に変わりました。


アーキテクチャ

コンポーネントは3つです。

[CI/CD・curl など]
        │ HTTP :8080
        ▼
┌─────────────────────┐
│   API Server         │  REST API + Web コンソール(React SPA)
│   (cmd/branchdb)     │  DatabaseBranch CR の CRUD
└──────────┬──────────┘
           │ watch / CRUD
           ▼
┌─────────────────────┐
│   Kubernetes         │
└──────────┬──────────┘
           │ watch
           ▼
┌─────────────────────┐
│   Operator           │  controller-runtime の Reconciler
│   (cmd/operator)     │
│   ├─ VolumeProvider  │──→ ZFS Agent(別ホスト)
│   └─ DatabaseProvider│──→ Pod + PVC + NodePort Service
└─────────────────────┘
           │ HTTP :9090
           ▼
┌─────────────────────┐
│   ZFS Agent          │  ZFSサーバー上で動く軽量HTTPサーバー
│   (cmd/zfsagent)     │  snapshot / clone 操作を実行
└─────────────────────┘

Operatorcontroller-runtime ベースの Reconciler です。DatabaseBranch CR の変化を watch して、ZFS Agent に clone を依頼し、Pod / PVC / Service を作成・削除します。

API Server は REST API と React 製の Web コンソールを提供します。POST /branches でブランチを作成、GET /branches/{name} で接続情報(DSN)を取得できます。Swagger UI も /docs で見られます。

ZFS Agent は ZFS サーバー上(Kubernetes の外)で動く小さな HTTP サーバーです。zfs snapshotzfs clonezfs destroy をラップして API 化していて、Bearer トークンで認証します。

image.png
image.png


技術的な工夫

ZFS クローンで「DB ブランチ」を実現

Git がファイルをブランチで管理するのと同じノリで、ZFS のスナップショットとクローンでDBの状態を管理します。

tank/mysql@base          ← スナップショット(本番データのコピー)
    ├─ tank/mysql/branches/feature-payment  ← ZFS クローン(PR用)
    ├─ tank/mysql/branches/feature-search   ← ZFS クローン(PR用)
    └─ tank/mysql/branches/hotfix-123       ← ZFS クローン(PR用)

zfs clone tank/mysql@base tank/mysql/branches/feature-payment は一瞬で終わります。100GBのDBでも変わりません。

NFS 上でデータベースを壊さないための工夫

ストレージとコンピュートを分けた結果、ブランチDBの Pod は ZFS クローンを NFS 経由でマウントして使います。で、ここが最大のハマりどころでした。MySQL も PostgreSQL も、デフォルトはローカルディスク前提の I/O 設定になっているので、そのまま NFS の上で動かすとデータファイルが壊れたり、起動が異様に遅くなったりするんです。

とはいえ、これは逆にラッキーでもありました。BranchDB が相手にしているのは、あくまで開発・テスト用の使い捨てDB。「壊れたら作り直せばいいや」「データが多少飛んでも本番じゃないし」と開き直れるので、本番では口が裂けても言えないような割り切った設定を、堂々と入れられたんです。制約があったからこそ思い切れた、という感じですね。

なので先に正直に言っておくと、

⚠️ これから挙げる設定は、データの安全性より速度を優先しています。BranchDB は使い捨てDB前提なので、あえてこのトレードオフを選んでいます。本番DBには使わないでください。

工夫1: NFS マウントオプションの選定(Kubernetes PV 側)

var nfsMountOptions = []string{
    "hard",          // ネットワーク瞬断時にI/Oをブロック(softは絶対NG)
    "proto=tcp",     // UDP より信頼性の高い TCP を使用
    "nfsvers=4.1",   // NFSv3 の NLM ロック問題を回避
    "rsize=1048576",
    "wsize=1048576", // ブロックサイズ 1MB でスループット最大化
    "retrans=2",
}

ここでいちばん大事なのは soft絶対に使わないこと。soft だとネットワークが一瞬切れたときに I/O エラーを返してしまって、DBファイルが中途半端な状態で固まります。hard にして I/O をブロックさせておけば、そこでデータが壊れるのを防げます。

工夫2: MySQL は innodb_flush_log_at_trx_commit=2

[mysqld]
innodb_flush_log_at_trx_commit=2

デフォルトの 1 はトランザクションのたびに fsync を呼びます。NFS の fsync はとにかく遅いので、これだと開発用途でも待ってられません。2 にすると OS キャッシュへの書き込みで完了扱いになり、実際の fsync は OS に任せます。これだけで体感がガラッと変わります。

おまけ: initContainer で UID/GID の権限問題を解決

地味ですが、これがないとそもそも Pod が起動しません。

MySQL / PostgreSQL は UID=999 で動く
NFS マウントしたディレクトリは root 所有
→ initContainer で `chown 999:999` を実行してから本体を起動

MySQL 以外のデータベースにも広げやすい

メインは MySQL ですが、PostgreSQL も同じ仕組みに乗せて実験的に動かしています。ここでのポイントは DB ごとの違いが「起動コマンドと NFS 向けの設定」だけに収まっていることです。新しいDBを足したくなっても、そのDB向けの設定とヘルスチェックを書けばだいたい済みます。払い出しの処理を DatabaseProvider というインターフェースで抽象化しているので、Redis みたいな別のデータストアも同じ仕組みの延長で足せる、という作りになっています。

ユースケースに合わせた2つの発行手段

「誰が・どこからブランチを払い出すか」はチームによってけっこう違います。なので BranchDB では入口を2つ用意しました。

ひとつは REST API です。CI/CD のパイプラインや自動化スクリプトから叩く用途を想定しています。POST /branches を投げると、レスポンスに接続用の DSN がそのまま返ってくるので、GitHub Actions などからワンステップでテスト用DBを払い出せます。Swagger UI(/docs)も同梱です。

もうひとつが Web コンソール です。「今あるブランチを一覧で眺めたい」「手で1個サッと作りたい」「いらないブランチを掃除したい」みたいなのは、ブラウザでやったほうが断然早い。そのための管理コンソールを React + TypeScript で作って、Go バイナリに //go:embed で埋め込んでいます(なのでデプロイするバイナリは1個だけ)。

機械向けの REST API と、人間向けの Web コンソール。この2つがあることで、CI 連携から日々のちょっとした運用まで、ひと通りカバーできます。


どんなチームに向いているか

ここまで読んでもらった内容を踏まえて、BranchDB が向くケース・向かないケースを整理しておきます。

向いているケース

  • オンプレまたはプライベートクラウドで Kubernetes を運用している
  • 本番データを外部サービスに預けられない(GDPR・個人情報・社内ポリシーなど)
  • MySQL を使っている(PostgreSQL も実験的にサポート)
  • 数十〜数百 GB の大容量DBでも高速なブランチ環境が必要

向いていないケース

  • フルマネージドで OK かつ PostgreSQL のみ → Neon の方が簡単
  • ZFS サーバーを用意できない
  • Kubernetes を使っていない

Web コンソールでできること

先ほど触れた管理用 Web コンソールでは、ブラウザから次の操作ができます。

  • ブランチ一覧・作成・削除
  • Pod ステータス・接続情報の確認
  • スナップショット管理(取得・上書き・削除・GC)
  • Full Refresh(全ブランチ・スナップショットを破棄してクリーンな状態に)
  • Swagger UI(/docs

※ ここに Web コンソールのスクリーンショットを1枚貼ると、GUI があることが一目で伝わります。


インストールと使い方

インストールは Helm でだいたいワンコマンドです。手順や設定値の細かいところは README にまとめてあるので、そちらを見てもらえればと思います。

https://github.com/MaSuCcHI/branchdb-operator

ざっくり流れだけ書くと、Helm で入れたあと POST /branches を叩くと、レスポンスに接続用の DSN がそのまま返ってきて、すぐテストに使える、という感じです。

⚠️ まだベータ版です。今のところ動作確認できているのは Colima の VM 上に立てた k3s 環境 だけで、本格的なマルチノードや本番運用での検証はこれからです。「うちの環境だと動かないよ」「ここハマった」みたいな報告、めちゃくちゃ助かります。ぜひ Issue で教えてください 🙏


今後の予定

  • Amazon FSx for OpenZFS への対応。今は自前の ZFS サーバーが前提ですが、ここを FSx for OpenZFS に差し替えられれば、EKS 上でマネージドな ZFS をそのまま使えます。VolumeProvider インターフェースで抽象化してあるので、その実装を1つ足せば対応できるはずです
  • Redis ブランチのサポート
  • OIDC 認証・マルチテナント(Pro 版)
  • branchctl CLI の整備
  • GitHub Actions との連携テンプレート

詳しくはロードマップ参照 → https://github.com/MaSuCcHI/branchdb-operator/blob/main/docs/roadmap.md


おわりに

「PRごとに本番データ入りのDBを一瞬で用意する」という体験を、できるだけシンプルな形で実現したくて作りました。ZFS のクローンという枯れた技術と Kubernetes を組み合わせたことで、ちゃんとスケールする形にできたかなと思っています。

ちなみに、コンセプトやアーキテクチャの設計は自分で考えましたが、実装はほぼ Claude Code に任せました。「こういう仕組みにしたい」というアイデアを投げて、コードに落としてもらう、という進め方です。ここまで作り切れたのは正直 Claude Code のおかげが大きいです。

なんなら、この記事自体も Claude と一緒に手直ししながら書きました。設計も、実装も、文章も。いやー、いい時代になりました。LLM 万歳。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?