From 60ec6db9cdaa07c761c3879fa3f28e3fea5ac870 Mon Sep 17 00:00:00 2001 From: Martin Bergo Date: Wed, 18 Feb 2026 16:08:23 +0100 Subject: [PATCH] docs(organization): document external IAM management for logging sinks at scale (#3746) * docs(organization): document external IAM management for logging sinks at scale * Update TOC --------- Co-authored-by: Julio Castillo --- modules/organization/README.md | 63 +++++++++++++++++++ .../examples/logging-iam-external.yaml | 47 ++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 tests/modules/organization/examples/logging-iam-external.yaml diff --git a/modules/organization/README.md b/modules/organization/README.md index 7322d000d..c54248f32 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -26,6 +26,7 @@ To manage organization policies, the `orgpolicy.googleapis.com` service should b - [Privileged Access Manager (PAM) Entitlements Factory](#privileged-access-manager-pam-entitlements-factory) - [Hierarchical Firewall Policy Attachments](#hierarchical-firewall-policy-attachments) - [Log Sinks](#log-sinks) + - [Externally Managing IAM for Log Sinks](#externally-managing-iam-for-log-sinks) - [Data Access Logs](#data-access-logs) - [Custom Roles](#custom-roles) - [Custom Roles Factory](#custom-roles-factory) @@ -447,6 +448,68 @@ module "org" { # tftest inventory=logging.yaml ``` +### Externally Managing IAM for Log Sinks + +By default the module creates one conditional IAM binding per sink for `roles/logging.bucketWriter` on the destination project. GCP enforces a hard limit of [20 conditional bindings per role and principal](https://cloud.google.com/iam/docs/conditions-overview#limitations) on a single resource. If you route many sinks to the same destination project, you will hit this limit. + +Set `iam = false` on the affected sinks and manage the IAM binding externally, consolidating multiple destinations into fewer bindings using an OR'd CEL condition expression (max 12 logical operators per condition). + +```hcl +module "log-bucket-0" { + source = "./fabric/modules/logging-bucket" + parent = var.project_id + name = "audit-0" +} + +module "log-bucket-1" { + source = "./fabric/modules/logging-bucket" + parent = var.project_id + name = "audit-1" +} + +module "org" { + source = "./fabric/modules/organization" + organization_id = var.organization_id + logging_sinks = { + audit-0 = { + destination = module.log-bucket-0.id + filter = "severity=NOTICE" + type = "logging" + iam = false + } + audit-1 = { + destination = module.log-bucket-1.id + filter = "severity=WARNING" + type = "logging" + iam = false + } + } +} + +resource "google_project_iam_member" "log-bucket-writer" { + project = var.project_id + role = "roles/logging.bucketWriter" + member = module.org.sink_writer_identities["audit-0"] + condition { + title = "log_bucket_writer" + description = "Grants bucketWriter for audit-0, audit-1." + expression = join(" || ", [ + "resource.name.endsWith('${module.log-bucket-0.id}')", + "resource.name.endsWith('${module.log-bucket-1.id}')", + # add up to 11 more + ]) + } + lifecycle { + create_before_destroy = true + } +} +# tftest inventory=logging-iam-external.yaml +``` + +When you exceed 13 sinks per binding, use Terraform's `chunklist()` with `for_each` to generate multiple `google_project_iam_member` resources automatically. + +For production-scale deployments or strict per-sink isolation, consider using [user-managed service accounts for log routing](https://cloud.google.com/logging/docs/routing/user-managed-service-accounts) instead of the default shared writer identity. This removes the conditional binding limit entirely and provides per-sink auditability. + ## Data Access Logs Activation of data access logs can be controlled via the `logging_data_access` variable. diff --git a/tests/modules/organization/examples/logging-iam-external.yaml b/tests/modules/organization/examples/logging-iam-external.yaml new file mode 100644 index 000000000..5d7fe003e --- /dev/null +++ b/tests/modules/organization/examples/logging-iam-external.yaml @@ -0,0 +1,47 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +values: + module.log-bucket-0.google_logging_project_bucket_config.bucket[0]: + bucket_id: audit-0 + location: global + project: project-id + retention_days: 30 + module.log-bucket-1.google_logging_project_bucket_config.bucket[0]: + bucket_id: audit-1 + location: global + project: project-id + retention_days: 30 + module.org.google_logging_organization_sink.sink["audit-0"]: + filter: severity=NOTICE + include_children: true + name: audit-0 + org_id: '1122334455' + module.org.google_logging_organization_sink.sink["audit-1"]: + filter: severity=WARNING + include_children: true + name: audit-1 + org_id: '1122334455' + google_project_iam_member.log-bucket-writer: + project: project-id + role: roles/logging.bucketWriter + condition: + - title: log_bucket_writer + +counts: + google_logging_organization_sink: 2 + google_logging_project_bucket_config: 2 + google_project_iam_member: 1 + modules: 3 + resources: 5