AIに本番DBを見せるとき — 書き込みはプロンプトではなくDB権限で止める
execがあるのに調査用リードレプリカと踏み台を別に作った理由と、PostgreSQLで確かめたこと
先に結論。AIエージェントに本番データを見せるなら、書き込みはDBに拒否させ、読み取りも顧客が使うDBから切り離すのがいい。この記事を読めば、プロンプト・セッション設定・ロール権限・レプリカがそれぞれどこまで守ってくれるかがわかり、自分のサービスにどの層が要るかを選べる。
会社(Railsで作った契約管理SaaS、ECS Fargate + RDS PostgreSQL)で、本番に調査用リードレプリカと踏み台(bastion)サーバーを追加するという告知が出た。
最初に思ったのは「もうexecで本番コンテナに入ってrails consoleもSQLも使えるのに、なぜ道をもう一本作るのか」だった。調べてみると、既存の道は開発者が気をつけて使う前提で作られていて、今回は使う人が変わった。顧客対応エンジニアがAIエージェントに本番データの調査を頼むのだ。事故が起きたから作ったのではなく、起きる前に止めるためだ。
既存の道:ECS Execで稼働中のコンテナに入る
ECS Execは動いているコンテナの中でシェルを開く機能だ。入るとアプリと同じDB接続情報をそのまま使う。つまり書き込みできるプライマリ(マスター)DBに、アプリの権限でつながる。
分解すると問題は4つあった。
書き込み防止が言葉頼みだった。 照会用スキルのドキュメントに「update・destroy禁止」と書いてあるだけ。人なら気をつければいい。AIへの「読むだけにして」は確率的な約束だ。指示を一度読み落とすか、プロンプトインジェクション(データに紛れた指示に乗せられること)を一度受ければ update_all が飛ぶ。
読み取りもタダではない。 調査クエリは条件が毎回違うので、インデックスにあまり乗らない。数十GBのテーブルのフルスキャンが、顧客リクエストと同じプライマリで走る。しかもexecはWebリクエストを受けているコンテナの中で動く。
execセッションが非対話の使い方に合わない。 ローカルのstdinがEOFになると、2秒ほどでセッションが切れた。AIがコマンドを投げて結果を受け取る用途では、sleep N | でstdinを引き留める小技が必要だった。
ネットワークが思ったより開いていた。 既存のDBセキュリティグループは、VPCの範囲全体に5432を開けていた。踏み台をVPC内に置くだけでプライマリにも届く。そこでレプリカ専用のセキュリティグループを新しく作り、踏み台からの5432だけを許可した。これは権限制御というより「うっかりプライマリにつなぐ事故」を防ぐ仕掛けだ。
何が本当に書き込みを拒否するのか — 試してみた
「SELECTだけのロールを作ればいい」「セッションをread-onlyで開けばいい」「レプリカはそもそも書けない」。どれも正しそうだが、止める力が違う。ローカルにPostgreSQL 18.6のプライマリと物理レプリカを立て、20万行のcontractsテーブルで確かめた。
-- 調査ロール:SELECT権限のみ + default_transaction_read_only=on + statement_timeout=2s(プライマリ)
UPDATE contracts ... ERROR 25006 cannot execute UPDATE in a read-only transaction
SET default_transaction_read_only = off;
UPDATE contracts ... ERROR 42501 permission denied for table contracts
SELECT pg_sleep(5) ERROR 57014 canceling statement due to statement timeout(2.1秒)
SET statement_timeout = 0;
SELECT pg_sleep(3) OK(3.0秒)
-- アプリのロール:read-onlyのデフォルトだけ付けたとき(プライマリ)
UPDATE contracts ... ERROR 25006
BEGIN READ WRITE; UPDATE ...; COMMIT; OK
-- レプリカ:スーパーユーザーで
UPDATE contracts ... ERROR 25006
BEGIN READ WRITE ERROR 0A000 cannot set transaction read-write mode during recovery
CREATE ROLE x ERROR 25006 cannot execute CREATE ROLE in a read-only transaction
ここからわかったことは3つ。
read-onlyセッションは壁ではない。 default_transaction_read_only は「デフォルト値」なので、セッションが BEGIN READ WRITE 一行で越えられる。アプリのロールにこれだけ付けても、書き込みは普通に通る。最初はこれで十分だと思っていた。
ロール権限は壁だ。 read-onlyを外しても、SELECTだけ持つロールは permission denied で止まる。最低限これは要る。
物理レプリカはスーパーユーザーでも書けない。 リカバリ中のサーバーなので、読み書きモードに切り替えること自体が拒否される。だから調査用ロールもレプリカ側では作れない。プライマリで作ればレプリカに複製されて届く(上の実験でもそう届いた)。
おまけに、ロールに付けた statement_timeout もデフォルト値なので、セッションが0に戻せる。会社の設計では10分にしているが、これは重いクエリを流しっぱなしにする事故を切る仕掛けで、AIを縛る仕掛けではない。レプリカを分ける理由がここにもある。誰かがタイムアウトを外しても、顧客側のDBは重くならない。
レプリカの落とし穴:長い照会が途中で取り消される
レプリカを作るとすぐぶつかるのがこれだ。プライマリでVACUUM(削除した行を実際に片付ける掃除)が走ると、レプリカでその行を読んでいる照会とぶつかる。レプリカはしばらく待ってから照会を取り消す。同じ実験環境で6秒の照会を流しておき、プライマリで行を消してVACUUMを回した。
レプリカの設定 6秒の照会 プライマリのVACUUM
feedback off、待ち1秒 2.3秒で取り消し 消した行を片付けた
feedback on、 待ち1秒 最後まで通った 消した66,667行を片付けられず
feedback off、待ち30秒 最後まで通った 片付けた(その間レプリカの反映が止まる)
hot_standby_feedback をonにすると、レプリカが「この行はまだ読んでいる」とプライマリに伝えるので照会は切れない。代わりにプライマリのVACUUMがその行を片付けられない。調査クエリが長いほど、顧客が使う大きなテーブルが膨らむ。max_standby_streaming_delay を伸ばすと逆で、プライマリは無事なまま、レプリカのデータがその分遅れる。
会社では待ち時間を伸ばすほうを選んだ。大きなテーブルは十数GBあって、膨らむと顧客側が痛い。調査用なのでデータが数分遅れるのは構わない。レプリカを分けた目的(顧客のDBに触らない)を考えれば自然な選択だ。
どこまで止めるか — 選ぶ基準
| 止め方 | 止められるもの | 抜けるところ | いつ使うか |
|---|---|---|---|
| プロンプト・スキル文書に「読むだけ」 | たいていのミス | 指示の読み落とし・インジェクション一回 | 人がそばで見ているときだけ |
default_transaction_read_only |
うっかり出るUPDATE | セッションが BEGIN READ WRITE で越える |
ロール権限に足す補助として |
| SELECT専用ロール | すべての書き込み | プライマリの負荷・機微データの閲覧 | 最低限これ |
| 物理レプリカ + SELECT専用ロール | 書き込み + 顧客側の負荷 | データが遅れうる・インスタンス費用 | AIに常時調査を任せるとき |
会社の構成は最後の行だ。踏み台は公開IPもinboundもない小さなEC2で、SSMのポートフォワーディングでレプリカをローカルの localhost:5432 につなぐ。AIは普通のDBにつなぐのと同じようにpsqlを使えばいい。アプリの database.yml にレプリカの設定はない。アプリは一度もレプリカを使わず、純粋に調査専用だ。
まとめながら面白かったこと
この道で接続するには、もともと管理者権限が要る。その人はその気になれば何でもできる。だからこの構成が止めるのは悪意ではなく事故だ。セキュリティの境界というより「デフォルトを安全にする」設計に近い。
レプリカは普通、読み取り性能のために作る。ここでは隔離が目的だ。それに全テーブルSELECTなので、契約書の本文もAIが読めるようになる。これは技術では解けない。始める前に社内で合意を取った。
AIに運用データを扱わせる構成を考えていて、どこまで止めるべきか迷っているなら、negabaroに聞いてもらってかまわない。今の接続経路を一緒に描いて、必要な層だけを選べる。
キーコンセプト
AI調査用ロールにはSELECT権限だけ渡す — read-onlyセッション設定は補助にすぎない
ロールはプライマリで作る。物理レプリカではスーパーユーザーでも作れない
調査は顧客が使わないレプリカで。セキュリティグループでプライマリへの道を塞ぐ
長い照会の取り消しは、hot_standby_feedbackではなくレプリカの待ち時間を伸ばして解く