Google Cloud のフォルダ Log Sink で特定プロジェクトだけログを二重保管してみた

Google Cloud のフォルダ Log Sink で特定プロジェクトだけログを二重保管してみた

2026.04.30

Google Cloud のフォルダ / 組織レベル Log Sink でインターセプトを有効化すると、対象プロジェクトの _Default バケットからはログが消え、集約先プロジェクトにだけ残ります。SIEM 連携や監査証跡の一元化を進めたい組織では、これが基本形になります。

ただ運用上、「特定のプロジェクトだけは自プロジェクトの _Default にもログを残したい」というケースが出てきます。今回はこのパターンを exclusion filter と非インターセプト集約シンクの組み合わせで実装し、実機で動作を確認しました。設計と Terraform コード、検証結果をまとめます。

なお、実機検証はインターセプトの挙動を直接観察できる独自ログ(gcloud logging write で書ける任意の logName)で行います。Admin Activity Audit Log は Required ログ扱いで intercept が適用されないため、検証用途には向きません(詳細は「ハマりどころ」で後述)。

前提

  • 組織またはフォルダレベルで Log Sink を管理できる権限(roles/logging.admin
  • Terraform hashicorp/google プロバイダ v6 以降(intercept_children 対応版)
  • 同一組織配下に複数プロジェクトが存在する環境

_Required / _Default バケット、集約シンク、インクルージョン / エクスクルージョンフィルタの基本知識は前提とします。

背景: なぜ個別保管対象プロジェクトだけ二重保管したいのか

組織全体としては、ログを集約先プロジェクトに一元化したい状況を想定します。SIEM への横断連携や Security Command Center での横断分析、監査証跡の一元保管などが目的です。

一方で、一部のプロジェクトは管理者が別のチームで、そのチームが自プロジェクト内でもログを閲覧できる状態を求めるケースがあります。コンプライアンス要件で「自プロジェクト内にもログを必ず残すこと」と決まっている場合もあります。

ここでインターセプトを全プロジェクト一律に有効化すると、個別保管対象プロジェクトの _Default からログが消えてしまいます。逆にインターセプトをやめると、全プロジェクトの _Default にログが残ってストレージコストや運用負荷が増えます。

そこで、「組織のインターセプトは有効化しつつ、個別保管対象プロジェクトだけ exclusion で除外し、別途非インターセプト集約シンクでその個別保管対象プロジェクトを拾う」という構成を取りました。

2本のシンクで二重保管する構成

前提となる Cloud Logging の仕様

設計の根拠になる挙動を整理します。

  • インターセプトシンク: マッチしたログを集約先にルーティングし、ソースプロジェクトの _Default には送らない(_Required には残る)
  • exclusion filter にマッチしたログ: そのシンクから除外され、ソースプロジェクト側のシンクに通常どおり流れる
  • 非インターセプト集約シンク: マッチしたログを集約先にルーティングするが、ソースプロジェクト側のシンクも並行して動作する
  • 制約: インターセプトシンクの宛先はプロジェクトのみで、ログバケットを直接指定できない

参考ドキュメント:

構成図

シンクの役割は次の通りです。

シンク 種類 対象
1本目 インターセプト集約シンク フォルダ配下全体。PROJECT_A は exclusion で除外
2本目 非インターセプト集約シンク PROJECT_A のみ

ログの流れ

ソース 経路 最終格納先
PROJECT_A(個別保管対象) 1本目で除外 + 2本目で拾う PROJECT_A の _Default と集約先の両方
PROJECT_B(通常) 1本目でインターセプト 集約先のみ

Terraform 実装

google_logging_folder_sink を2本作るだけのシンプルな構成です。

main.tf
# 1本目: インターセプトシンク(PROJECT_A は exclusion で除外)
resource "google_logging_folder_sink" "intercept" {
  name        = "org-intercept-sink"
  folder      = var.folder_id
  destination = "logging.googleapis.com/projects/${var.dest_project_id}"

  include_children   = true
  intercept_children = true

  filter = "logName:\"test-verify\""

  exclusions {
    name   = "exclude-project-a"
    filter = "logName:\"projects/${var.project_a_id}/\""
  }
}

# 2本目: 非インターセプトシンク(PROJECT_A のみ対象)
resource "google_logging_folder_sink" "project_a_dual" {
  name        = "project-a-dual-sink"
  folder      = var.folder_id
  destination = "logging.googleapis.com/projects/${var.dest_project_id}"

  include_children = true
  # intercept_children は指定しない(= 非インターセプト)

  filter = "logName:\"projects/${var.project_a_id}/logs/test-verify\""
}

# Sink writer SA への IAM 付与
resource "google_project_iam_member" "intercept_writer" {
  project = var.dest_project_id
  role    = "roles/logging.logWriter"
  member  = google_logging_folder_sink.intercept.writer_identity
}

resource "google_project_iam_member" "project_a_dual_writer" {
  project = var.dest_project_id
  role    = "roles/logging.logWriter"
  member  = google_logging_folder_sink.project_a_dual.writer_identity
}

ポイントは以下です。

  • 1本目に intercept_children = true を付け、フォルダ配下のログを集約先に奪う
  • 1本目の exclusions で個別保管対象プロジェクトのログを除外する
  • 2本目は intercept_children を指定せず、個別保管対象プロジェクト分だけ別経路で集約先に流す
  • 2本のシンクそれぞれの writer SA に roles/logging.logWriter を付与する

集約先のバケットを _Default 以外(保持期間延長や CMEK 適用済みのバケットなど)に変えたい場合は、集約先プロジェクト内で google_logging_project_sink を別途作って _Default を上書きする2段階構成にします。インターセプトシンクの宛先がプロジェクト固定で、ログバケットを直接指定できないためです。

ログルーター_–_ロギング_–_masaki-lab_–_Google_Cloud_コンソール.png

ログルーター_–_ロギング_–_masaki-lab_–_Google_Cloud_コンソール.png

ログルーター_–_ロギング_–_masaki-lab_–_Google_Cloud_コンソール.png

やってみた

検証環境

同一フォルダ配下に3プロジェクトを用意しました。役割と環境変数のセットを以下のようにしておきます(以降のコマンドで使い回します)。

export PROJECT_A=your-project-a   # 除外対象(個別保管対象プロジェクト相当)
export PROJECT_B=your-project-b   # インターセプト対象(通常プロジェクト相当)
export DEST=your-dest-project     # 集約先

logName:"test-verify" で絞れる検証用ログを各プロジェクトから書き込みます。gcloud logging write で書いた独自ログは Admin Activity Audit Log と違って _Default バケットに格納されるので、今回のフィルタおよび検証観点に合います。

# シンク作成から3分以上待ってから実行(伝搬遅延対策)
gcloud logging write test-verify "from PROJECT_A" --project=$PROJECT_A
gcloud logging write test-verify "from PROJECT_B" --project=$PROJECT_B

各プロジェクトの _Default バケットを直接読みに行って確認します。

for p in $PROJECT_A $PROJECT_B $DEST; do
  echo "=== $p ==="
  gcloud logging read 'logName:"test-verify"' \
    --project=$p \
    --bucket=_Default --location=global --view=_AllLogs \
    --freshness=10m --limit=5 \
    --format='value(logName,textPayload,timestamp)'
done
実行結果
=== your-project-a ===
projects/your-project-a/logs/test-verify    from PROJECT_A  2026-04-30T02:25:33.817597161Z
=== your-project-b ===
=== your-dest-project ===
projects/your-project-b/logs/test-verify  from PROJECT_B  2026-04-30T02:25:34.355154046Z
projects/your-project-a/logs/test-verify    from PROJECT_A  2026-04-30T02:25:33.817597161Z

結果

バケット 期待値 実測
PROJECT_A _Default PROJECT_A 自身のログあり あり
PROJECT_B _Default
DEST _Default PROJECT_A と PROJECT_B の両方 両方あり

期待どおりに動作しました。個別保管対象プロジェクトだけ二重保管できています。

ハマりどころ

集約先プロジェクトに請求アカウントが紐付いていないと silent に失敗する

集約先プロジェクトに請求アカウントが紐付いていないと、シンク作成・IAM 付与は成功するのに DEST のバケットには何も届かない、という事象に遭遇しました。エラー出力もないため、紐付け忘れに気づくのが難しいです。新規作成した集約先プロジェクトを使うときは最初に確認しておくのがおすすめです。

# 集約先プロジェクトの billing 状態を確認
gcloud billing projects describe <DEST_PROJECT_ID>

# 紐付いていなければリンク
gcloud billing projects link <DEST_PROJECT_ID> \
  --billing-account=<BILLING_ACCOUNT_ID>

Audit Log の経路でこのパターンを検証しようとしない

cloudaudit ベースで検証しようとすると上手くいきません。インターセプトシンクの仕様として Required ログ(Admin Activity Audit Log など)には intercept が適用されない ためです。logName:"cloudaudit.googleapis.com" でフィルタを組むと、PROJECT_B の _Required には自プロジェクトの audit log が残り続けるため、「インターセプトで奪う」効果を観測できません。

検証は gcloud logging write で書ける独自ログ名を使うのが確実です。

gcloud logging read で他プロジェクト由来のログが見えない

集約先プロジェクトで gcloud logging read を実行しても、ルーティングされてきた他プロジェクトのログが出てこないことがあります。デフォルトのログスコープが現行プロジェクトに絞られているためです。

--bucket=_Default --location=global --view=_AllLogs を明示してバケットを直接読むと出てきます。

おわりに

intercept_children = true と exclusion filter、非インターセプト集約シンクの組み合わせで、個別保管対象プロジェクトだけログを二重保管できました。google_logging_folder_sink を2本書くだけで済むので、構成自体はシンプルです。

この記事をシェアする

関連記事