Google Cloud のフォルダ Log Sink で特定プロジェクトだけログを二重保管してみた
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本作るだけのシンプルな構成です。
# 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段階構成にします。インターセプトシンクの宛先がプロジェクト固定で、ログバケットを直接指定できないためです。



やってみた
検証環境
同一フォルダ配下に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本書くだけで済むので、構成自体はシンプルです。







