「このUPDATE、誰が流したの?」。監査でそう聞かれて、答えられない。各自のPCにDBクライアントを配って本番を触らせる運用は、たいていどこかでこの一点に詰まる。
先に断っておくと、筆者はLibreDB Studio(MITライセンスのOSS)の開発チームの一員だ。自分たちのツールの話が入るので宣伝に見える部分はあると思うが、ここで書きたいのは機能の一覧ではなく、「DBを触るツールをどこに置くか」という設計上の判断と、その理由だ。
手元にDBクライアントを配る運用は弱い
チームで本番DBを触るとき、各自のPCにGUIのDBクライアントを入れ、接続情報を配って回る運用は珍しくない。これが地味に効いてくる。
- 本番DBの生パスワードが人数分、各PCのローカルに散らばる
- 誰がいつどのクエリを流したか、後から追えない
- 退職・異動のたびに、配った認証情報をどう回収するかが曖昧になる
接続情報を配った時点で、管理はかなり難しくなる。冒頭の「このUPDATEを流したのは誰か」に答えられない、という状況はできれば避けたい。配った時点で統制が難しくなるなら、いっそ配らない方向で考えればいい。出発点はそのくらい単純な話だった。
IDEはDBの隣に置く
LibreDB Studioの発想はシンプルで、IDEをDBの隣(同じネットワーク内)にコンテナやHelm、npmで立て、利用者はブラウザからアクセスするだけにする。
docker run -p 3000:3000 libredb/libredb-studio
こうするとDBへの接続情報はサーバー側に一元化され、各PCに生パスワードを配る必要がなくなる。あとは利用者がブラウザからアクセスするだけでいい。
その上で、MITの範囲内で次のものが使える。
- SSO(OIDC)による認証
- RBACによる権限分離
- 全クエリの監査ログ
監査ログだけあっても、SSOで実行者が特定できなければ「誰が」が埋まらない。RBACがなければ全員が同じ権限で入ることになる。この三つはどれか一つでは統制として成立しないので、まとめて同じ層に用意している。
「全部同じUIに載る」は、すぐ破綻する
対応エンジンは16種類ある。ただ、この記事で名前を出したいのは、共通UIに載せようとすると素直に載らないものだけだ。実装していて一番の発見は、そこで正直に線を引いたところだった。網羅的な一覧はリポジトリにある。
Druid・Elasticsearch・OpenSearchは分析・検索が主用途で、このIDEの対話的なSQLではUPDATEやCREATE TABLEといった書き込み系を対象にしていない(Druidのバッチ取り込みやESのインデックスAPIといった専用の書き込み経路は別の話だ)。ここで無理に共通UIへ寄せると、編集ボタンは並んでいるのに実行すると謎のエラーが返る、という最悪の使い勝手になる。対応していないものは「サポートしていません」と明示する方がいい。
Cassandraはもっと厄介だ。フッターの件数欄が実データと食い違う。500行きっかりのテーブルで件数を取ると、143のような数字が出る。
-- 実際には500行あるテーブル
SELECT count(*) FROM events; -- 完走すれば実数(500)を返すが、全走査するため大きなテーブルではread timeoutで落ちやすい
count(*)自体は全パーティションをスキャンして実数を返すもので、フッターの件数欄が使っているのはそれではない。Cassandraは各ノードがトークンレンジ単位で保持するsystem.size_estimates(推定パーティション数)を集計して概算を出しており、UIの件数欄はそちらを表示している。この推定値はcompaction前後やtombstoneの影響で実数から大きくずれるため、500行のテーブルで143のような数字になる。分散ストレージの構造上、ずれること自体は異常ではない。
だが、フッターに「143件」と平然と出せば、ユーザーはそれを事実だと信じてしまう。間違った数字を出すくらいなら件数欄は空にする。Cassandraでは行数を表示しないことにした。UIの一貫性より、表示する数字が嘘でないことのほうが大事だ。
Trinoを繋ぐと、今度はテーブル一覧の横に主キーやインデックスを出そうとして詰まる。Trinoはクエリエンジンであってデータベースではなく、背後のカタログを束ねて実行するだけなので、そもそもインデックスという概念を持たない。メタデータ取得を全エンジン共通のインターフェースにするのは諦め、エンジンごとに「何に対応していて何に対応していないか」を宣言させる形に変えた。
ワイヤ互換のエンジンも同じだ。プロトコルがワイヤ互換なので、MariaDBやTiDB、ValkeyはMySQL/Redisのクライアントとして接続はできる。ただし、接続できることと全パネルが動くことは別の話だ。互換性はプロトコルの話であって、EXPLAINの出力形式やシステムテーブルの中身までは一致しない。そのため、ER図は出るか、スキーマ差分は取れるか、EXPLAINは可視化できるかを、互換エンジンごとにパネル単位で実際に叩いて確認している。地味な作業だが、ここを実測しないまま「対応」と書くわけにはいかない、と思っている。
ローカルLLMなら、データは外に出ない
AIの接続先は差し替え式にした。狙いはモデルを自分で選べるようにすることで、GeminiやOpenAIのほか、ローカルのOllamaを指定できる。自然言語からSQLを生成し、生成したSQLは人が確認してから流す前提で、エージェント自体はリードオンリーで動く。
個人的にはここが本題だ。Ollamaのようなローカルモデルを指定した場合、プロンプトもスキーマもデータも外部APIへ出ていかない。GeminiやOpenAIを指定すれば当然データはネットワークの外に出るが、ローカルモデルならその通信自体が発生しない。これで、機密データや規制対象のデータを扱う環境でもAI支援を切らずに済む。
まとめ
要点は二つ。DBの隣にIDEを置いて生パスワードを配らず、SSO・RBAC・監査ログで統制する。AIはローカルモデルを選べばデータを外に出さない。加えて、マルチエンジン対応で学んだのは、共通化できないものを共通化したふりをしないことだった。表示できない数字は出さない、できない操作は明示する。派手ではないが、運用と監査で効く部分だと思っている。
ここで書いた挙動は実装に依存する部分が多い。細かいところはリポジトリ(github.com/libredb/libredb-studio)を見てもらうのが早いと思う。手元で動かして確かめたければ、デモ環境(app.libredb.org)も置いてある。