はじめに
弊社では BI ツールとして Redash を運用しています。操作内容の監査を考える場合、BI ツールという性格上、誰がいつどのデータソースに対して何を実行したかを後から追える状態を保つことは、監査の観点で外せない要件になっています。
これに対し、営業時間外(平日深夜早朝・土日終日)に Redash 上で操作の形跡があれば、その操作者本人に Slack 上で利用目的の回答を促す、という監査運用を敷いてきました。検知した操作を放置せず、必ず本人に説明責任を返す、という運用思想を名前にした格好です。なお、操作者本人まで辿れるのは、Redash へのログインを IAM Identity Center を IdP とする SSO に寄せているためです(後述)。
監査対象者が Redash 上でどういった操作をしたか追跡したいケースにおいて、Redash の実装上の都合により、追跡の材料にログだけを用いると
- いつどういったクエリが実行されたか
- そのクエリはどのデータソースを対象として実行されたか
といった事項が難しいという課題があります。
本稿は、ログに乗ってくる情報が弱かった Redash の操作ログ監査を、Redash 内部の DB を引っ張り出すことでなんとか実用に堪える水準まで引き上げた話になります。
実態調査
監査通知の仕組みは動いていますが、これは営業時間外の検知に特化した運用です。平日日勤帯も含めた全期間で日常的にどんな操作が行われているのか、その全体像については、一度じっくり把握しておきたいところでした。
そこで既存の Athena 基盤を用いて、半年分の操作を全数で棚卸ししてみました。Redash の操作ログを Athena で追えるようにした整備そのものについては、以下拙稿を参照ください。
ユーザ別・操作種別・時間帯・接続データソース別など、思いつく限りの切り口で集計してみたのですが、ここで一つ無視できない事実に行き当たりました。
人手による Redash のクエリ実行のうち、その大半が「保存済みクエリ」ではなく、その場限りの adhoc 実行だったのです。Redash では保存済みクエリを開いて実行すると個別のクエリ ID が記録されますが、エディタにその場で SQL を打ち込んで流す adhoc 実行は、すべて query_id が adhoc という固定値で記録されます。
つまり、日常の操作のほとんどが、保存済みクエリではなく、その場限りの adhoc 実行で占められていたわけです。冒頭で触れた「どんなクエリを、どのデータソースに対して流したか」を追いにくいのは、まさにこの adhoc 実行でした。日常の操作の主役がここである以上、その中身まできちんと追える状態にしておく意義は大きい、ということになります。
課題感
では、なぜサーバログだけでは adhoc 実行の中身まで取れないのか、既存の監査ログの作りから振り返ります。Redash のサーバログは CloudWatch Logs 経由で S3 に外出ししてあり、Athena から横断的にクエリできるよう整備済みでした(この収集パイプラインをどう組んだかは前掲記事で扱っています)。ただしこのログから操作内容を抽出する view は、ログ行のうちジョブ投入を示す行を正規表現で拾い上げるだけの作りになっていました。
この方式で取れるのは、おおむね次の情報に限られます。
- いつ実行されたか(実行時刻)
- 誰が実行したか(実行ユーザ)
- どのクエリ ID か(
adhoc実行の場合はすべてadhoc)
裏を返すと、
- 実際に流した SQL 本文
- どの データソース(=どのテナント DB)に対して実行したか
が、まるごと欠落していました。営業時間外に adhoc 実行が検知されても、通知を受け取る側が分かるのは「Redash で何か操作した」という事実までで、「どのテナント DB に対してどんな SQL を流したか」という、監査でまさに知りたい肝心の中身に踏み込めなかったのです。
検知して本人に回答を強制するところまでは出来ているのに、回答を突き合わせる材料が手元に無い。この中身を補うことが、今回の出発点になりました。
改善
やるべきことがわかりました。改善をやっていきます。具体的には以下のような取り組みをおこないました。
- Redash 内部 DB から events を吸い出して Athena に載せる
- 通知本文に操作内容の要約を載せる
1. Redash 内部 DB から events を吸い出して Athena に載せる
前述のとおり、SQL の本文とデータソースはサーバログには出ず、Redash 自身が内部状態の管理に用いる Postgres、その events テーブルにのみ記録されます。adhoc 実行であっても、流された SQL の本文も接続データソースの ID も、この内部 DB の中にはきちんと残っています。外向きのログが弱いぶんは、この内部 DB から補えばよい、という理屈になります。
とはいえ、本番で稼働している Redash の Postgres へ監査の都合で直接コネクションを張りにゆくのは避けたいところでした。そこで、Redash 自身が events を読み出す API を備えている点を活かし、方針は次のとおりとしました。
eventsテーブルを 日次で Redash の API 経由で取得する- 取得結果を、既存の監査ログ集約用 S3 バケットに置く
- Athena 側から Glue table として読み、監査用の view に合流させる
events 参照専用のユーザを Redash に用意し、その API キー経由で events を吸い出します。実際に投げているのはおおよそ次のようなクエリで、execute_query(クエリ実行)のイベントに絞り、誰が・いつ・どのデータソース(data_source_id)に対して・何を実行したか(details に SQL 本文が入る)を取り出します。
SELECT e.id, e.created_at, u.email, e.action, e.object_type, e.object_id AS data_source_id, e.additional_properties::text AS details FROM events e LEFT JOIN users u ON u.id = e.user_id WHERE e.created_at >= (current_date - interval '2 days') AND e.action = 'execute_query' ORDER BY e.created_at
取りこぼしを避けるため当日分に前日分の余裕を持たせて取得し、重複は後段の view で排除しています。取得結果は日付別のプレフィックスを切って NDJSON で S3 に PUT します。SQL 本文には改行やカンマが含まれうるため、CSV ではなく 1 行 1 レコードの NDJSON を選んでいます。
S3 側は日付(年/月/日)でパスを切ってあるので、Athena 側は partition projection でパーティションを自動認識させ、クローラを別途回す必要はない格好にしてあります。Glue table の設定はこうです。
parameters = { "projection.enabled" = "true" "projection.dt.type" = "date" "projection.dt.format" = "yyyy/MM/dd" "projection.dt.range" = "2026/05/01,NOW" "projection.dt.interval" = "1" "projection.dt.interval.unit" = "DAYS" "storage.location.template" = "s3://<監査ログ用バケット>/events/$${dt}/" }
dt パーティションを日付として NOW まで射影しておけば、新しい日付のオブジェクトが増えても定義変更もクローラ実行も要りません。NDJSON は JsonSerDe で読み込みます。
その上で、既存の監査用 view に対して、adhoc 実行の行へ events 由来の SQL 本文とデータソース情報を LEFT JOIN で補完しました。これにより、従来は「adhoc を実行した」としか言えなかった行に、「どのデータソースに対して、どんな SQL を流したか」という中身が紐付くようになりました。
全体の構成図は以下のとおりです(通知への Bedrock 要約は「改善その2」で後述します)。

