メインコンテンツまでスキップ

コラム: 監査ログ、全部DBに入れとけばよくない?

「監査ログってとりあえずDBに書いておけばいいでしょ?」

— たいていのプロジェクトは、この一言から始まる。そして1年後に後悔する。


IDサービスを運用していると、監査ログは静かに、しかし確実に膨れ上がります。認証、トークン発行、API呼び出し。ユーザーが操作するたびにログは増え続けます。最初は数GBだったテーブルが、気づけば数百GB。クエリは遅くなり、バックアップは終わらず、ストレージ代は跳ね上がる。

このコラムでは、「監査ログをどう扱うか」について、記録・検索・保存・コストの4つの観点から考えます。


そもそも監査ログは何が特殊なのか

監査ログが厄介なのは、他のデータと根本的に性質が違うからです。

ユーザーデータは増えても、せいぜいユーザー数分です。セッションやトークンはTTLで自動消滅します。でも監査ログは 「全操作の記録」かつ「消してはいけない」 データです。増える一方で、減ることがない。

しかも、4つの要求が互いに矛盾します。

  「1件も漏らさず記録したい」 ←→ 「認証を遅くしたくない」
「すぐに検索したい」 ←→ 「安く長期保存したい」

全部をDBに入れておけば記録も検索も簡単です。でもDBのストレージ代はS3 Glacierの700倍。7年保存が必要なデータを全部DBに抱えるのは、冷凍すればいい食材を全部冷蔵庫に入れているようなものです。


記録: 同期か、非同期か

最初に直面する判断が「監査ログを同期で書くか、非同期で書くか」です。

同期書き込みは単純です。認証処理のトランザクション内でINSERTするだけ。確実だし、実装も簡単。ただし、DBが遅くなると認証全体が道連れになります。

認証リクエスト → 認証処理 → 監査ログINSERT → レスポンス

ここが遅いと全部遅い

非同期書き込み(SQS経由) にすれば、認証のレイテンシからは解放されます。でも代わりに、コンシューマーの実装、DLQ(デッドレターキュー)の監視、リトライ設計が必要になります。しかもSQSへの送信自体が失敗すればログは欠損します。非同期にしたからといって、欠損リスクがゼロになるわけではありません。

結論: 小規模のうちは同期でいい。 テーブル設計がまともなら(後述のインデックスとパーティション参照)、数千万行までは認証レイテンシへの影響は最小限です。「ピーク時にDBの書き込みタイムアウトが出始めた」が非同期に切り替える合図です。


検索: 「いつのデータを探すか」で手段が変わる

監査ログに対する検索は3パターンあります。

パターンどれくらい速くほしいか
運用調査「ユーザーAの直近1時間のログイン試行」秒以内
インシデント対応「このIPからの全アクセスを過去90日分」数秒以内
コンプライアンス監査「2023年度のテナントXの全認証イベント」分〜時間でも可

この3つを1つのデータストアで全部カバーしようとすると破綻します。直近データの検索を速くするためにインデックスを追加すれば書き込みが遅くなるし、7年分のデータにインデックスを張ればストレージが膨れ上がる。

解決策は「時間で分ける」ことです。直近のデータは高速に検索できる場所に、古いデータは安い場所に置いて、それぞれに合った方法で検索する。当たり前のことですが、意外とこれをやらずに「全部DBに入れてGROUP BYが遅い」と嘆くプロジェクトは多いです。

Aurora上の直近データについては、検索パターンに合わせたインデックスを3本程度に絞るのが現実的です。監査ログは書き込み中心なので、インデックスを増やしすぎると書き込み性能を食います。

-- この3本で運用調査とインシデント対応をカバーする
CREATE INDEX idx_tenant_user_time ON audit_logs (tenant_id, user_id, created_at DESC);
CREATE INDEX idx_tenant_event_time ON audit_logs (tenant_id, event_type, created_at DESC);
CREATE INDEX idx_ip_time ON audit_logs (ip_address, created_at DESC);

保存: 3層に分ける

