テナント別 SQLite による安全な AI Agent アクセス
本番 DB に AI Agent を直接接続させず、テナント別 SQLite Mirror + Read Only で分析基盤を分離しました。構成、同期方式、運用上のトレードオフをまとめます。
by Speria Engineering
- 本番 PostgreSQL からテナント別 SQLite(Turso)への Read Only Mirror
- AI Agent と分析ダッシュボードはミラー側にのみ接続
- 3 つの分離で AI Agent から本番 DB を守る
- テナント単位の分離
- 書き込み権限の分離
- 同期処理の分離
- 用途は分析と LLM 参照に限定
- 秒〜分の遅延と結果整合性は許容する
- リアルタイム監視や厳密な金額計算には使わない
1. AI Agent から本番 DB へのアクセス
LLM や AI Agent を業務システムに組み込むと、DB 接続の扱いが早い段階で問題になります。大きくは 3 つの方法があります。
- 本番 DB に直接接続させる
- Read Replica を経由させる
- スナップショットや Mirror を経由させる
ここでいう本番 DB は、複数 Workspace のデータを同じ PostgreSQL で扱うマルチテナント DB です。通常のアプリケーション経由では Workspace の境界をコードで強制できますが、AI Agent に汎用 SQL を発行させる場合は、その境界をクエリごとに守る必要があります。
本番 DB に LLM を直接接続させる構成は本番運用と相性がよくありません。
- LLM が誤った
DELETE/UPDATEを生成した場合、本番データを変更する経路になる workspace_idの条件漏れや join ミスで、テナント間のデータが混ざって見える- 重い分析クエリが OLTP のレスポンスタイムを劣化させる
- AI Agent が走らせたクエリが本番 DB の監査ログに混ざる
これらを「LLM のプロンプト設計と権限管理だけで守る」アプローチには限界があります。boringSQL の「Don't let AI touch your production database」も、AI に本番を触らせない立場から書かれています。
Speria ではマルチテナントの本番 DB から切り離した、テナント別 SQLite の Read Only Mirror を置きました。AI Agent と分析ダッシュボードは Mirror 側だけに接続します。
per-tenant DB の考え方では、reconfigured の「Single-Tenant Database Architecture: The Secret Weapon for B2B Apps in the AI Era」が security / noisy neighbor / AI query isolation の観点を整理しています。Speria ではその考え方を、本番 DB を読み取り負荷からも切り離す Mirror として扱っています。
同期には after_commit → Cloud Logging → Pub/Sub を使っています。DB のログを直接読む方式ではなく、transactional outbox に近い実装です。
2. テナント別 SQLite Mirror + Read Only
構成はこうです。
[本番 PostgreSQL]
│ after_commit → 構造化ログ
↓
[Cloud Logging → Pub/Sub]
│ Worker
↓
[テナント別 SQLite(Turso)] 1 Workspace = 1 物理 DB
│ Read Only
↓
[AI Agent / 分析ダッシュボード]
狙いは次の 3 点です。
- AI Agent と分析ダッシュボードは Mirror にしか接続しない
- Mirror は本番のスナップショットで書き込み権限を持たない
- 同期処理を本番のレスポンスタイムに乗せない
クエリ実行先を Mirror に限定すると、分析ダッシュボードや LLM の試行クエリが本番 PostgreSQL のコネクション、キャッシュ、クエリログを消費しません。実行計画の外れや大きな集計が起きても、影響範囲は Mirror 側に閉じます。
3. SQLite(Turso)の採用
最初に決めたのは、Mirror を本番 PostgreSQL から切り離してテナント単位で持つことです。必要だったのは強い OLAP 性能ではなく、DB を分けやすいこと、SQL 方言が小さいこと、ローカルでも同じ形で動かせることでした。
候補は次のとおりです。
| 候補 | テナント分離 | LLM 親和性 | コスト感 | 採用判断 |
|---|---|---|---|---|
| SQLite(Turso) | DB 単位で分離 | 型が単純で方言が小さい | テナント数増加に強い | ✅ |
| DuckDB | DB ファイル単位で分離可 | OLAP 特化で参照用途には過剰 | 同上 | ❌ |
| BigQuery | プロジェクト / データセットで論理分離 | 方言と特殊型が多く LLM が誤りやすい | クエリ課金でテナント運用が膨らむ | ❌ |
| Read Replica | 物理 1 つ共有、分離はアプリ層 | 本番と同じ schema | レプリカ運用が必要 | ❌ |
| DB ログ起点の CDC + 任意 DB | 採用先による | 採用先による | DB ログ依存で運用が重い | ❌ |
Read Replica を選ぶ場合は、Rietta の「Protect from AI Agentic Risks」のように RBAC と字句検証を重ねる設計になります。ただし Speria の要件では、共有レプリカに LLM や分析クエリが集まる問題は残ります。Speria では参照先そのものをテナント別に分ける方を優先しました。
SQLite を選んだ理由は次の 3 つです。
- テナント分離がしやすい
- 1 Workspace = 1 DB の単位で考えやすい
- Mirror を破棄して作り直す運用とも相性がよい
- SQL 方言が小さい
- PostgreSQL 特有の
tsvectorやjsonbを Mirror 側に持ち込まずに済む - AI Agent に渡す参照用 schema を小さく保ちやすい
- PostgreSQL 特有の
- ローカルで再現しやすい
- 開発時は
*.dbファイルで動かせる - Turso クラウドに接続しなくても Mirror 経路のテストができる
- 開発時は
そのうえで Turso を選んだのは、SQLite 互換の DB をクラウド側でテナント単位に持てるためです。ファイルベースの SQLite だけでは、接続先管理、バックアップ、リモートからの接続、将来的な同期の扱いを自前で見ることになります。古い記事ですが、Turso 公式の「Analytics for Per-User Database Architecture」も、per-user DB と analytics を組み合わせる場合の集計経路を扱っています。Turso なら SQLite に近い開発体験を残しつつ、クラウド側の運用に寄せられます。
Turso 公式の Agent Databases ガイド が示す database-per-agent パターンも参考にしました。Speria の Mirror では「テナント = AI Agent が参照してよい作業領域」に近い境界を作っています。
4. 設計を支える 3 つの分離
この構成では、テナント、書き込み権限、同期処理をそれぞれ分けています。
4.1 テナント単位の分離
- 1 Workspace = 1 物理 SQLite DB で分離する
- アプリケーションロジックで絞り込むのではなく接続先 DB そのものが別
- Workspace のスコープ外でクエリを発行しようとすると
NoTenantErrorで例外が上がり、コードレベルで境界を強制する
接続先 DB が別なので、クエリのバグでテナント間データが混ざる事故を避けやすくなります。
4.2 書き込み権限の分離
- Mirror DB は本番 PostgreSQL のスナップショット
- AI Agent / 分析ダッシュボードからは読み取り専用クライアントでしか接続できない
- 書き込みは内部の同期 Worker が独占する
LLM が誤って DELETE や UPDATE を生成しても、書き込み権限がなければ空振りに終わります。AI Agent の hallucination が本番データを書き換える経路を断てます。
4.3 同期処理の分離
同期は次の経路で動きます。
本番 Rails の after_commit
→ 構造化ログ(1 行 JSON)
→ Cloud Logging
→ Pub/Sub
→ Worker → SQLite Mirror に書き込み
これは厳密には CDC(Change Data Capture)ではありません。CDC は DB の変更ログを起点に配送する方式ですが、ここではアプリ層の callback を起点にしています。本番 DB の WAL を読む方式とは性質が違います。
非同期にすると本番側を巻き込みにくくなります。
- 本番処理の応答時間に Mirror 同期コストが直接乗らない
- Mirror 障害は結果整合性の遅延として吸収し、本番処理に直接波及させない
- 本番側のトランザクションは Mirror の事情を知らずに完結する
5. 運用上のトレードオフ
Mirror 構成は本番 DB への悪影響を避けやすい反面、いくつかのトレードオフがあります。
5.1 リアルタイム性は犠牲にしている
- 同期は秒〜分単位で遅延する(Cloud Logging → Pub/Sub → Worker 経由のため)
- 「Mirror で見える値 = 本番の最新の値」ではない
分析や可視化では分単位の遅延を許容できます。アラート、決済、在庫の即時反映などリアルタイム性が要件になる用途では別の経路を使います。
5.2 整合性は結果整合性まで
本番 PostgreSQL と Mirror の間では一時的な不整合が起きえます。厳密な金額計算、監査ログのソース、法的証跡には使えません。「分析・可視化・LLM 参照」専用と割り切る前提の設計です。
5.3 Mirror の運用コスト
- テナント数だけリソースと運用対象が増える
- 接続情報管理や障害対応の対象が線形に増える
- スキーマ変更時は全テナント DB へのマイグレーションが必要になる
Turso は 2026 年 5 月に有料プランにおける active database 数の上限撤廃を発表しました。DB 数そのものは以前より制約になりにくくなっています。
ただし、ここでいう運用コストは Turso の DB 数上限だけの話ではありません。各 Mirror DB のストレージ、読み書き、同期量、接続情報、監視、再構築の対象は残ります。テナント別 DB はコストをゼロにする設計ではなく、テナント分離のために運用単位を増やす設計です。
Mirror は本番 DB のスナップショットなので固有データを持ちません。互換性のないスキーマ変更が必要になった場合は、全テナントの Mirror DB を破棄して作り直します。Source of Truth は PostgreSQL 側にあるため、再構築コストは初回バックフィルと同じオーダーで済みます。非互換の変更が発生しても複雑なマイグレーションを避けられます。
6. アプリ層イベントを Cloud Logging で配送する
非同期分離は Cloud Logging と Pub/Sub で実装しました。
同期方式の比較
| 方式 | アプリ変更 | 配信保証 | 運用負荷 | 採用 |
|---|---|---|---|---|
直接同期(after_commit で Mirror に書く) | あり | 失敗時に本番処理が引きずられる | 低 | ❌ |
| ジョブキュー(SolidQueue 等) | あり | キューの可用性に依存 | 中 | ❌ |
| DB ログ起点の正規 CDC | なし | DB ログ依存で堅牢 | 高(運用が重い) | ❌ |
| Cloud Logging → Pub/Sub(transactional outbox 風) | あり | ログ基盤 + Pub/Sub の at-least-once | 低(マネージド) | ✅ |
DB ログ起点の正規 CDC は堅牢ですが、現在のチーム規模には運用負荷が重いと判断しました。after_commit を outbox の起点に使えばアプリ層の変更は必要です。その代わり、イベント配送はマネージドサービスだけで完結します。
Pub/Sub は at-least-once の配送なので、Worker 側は同じ同期ログを複数回受け取っても同じ Mirror 状態に収束する前提で実装します。失敗時は本番処理を巻き戻さず、Mirror の遅延として扱って再配送や再構築で吸収します。
同期ログのフォーマット
ログは 1 行 JSON で、5 フィールドだけに絞っています。
{
"namespace": "dbMirrorSync",
"model": "Reservation",
"action": "create",
"record_id": 123,
"workspace_id": 456
}
ログには「何が起きたか」だけを書きます。解釈は Worker 側の責務です。たとえば「Deactivation の create を受けたら Mirror 側で対応する Reservation を物理削除する」といった判断は Worker に寄せます。ログ自体には書きません。
この分担にしておくとソースモデルが増えても拡張しやすくなります。Worker 側の解釈テーブルを追加するだけで、ログのフォーマットには手を入れずに済みます。
SyncLoggable concern の Ruby 実装、Logging::LogEntries の構造、Terraform の Sink / IAM 設定は連載の次回で扱います。
7. ローカル開発
SQLite を選ぶとローカル開発も楽になります。
- 開発時はファイル 1 個で完結する
- Turso クラウドに接続せず
*.dbファイルを直接読み書きできる - AI Agent が触る分析パスのテストを手元で実行できる
OSS 化している ActiveRecord アダプタを使うと、DATABASE_URL を file:./tmp/mirror.db に切り替えるだけで開発に入れます。Turso のアカウントを開発者全員に配る必要はありません。
LLM 連携の開発体験は試行錯誤の回数に左右されます。ローカルですぐ試せることは、設計判断としても重要でした。
8. まとめ
- AI Agent に本番 DB を直接触らせない
- テナント別 SQLite Mirror + Read Only を採用
- AI 分析や LLM 参照の用途では現実的な落としどころになる
- 3 つの分離(テナント単位 / 書き込み権限 / 同期処理)で別々の経路を塞ぐ
- トレードオフは率直に認識する
- リアルタイム性・強整合性・運用コストとは引き換えになっている
- これらが要件になる用途には別の経路を引く必要がある
- 同期は CDC ではなく transactional outbox
- アプリを経由しない更新は拾えないという限界がある
AI Agent を業務システムに組み込むプロダクトでは、似た判断が必要になるはずです。本番 DB から切り離した Read Only Mirror と transactional outbox の組み合わせは選択肢の一つになります。