考慮箇所
実装上ひとつ難儀したのが、この日次バッチの Lambda から Redash の API を叩く経路でした。前提として Redash は ALB の背後に立っており、その手前で接続元 IP を許可リストで絞ることで、限られたネットワークからのみ到達できるようにしてあります(EC2 + ALB でどう立てているかは前掲の拙稿でも触れています)。Lambda は VPC 内で動かしているため、その出口となる NAT Gateway の EIP をこの許可リストに足さないと、API までたどり着けません。
ここで安易に既存の社内 CIDR 変数へ EIP を混ぜ込むと、その変数はセキュリティグループなどで広範に再利用されているため、意図しない範囲まで許可が広がってしまいます。そこで NAT Gateway の EIP を /32 のリストとして locals で組み立て、Redash のリスナルール専用に連結する、という形に切り分けました。
locals { # events export Lambda は VPC 内から NAT GW 経由で外向きに出る # その NAT GW EIP を Redash ALB リスナルールの source_ip allowlist に含めて API 到達性を確保する # 既存の社内 CIDR 変数には混ぜない (SG 等で広範に再利用されており副作用が大きいため) redash_listener_rule_source_cidrs = concat( var.allowed_cidrs, [for ip in module.vpc.nat_public_ips : "${ip}/32"], ) }
もうひとつ細かいハマりどころがあります。ALB のリスナルールは 1 ルールにつき条件値を 5 個までしか持てず、Host ヘッダの条件で 1 つ消費しているため、IP の条件値は 4 個が上限になります。許可したい IP がそれを超えるので、chunklist で 4 個ずつに割り、ルールを複数に分けて回避しています。
resource "aws_lb_listener_rule" "redash" { count = ceil(length(local.redash_listener_rule_source_cidrs) / 4) listener_arn = aws_lb_listener.admin.arn action { type = "forward" target_group_arn = aws_lb_target_group.redash.arn } condition { source_ip { values = chunklist(local.redash_listener_rule_source_cidrs, 4)[count.index] } } condition { host_header { values = [aws_route53_record.redash_external.fqdn] } } }
監査の仕組みを足したことで別の口がうっかり広がる、というのは本末転倒につき、ここは経路を限定して取り扱っています。
2. 通知本文に操作内容の要約を載せる
中身が Athena 側で取れるようになったとはいえ、それだけでは監査担当が S3 上のデータを手で開いて目を通さないと内容を把握できません。せっかく SQL 本文が手に入っても、通知を見た人が毎回データを開きに行くのでは運用として回りません。
そこで、本人へ送る Slack 通知の本文そのものに、実行内容の要約を載せることにしました。要約には Amazon Bedrock を使い、以下のような観点でまとめさせています。
- どのデータソースに対して何件の操作があったか
- SQL の動詞分布(SELECT / UPDATE / DELETE などの内訳)
- 連続して実行された SQL のおおまかな流れ
従来の通知本文は、実のところ時間範囲を伝えるだけのものでした。
2026-06-20 22:00〜23:30 (JST) に Redash での操作が検知されました。 営業時間外の操作のため、利用目的の確認をお願いします。
これが、要約を載せたことで次のような格好になります(以下は架空の例です)。
2026-06-20 22:00〜23:30 (JST) に Redash での操作が検知されました。 営業時間外の操作のため、利用目的の確認をお願いします。 【操作内容の要約】 - データソース A に対して SELECT を中心に 12 件 - 特定レコードの状態を確認するための参照系クエリが主体 - 更新系(UPDATE / DELETE)の実行は検知されていない
この要約は、events から組み立てたクエリ実行の一覧を Amazon Bedrock に渡し、次のようなプロンプトで生成しています(抜粋)。details に入っている SQL 本文から動詞と対象テーブルを読み取らせ、更新系は埋もれないよう個別に列挙させる、といった監査向けの要件をルールとして与えています。
## 出力フォーマット Slack の mrkdwn 記法を使用してください。 🗓 *最終検知日時:* YYYY-MM-DD HH:MM (JST) 💡 *操作概要:* (SQL の動詞 (SELECT / INSERT / UPDATE / DELETE 等) の分布、頻繁にアクセスされたテーブル、目的推測などを 2〜3 行で要約してください) 📋 *検知された操作:* ・クエリ説明 (HH:MM) ... ## ルール - eventtime は UTC なので JST (+9 時間) に変換してください - 各クエリは details.query から SQL の動詞と主要なテーブル名を読み取り、「テーブル名から〜を取得」のような日本語の操作説明に変換してください - adhoc 実行の場合は SQL の概要 (動詞 + 対象テーブル) を、保存クエリの場合は「保存クエリ #<query_id> の実行」を主としてください - UPDATE / DELETE 等のデータ更新系クエリは個別に列挙してください (営業時間外の更新は要注意のため) - 最大 20 件まで表示し、それ以上は「…他 N 件」としてください
実は、手動オペレーションや S3 イベント経由の監査通知では、すでに Bedrock 要約を載せた通知に切り替え済みでした。こうした操作を検知して本人に背景を問い合わせる監査運用、そしてその通知に Bedrock 要約を載せる仕組みそのものの成り立ちについては、弊社ブログに詳しい記事があります。
今回 Redash のクエリ実行経路も同じパターンに揃えた、という位置付けです。これにより、通知を受けた本人もレビューする側も、S3 のデータを開きに行くことなく「何が起きたか」の概要を Slack 上で把握できるようになりました。
なお、ログイン検知のような詳細データを持たない経路は、従来どおり時間範囲だけの通知で十分につき、Bedrock 要約の対象には含めていません。要約する中身を持つ経路にのみ要約を載せる、という切り分けです。
効果
一連の対応を経て、監査の解像度は次のように変わりました。
- これまで「
adhocを実行した」としか言えなかった操作に、SQL 本文とデータソースが紐付くようになった - 監査担当が S3 のデータを開きに行かずとも、Slack の通知本文だけで「どのデータソースに、どんな種類のクエリが流れたか」の概要を把握できるようになった
- 検知 → 本人への回答強制、という従来の運用に、回答を突き合わせるための材料が揃った
監査ログは、取得を始めて直ちに何らか嬉しさが生じるようなものではありませんが、いざインシデント対応や定期監査で「あの操作は何だったのか」を遡る局面でこそ効いてくる、将来への投資の類だと考えています。
おわりに
今回の対応は、半年分を一度きちんと棚卸ししてみたことが起点でした。日常の操作の大半が、ログだけでは中身を追いにくい adhoc 実行で占められていると数字で分かったことで、内部 DB を覗いてでも中身を補いにゆく方針が固まった格好です。調査が改善を呼ぶ、という順序を地で行く一連となりました。
Redash の操作ログ監査に取り組む向きに、手法の事例のひとつとして参考になれば幸いです。
文責:MNTSQ 株式会社 SRE 秋本
注記:この記事は、構成図を除く文章の8割程度を、文責者の過去記事や社内の関連ドキュメントをもとに Claude Opus 4.8 が執筆しています。
追伸:本稿の執筆・推敲にあたっては、日本語技術文書の規範をまとめた k16shikano 氏の文書 を参照しました。記して感謝します。