ここが監査ログ設計の核心です。データの鮮度で3つの層に分離します。

  書き込み


┌──────────┐ 90日後 ┌──────────┐ 1年後 ┌──────────┐ 7年後
│ Aurora │ ──────→ │ S3 │ ─────→ │ Glacier │ ──→ 自動削除
│ (ホット) │ │(ウォーム) │ │(コールド) │
│ │ │ │ │ │
│ SQL検索 │ │ Athena │ │復元→Athena│
│ ミリ秒 │ │ 秒〜分 │ │ 分〜時間 │
└──────────┘ └──────────┘ └──────────┘

ホット層(Aurora、直近90日): 月次パーティションとインデックスで高速検索。90日を過ぎたらパーティションDROPで除去します。ここがポイントで、DELETE WHERE created_at < ... はテーブルスキャン+VACUUMが必要ですが、DROP TABLE audit_logs_2024_01 はメタデータ削除のみ。データ量が大きいほど差が出ます。

ウォーム層(S3 Standard、90日〜1年): AuroraからエクスポートしたデータをParquet形式で保存します。JSONのまま置くと、Athenaのスキャン量(=料金)が10〜20倍になります。Parquetは必要なカラムだけ読むので、SELECT event_type, created_at FROM ... のようなクエリで効果が絶大です。

コールド層(S3 Glacier、1年〜7年): S3ライフサイクルポリシーで自動遷移。Object Lock(Complianceモード)で改ざん防止。年に1回あるかないかの監査対応用です。

移行はEventBridge+Step Functionsで毎晩自動実行。Aurora→S3エクスポート→Athenaパーティション更新→AuroraパーティションDROPの4ステップです。一度作れば放置できます。


コスト: 数字で見ると一目瞭然

「3層に分けるのは面倒だけど、本当に効果あるの?」 — 数字で見てみます。

前提: 1ユーザーあたり1日10イベント、1イベント1KB(JSON)/ 0.1KB(Parquet)

規模3層設計Aurora全保持
1万ユーザー〜$0/月$3/月ほぼ変わらない
10万ユーザー$15/月$90/月6倍
100万ユーザー$123/月$900/月7倍以上

1万ユーザーでは差はわずかです。だから最初は「全部DBでいい」は正しい。でも10万ユーザーを超えたあたりから差が開き始め、100万ユーザーでは年間$9,000以上の差になります。

しかもこれはストレージ代だけの話です。Aurora上に数百GBのテーブルを抱えると、クエリが遅くなる、バックアップに時間がかかる、リストアのRTOを満たせない、といった問題がコストとは別に発生します。

100万ユーザーの監査ログ(7年分、全部Aurora):

Aurora上のデータ量: 約 2.5TB
バックアップ所要時間: 数時間
リストア所要時間: 数時間
90日前のログ検索: 秒〜分(パーティションあれば)
3年前のログ検索: 分〜タイムアウト(使い物にならない)

100万ユーザーの監査ログ(3層設計):

Aurora上のデータ量: 約 90GB(直近90日のみ)
バックアップ所要時間: 数十分
リストア所要時間: 数十分
90日前のログ検索: ミリ秒〜秒
3年前のログ検索: 秒〜分(Athena)

別の選択肢: 最初からS3に書いたらどうなる?

ここまで「Aurora → S3 → Glacier」の3層設計を説明してきましたが、「そもそもAuroraを経由せず、最初からS3に書けばもっとシンプルで安いんじゃないか?」という疑問は自然です。

Kinesis Data Firehoseを使えば、アプリからのログをバッファリングしてS3にParquet形式で直接書き出せます。Auroraは完全にスキップ。

S3直接書き込みアーキテクチャ:

アプリ ──→ Kinesis Firehose ──→ S3 (Parquet)
バッファ: 60秒 or 1MB │
Athena で検索

コストは確かに安い。 Aurora不要なので、10万ユーザーで月$15だった監査ログ関連コストが$5以下になります。

でもこのアーキテクチャには明確なトレードオフがあります。

