Organization Policies at Scale: Attach High, Pick Managed Constraints, Roll Out in Dry Run

This post is for cloud engineers building a GCP foundation with Terraform who want Organization Policy to keep working once the hierarchy grows past a few projects. My argument is that org policy at scale comes down to three decisions. Attach constraints as high in the hierarchy as you can. Handle exceptions with conditions, not scattered overrides. Once other teams own workloads, never enforce a new constraint until it has run in dry-run mode. By the end you should know where each policy belongs, which kind of constraint to reach for, and how to promote one from observed to enforced.

What org policy actually controls

Google’s Organization Policy overview (as of November 2026) draws the line clearly. IAM decides who can act on a resource. Organization Policy decides what configurations are allowed. Each organization policy configures exactly one constraint. You set it on an organization, folder or project, and every descendant inherits it.

For a Terraform shop, the most important property is the one Google highlighted in a September 26, 2022 post on the Organization Policy Service. Org policies are enforced no matter where a request comes from: infrastructure-as-code, Google Cloud services calling each other, or a user in the console. A module-level validation covers only the Terraform path. The org policy covers all of them. That is why I treat module checks as a convenience for developers and org policy as the actual control.

Attach high, carve exceptions with conditions

Inheritance makes this manageable. A policy on the organization node applies to everything beneath it, so one file describes the rule for the whole estate. A policy per project turns into sprawl nobody can read.

Exceptions are where this usually goes wrong. The overview describes how a single policy can hold several rules. One rule can enforce the constraint only on resources tagged environment=development, and another rule can leave it unenforced elsewhere. I’d use that pattern before I’d create a child-level override. A tag-conditioned rule keeps the exception in the same policy as the rule it bends, so a reviewer sees both at once.

The advice flips when a whole branch of the hierarchy has genuinely different requirements. Restricting the physical location of new resources is one of the listed use cases, and data residency is the obvious example. If one folder holds workloads that must stay in a region and the rest don’t, set that constraint on the folder. Encoding geography into tags across the estate is worse.

Prefer managed constraints for new work

The overview describes three kinds of constraint. The ones that used to be called predefined constraints are now legacy managed constraints. Managed constraints are designed to replace them, with more flexibility and better visibility from Policy Intelligence tools. Custom constraints are ones your organization defines and owns.

For anything new, I’d pick the managed constraint where one exists. The concrete reason is testing. Managed constraints support simulation in Policy Simulator by default, and any that don’t are flagged with simulation_disabled set to true. The overview’s own example shows iam.managed.disableServiceAccountCreation as the managed equivalent of iam.disableServiceAccountCreation:

1
2
3
4
name: organizations/1234567890123/policies/iam.managed.disableServiceAccountCreation
spec:
  rules:
  - enforce: true

Reach for a custom constraint only when nothing managed covers your rule. Google’s 2022 post describes writing them in Common Expression Language. Because they are “managed by your organization instead of by Google,” you also own keeping them correct as services change. Keep that list short.

Roll out in dry run, then enforce

According to the overview, each policy enforces its constraint in active mode, dry-run mode, or both. The 2022 post described dry run as an audit-only mode for observing behavior “during real operations without putting production workloads at risk.” At scale, you can’t know every workload a new constraint will hit.

The sequence I’d follow:

  1. Simulate, where supported. Run the constraint through Policy Simulator before it touches anything. For constraints flagged simulation_disabled, skip this and lean harder on dry run.
  2. Dry run. Deploy it in dry-run mode at the level where it will eventually live, and watch what it would have blocked.
  3. Enforce. Switch to active mode once the would-be violations are fixed or covered by a deliberate, tagged exception.

When enforcement does block something, the 2022 post notes that the requester gets an error code naming the violated policy, and administrators get logs. Make sure your teams know where to look for both before you flip the first constraint to active.

In Terraform, I’d keep each policy’s mode as an explicit, reviewed value in the foundation repo. Promoting from dry run to active then becomes a small, visible change in a pull request, not a console click.

When this is more than you need

If you have a handful of projects, one team and no folder structure yet, staged rollout is mostly ceremony. Set a short list of constraints at the organization and enforce them directly. Dry run first starts paying off once other teams own workloads you can’t inspect yourself, and from that point it should be the rule, not a preference.

If you are laying a foundation now, decide today which constraints live at the organization node and which belong to folders. Write those decisions into Terraform. As soon as a second team deploys into the estate, make dry run the required first state for every new constraint.