検索のレイテンシが秒〜分になる。 「さっき失敗したログインを調べたい」という運用調査で、Auroraならミリ秒で返るクエリがAthenaでは数秒〜十数秒かかります。1日に何十回も調査する運用者にとって、この差はストレスになります。

書き込みからの反映ラグがある。 Firehoseはバッファリングする(デフォルト60秒〜5分)ので、「今起きたイベント」がすぐには検索できません。インシデント対応の初動で「直近5分のログが見えない」のは地味に痛い。

Athenaはクエリごとに課金される。 1回あたりは安くても($5/TBスキャン)、運用者が1日50回クエリを打つ文化だとそれなりに積み上がります。Auroraなら何回SELECTしてもインスタンス料金内です。

                    Aurora + S3 3層       S3直接(Athena)
────────────── ─────────────
直近ログの検索速度 ミリ秒〜秒 数秒〜十数秒
書き込み反映ラグ なし(同期) 60秒〜5分
1日50回の調査 追加コストなし $0.5〜$5/日
月額コスト(10万user) 約$15 約$5
運用者の体感 快適 待ちが多い

結論: S3直接書き込みが向くのは「日常の運用調査がほとんどない」ケースです。 たとえばコンプライアンス用の保存が主目的で、普段のトラブルシュートはアプリケーションログ(CloudWatch Logs)で事足りるなら、Auroraを経由する必要はありません。

一方、IDサービスのように「認証失敗の原因調査」「不審なアクセスの追跡」を日常的に行うシステムでは、直近データの高速検索はほぼ必須です。ここをケチると運用品質が落ちます。

つまり 「検索のレイテンシに月$10払う価値があるか?」 が判断基準です。IDサービスの運用ではたいてい「ある」。


やりがちな失敗

最後に、監査ログで「あるある」な失敗を並べておきます。

「とりあえず全部DBに入れとく」 — 小規模のうちは正解。でも「いつ3層に切り替えるか」を決めずに放置すると、気づいたときにはテーブルが巨大化して移行が大工事になります。Phase 2の設計時に移行計画を立てておくのが現実的。

「JSONのままS3に置いてAthenaで検索」 — 動くには動きます。でも1ヶ月分の検索で10GBスキャンされて$0.05。Parquetにしておけば0.5GBの$0.0025。ケチなようですが、月100回クエリを打つ運用者がいると年間で差が出ます。

「パーティションを切らない」 — テーブルが小さいうちは不要ですが、数千万行を超えるとフルスキャンが秒単位になります。後からパーティションを追加するのは大仕事(テーブル再作成が必要)なので、Phase 1の時点で月次パーティションを入れておくのが安全策。

「DLQを作ったけどアラームを設定していない」 — 非同期書き込みのDLQにメッセージが溜まっても、誰も気づかなければログ欠損と同じ。DLQのメッセージ数にCloudWatchアラームを仕掛けるのは必須です。

「Object Lockを設定していない」 — 監査ログの要件は「改ざんされていないこと」の証明です。S3に置いただけでは、IAM権限があれば誰でも削除・変更できます。ComplianceモードのObject Lockにして初めて「rootでも消せない」が保証されます。


まとめ

監査ログの設計は、突き詰めると 「時間でデータを分ける」 というシンプルな原則に行き着きます。

  • 直近90日はAuroraで高速に検索できるようにする
  • 90日を超えたらS3にParquetで移してAthenaで検索する
  • 1年を超えたらGlacierに沈めて7年後に消す

これだけです。でも「これだけ」をやるかやらないかで、10万ユーザーを超えたあたりからコストが6倍、100万ユーザーで7倍以上の差になる。しかもクエリ性能やバックアップ時間まで改善される。

最初から3層設計を組む必要はありません。1万ユーザーのうちは全部Auroraで十分です。でも 「いつ切り替えるか」のトリガーだけは決めておく。監査ログテーブルのサイズが50GBを超えた、クエリのp99が1秒を超えた — そのサインが出たら移行を始める。それが現実的な監査ログ設計です。


関連ドキュメント