GCP Landing Zone

GCP Landing Zone: The Foundation Blueprint with Shared VPC and Org Policies

The day your GCP estate stops being one project and becomes ten, a quiet decision gets made for you whether you make it or not: who owns the network, who pays the bill, and which guardrails are non-negotiable. A GCP landing zone — Google’s reference for it is the Foundation Blueprint (delivered as the terraform-google-modules / terraform-example-foundation repos and the Cloud Foundation Toolkit) — is the explicit, version-controlled answer to those questions. It is a resource hierarchy of an organization node, folders and projects; a Shared VPC so one platform team owns subnets, routes and firewalls while product teams deploy into them; a set of organization policy constraints that disable the dangerous defaults; centralized logging and billing; and a project factory — Terraform that mints every new project pre-wired to all of the above. Skip it and you get sprawl: projects on personal billing accounts, a network per project, no central log sink, and a security team that can enforce nothing.

This is the architect’s build guide, not a tour of the console. We treat the landing zone as a contract between platform, security, network and product teams, and we enumerate every clause: the four-level hierarchy and where each control attaches, the folder taxonomy and why you organize by blast radius rather than org chart, the project factory module and the exact gcloud/Terraform that drives it, Shared VPC host-and-service attachment with subnet-level IAM, the org policy constraints that actually matter (list and boolean, with the inheritance and mergePolicy semantics that trip people up), hierarchical firewall policies, the log sink and billing export wiring, IAM groups with separation of duties, and the Assured Workloads overlay for regulated estates. Every control comes with both the imperative gcloud and the declarative Terraform, because in a landing zone the Terraform is the source of truth and the CLI is only for confirming what it did.

By the end you will be able to design a hierarchy that won’t need re-cutting in eighteen months, stand up a project factory that makes a compliant project a pull request rather than a ticket, reason precisely about how an org policy at the org node interacts with one at a folder, and sequence the whole rollout in phases — bootstrap, org, environments, then workloads — so that you are never blocked on a control you forgot to lay down first. Because this is a reference you will return to mid-design, the hierarchy, the constraints, the IAM model and the rollout are all laid out as scannable tables: read the prose once, then keep the tables open while you write the HCL.

What problem this solves

Without a landing zone, GCP adoption follows a predictable decline. The first project is created by an engineer with an Owner role and a personal credit card. The second copies the first. By the tenth, you have ten default VPC networks (each with the wide-open default firewall rules Google ships), ten different IAM models (mostly roles/owner handed to whoever asked), audit logs scattered across projects with no central sink, and a finance team that cannot answer “what did the data platform cost last month” because spend is spread across unlabeled projects on three billing accounts. Nothing is wrong yet — it just cannot be governed, secured, or paid for predictably, and every one of those problems compounds.

What breaks specifically: the security team has no enforcement point — an org policy can disable public IPs on VMs across the whole company in one place, but only if there is an organization node and the policy is set there; without it they are filing per-project tickets forever. The network team cannot enforce a routing or egress posture because each project owns its own VPC. The platform team cannot offer self-service because there is no template — every new project is bespoke, so it is a ticket, a meeting, and a week. And finance cannot allocate cost because projects lack a consistent labeling and folder structure to roll spend up by.

Who hits this: any organization past a handful of GCP projects, and especially anyone in a regulated industry (finance, healthcare, public sector) where “show me your access controls and your audit trail” is an auditor’s opening question, not a nice-to-have. The landing zone is the difference between answering that question with a terraform plan and a single log project, versus a week of screenshots. The fix is never “tighten the projects we have” reactively — it is to lay down the hierarchy, the factory and the guardrails once, early, and route everything through them.

To frame the whole build before the deep dive, here is every layer of the landing zone, the problem it solves, and the single most important thing to get right:

Landing-zone layer What it solves Primary GCP construct The one thing to get right
Resource hierarchy A single root + inheritance for policy and IAM Organization → Folders → Projects An org node exists and folders map to blast radius, not the org chart
Project factory Self-service, compliant project creation Terraform project-factory module + CI/CD Projects are PRs, never click-created; baseline applied every time
Shared VPC One central network, many workload projects Host project + service project attachment Platform owns subnets/firewalls; service projects get subnet-level IAM only
Org policies Disable dangerous defaults company-wide gcloud org-policies / constraints Set the high-value constraints at the org node; test in a sandbox folder first
Hierarchical firewall Central, inheritable network guardrails Firewall policies on org/folder Deny the obvious (0.0.0.0/0:22) high in the hierarchy; allow specifics low
Centralized logging One audit trail across the estate Aggregated log sink → log project An org-level aggregated sink to a locked-down, access-restricted project
Centralized billing Cost visibility and allocation One billing account + BigQuery export All projects on one billing account; export to BigQuery from day one
IAM & SoD Least privilege, separation of duties Groups + roles at folder scope Bind groups (never users) at the highest sensible node; split the privileged roles
Assured Workloads (optional) Regulatory/data-residency controls Assured Workloads folders Only where a compliance regime demands it; it constrains, it doesn’t just label

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You should already understand the GCP basics this builds on: a project is the fundamental unit of billing, IAM and API enablement — every resource lives in exactly one; the GCP Resource Hierarchy of organization, folders and projects and how policy and IAM flow down it; how Google Cloud IAM binds a member to a role at a resource (the policy binding); and the shape of GCP networking, in particular that a VPC in GCP is global (one network spans regions, subnets are regional). You should be comfortable running gcloud, reading JSON/YAML output, and writing Terraform (the landing zone is Terraform end to end). Familiarity with GCP Service Accounts and least privilege helps, because the project factory runs as a service account and every guardrail is ultimately an IAM or policy decision.

This sits at the very foundation of a GCP estate — it is upstream of every workload article. It assumes the hierarchy and IAM fundamentals above and is the platform on which everything else (GKE, BigQuery, Cloud Run, data platforms) is deployed. It pairs tightly with GCP VPC and Shared VPC networking (the networking half of the blueprint) and GCP VPC Service Controls (the data-exfiltration perimeter you add on top once the hierarchy exists). When something goes wrong with access in the resulting estate, the GCP “Permission Denied” decision tree is the companion runbook.

A quick map of who owns which clause of the contract, so you know whose review a change needs:

Layer What lives here Who usually owns it What they enforce
Organization node Root IAM, org policies, billing link Cloud platform / CCoE The non-negotiable guardrails for the whole company
Folders (environment/team) Inherited IAM and policy, project grouping Platform + security Blast-radius boundaries; environment-specific policy
Project factory (Terraform + CI/CD) Project creation, baseline wiring Platform engineering Every project is compliant by construction
Shared VPC host project Subnets, routes, firewalls, Cloud NAT, DNS Network team The single network posture; subnet-level access
Service projects Workloads (GKE, Cloud Run, VMs, data) Product / app teams Their own resources; they consume the network
Logging project Aggregated audit log sink Security / SecOps The tamper-resistant, central audit trail
Billing account + export Spend, budgets, BigQuery export FinOps / finance Cost visibility and allocation

Core concepts

Six mental models make every later decision obvious.

The hierarchy is an inheritance tree, and where you attach a control decides its blast radius. GCP resources form a strict tree: the organization node is the root (created automatically when you verify a domain via Cloud Identity / Google Workspace), folders nest under it (up to several levels deep), and projects are the leaves; resources live in projects. Two things flow down this tree and accumulate: IAM policy bindings (a role granted at a folder applies to every project beneath it) and organization policy constraints (a constraint set at the org node applies everywhere unless overridden lower). This is the entire reason the hierarchy exists — it is not for tidiness, it is the surface you attach governance to. Grant roles/viewer to your auditors at the org node and they can read everything; set constraints/compute.skipDefaultNetworkCreation at the org node and no project anywhere gets a default network. Attach low and the blast radius is small; attach high and it is the whole company.

The project factory makes “a compliant project” a deterministic artifact, not a human act. A landing zone’s promise is self-service without sprawl, and the mechanism is the project factory: a Terraform module (terraform-google-modules/project-factory) that, given a few inputs (name, folder, billing account, the APIs to enable, the Shared VPC to attach, the labels), produces a project that is identical in shape to every other project — same baseline IAM, same log sink, same budget alert, same network attachment. Crucially it runs in CI/CD as a service account, so creating a project is opening a pull request, not clicking in the console. The factory is where compliance stops being a checklist someone might skip and becomes a property of the system.

Shared VPC decouples “who owns the network” from “who runs the workload.” In a Shared VPC, one project is designated the host project and owns the network resources — VPC, subnets, routes, firewall rules, Cloud NAT, Cloud Router. Other projects, service projects, are attached to the host and their resources (VMs, GKE nodes, Cloud Run with direct VPC egress) are placed onto the host’s subnets. The network team manages one network centrally; product teams deploy without touching CIDRs, routes or firewalls. The access control is deliberately fine-grained: a service project’s principals get roles/compute.networkUser on specific subnets, not the whole network, so a workload sees only the subnet it was given. This is the single most important architectural move in the blueprint.

Organization policies disable the dangerous defaults; they are not IAM. An organization policy is a constraint on what configurations are allowed, applied to a node in the hierarchy and inherited down. It is orthogonal to IAM: IAM says who can act; org policy says what the resulting resource may look like. There are two kinds. Boolean constraints are on/off (constraints/compute.disableSerialPortAccess — enforced or not). List constraints allow or deny a set of values (constraints/gcp.resourceLocations — allow only in:eu-locations; constraints/compute.vmExternalIpAccess — deny all, or allow specific VMs). The defaults Google ships are permissive for ease of onboarding (default network, public IPs allowed, any region); the landing zone’s job is to turn the dangerous ones off centrally, with full understanding of how a child policy can override or merge with a parent’s.

Centralization of logs and billing turns “scattered” into “answerable.” By default each project’s audit logs stay in that project and each project can be on its own billing account — which is exactly why nobody can answer estate-wide questions. The blueprint fixes both with central plumbing. An aggregated log sink at the organization (or folder) level captures logs from every project beneath it and routes them to one destination — a dedicated logging project (BigQuery for query, Cloud Storage for cheap retention, or Pub/Sub for streaming to a SIEM). One billing account is linked to the org and every project draws on it, with a BigQuery billing export so cost is queryable and a budget with alerts on each project. Now “who accessed what” and “what did we spend” are single queries.

Separation of duties means no one principal holds every key. A landing zone’s IAM model deliberately splits the powerful roles so that, for example, the person who can create projects cannot also rewrite org policies, and the person who manages the network cannot also grant themselves data access. The privileged roles — Organization Administrator, Organization Policy Administrator, Folder Admin, Project Creator, Network Admin, Security Admin, Billing Account Administrator, Logging Admin — are bound to distinct Google Groups at the appropriate scope, and humans are members of groups, never named directly in a binding. This is separation of duties (SoD): an auditor’s first checklist item and a real defense against both mistakes and insider risk.

The vocabulary in one table

Before the deep sections, pin down every moving part. The glossary at the end repeats these for lookup; this table is the mental model side by side:

Concept One-line definition Where it lives Why it matters to the landing zone
Organization Root node of the hierarchy, tied to a domain Cloud Identity / Workspace The single point to attach company-wide IAM + policy
Folder A grouping node under the org or another folder The hierarchy Blast-radius and inheritance boundary
Project The unit of billing, IAM and API enablement A leaf in the hierarchy Every resource lives in exactly one
Org policy constraint A rule on allowed configurations Org / folder / project Disables dangerous defaults; inherited down
Project factory Terraform that mints baseline projects A module run in CI/CD Makes compliance a property, not a checklist
Shared VPC host Project that owns the central network A designated project One network for many service projects
Service project Project attached to a host’s network Workload projects Runs workloads on the shared subnets
Aggregated log sink A sink capturing logs from a subtree Org / folder level The central, tamper-resistant audit trail
Billing account The payment + cost-rollup container Linked to the org One bill; export to BigQuery for FinOps
Hierarchical firewall policy Inheritable firewall rules on org/folder Org / folder Central network guardrails above per-VPC rules
Google Group A managed set of identities Cloud Identity / Workspace The thing you bind roles to (never users)
Assured Workloads A folder with compliance/residency controls A special folder type Enforces a regulatory regime, not just labels
Cloud Foundation Toolkit (CFT) Google’s reference Terraform/blueprints A module/blueprint repo The opinionated starting point for all of this

The resource hierarchy: design before you build

The hierarchy is the one decision that is genuinely expensive to change later — moving projects between folders can re-evaluate inherited IAM and org policy in ways that break running workloads — so it is worth getting right on paper first. GCP gives you an organization at the root, then folders (nestable, with a documented depth and breadth limit), then projects as leaves.

The four levels and what attaches where

Each level of the tree is a place you can attach IAM bindings and org policy. The art is attaching each control at the highest level where it is universally true, so it is inherited everywhere and you write it once:

Level What it is What you attach here Inheritance behavior
Organization The root, one per domain Company-wide org policies, break-glass IAM, billing link, org-level log sink + firewall policy Everything below inherits; the broadest blast radius
Top-level folders e.g. Common, Production, NonProduction, Development, Bootstrap Environment-wide policy and IAM differences Inherited by all sub-folders and projects beneath
Sub-folders e.g. team or business-unit folders within an environment Team-scoped IAM (the team’s groups), exceptions Inherited by the team’s projects
Projects The workload leaves Resource-specific IAM, project-only policy overrides The narrowest scope; no further inheritance

The reference foundation lays down a small, opinionated set of top-level folders. The two most important and least obvious are Common (also called fldr-common) — which holds the platform projects every environment depends on: the org-level logging project, the org Secrets/KMS project, the interconnect/networking host projects, the org Terraform/seed project — and Bootstrap (fldr-bootstrap), which holds the seed project and the CI/CD plumbing that builds the rest of the foundation and therefore must exist before anything else.

Why folders map to blast radius, not the org chart

The instinct is to model your company: a folder per department, mirroring who reports to whom. Resist it. Org charts re-organize every year or two; your folder structure should not. Worse, an org-chart structure puts the wrong things together — your “Marketing” folder would contain Marketing’s dev, staging and prod, so a too-broad IAM grant or a relaxed policy meant for dev leaks into Marketing’s prod. The blueprint instead cuts the top level by environment (Production, NonProduction, Development) so that the most important boundary — the one between prod and not-prod — is the highest boundary, where the strictest policies and tightest IAM attach and are inherited. Within an environment you can then subdivide by team or business unit. The principle: the dimension that most needs different guardrails should be the highest dimension in the tree. For almost every organization that dimension is environment (prod vs non-prod), not department.

Two folder-design models, weighed honestly:

Model Top-level cut Pro Con Use when
Environment-first (blueprint default) Production / NonProduction / Development / Common Strictest boundary (prod) is highest; policy/IAM inherit cleanly per environment Cross-team prod policy is uniform — less per-team nuance at the top Almost always — this is the recommended default
Business-unit-first A folder per BU, environments nested inside Strong cost/ownership isolation per BU; each BU self-governs Prod vs non-prod boundary is lower, so a BU-wide grant can span prod; re-orgs churn the tree Large, autonomous BUs each running a full SDLC with separate billing

A common refinement is a hybrid: environment at the top, then business unit beneath each environment (Production/RetailBU, Production/DataBU). You get the strict prod boundary high and per-BU subdivision — at the cost of more folders to manage.

Hierarchy limits you must design within

The tree is not infinitely deep or wide; design within Google’s documented limits so you never have to re-cut to fit:

Limit Value (default; many are raisable via quota) Why it matters to design
Folder nesting depth 10 levels below the org node Keep it shallow (3–4 used); depth costs clarity, not just quota
Folders directly under a parent 300 Don’t model 300 teams as flat folders; group them
Total projects under an org Bounded by project-creation quota (request increases) Plan for the factory creating many; raise quota early
Projects per folder No hard per-folder cap, but org-wide quota applies Distribute; don’t pile everything in one folder
Org policies evaluated per resource Inherited set is merged at evaluation Deep stacks of overriding list policies get hard to reason about
IAM bindings (policy size) per resource Members-per-policy and policy-size limits apply Bind groups, not users — groups keep policies small

The single most useful habit: keep the tree shallow and wide-at-the-right-level. Three or four levels (org → environment → team → project) is enough for almost everyone, reasons cleanly, and leaves headroom.

The project factory: compliant projects as pull requests

If the hierarchy is the skeleton, the project factory is the organ that keeps it alive — it is how new projects enter the estate without anyone hand-assembling guardrails. The reference implementation is the terraform-google-modules/project-factory module, run from a CI/CD pipeline (Cloud Build, GitHub Actions, GitLab) by a dedicated project-creator service account.

What the factory does on every project

A bare gcloud projects create gives you an empty, default-network, no-budget, no-log-sink project on whatever billing account the caller defaults to. The factory instead applies the entire baseline atomically:

Factory step What it sets up Why it’s part of the baseline
Create the project In a specific folder, with a deterministic ID Lands in the right blast-radius boundary from birth
Link the billing account The org’s single billing account No project escapes cost visibility
Enable required APIs Only the services this project needs Least functionality; smaller attack surface
Disable the default network Via the org policy + not creating one No wide-open default VPC ever exists
Attach to Shared VPC As a service project; grant subnet networkUser Workloads land on the central network, not a new one
Apply baseline IAM The owning team’s group as editor; viewers; no user Owners Least privilege and SoD from day one
Create a log sink (or rely on the aggregated one) Routes to the central logging project Audit trail is wired automatically
Set a budget + alerts A budget tied to the project’s expected spend Cost anomalies page someone, not surprise finance
Apply labels env, team, cost-center, data-class Cost rollup and policy targeting work

The factory in Terraform

The module call is small because the module is opinionated. A representative service project, attached to a Shared VPC and landed in the production folder:

module "prj_data_prod" {
  source  = "terraform-google-modules/project-factory/google"
  version = "~> 15.0"

  name              = "data-prod"
  random_project_id = true                     # avoids global name collisions
  org_id            = var.org_id
  folder_id         = var.folder_production_id  # land in Production
  billing_account   = var.billing_account_id    # the single org billing account

  # Attach to the Shared VPC host as a service project, on a specific subnet
  shared_vpc                 = var.host_project_id
  shared_vpc_subnets         = [var.subnet_data_prod_self_link]  # subnet-level networkUser

  activate_apis = [
    "compute.googleapis.com",
    "container.googleapis.com",     # only what this project needs
    "bigquery.googleapis.com",
    "logging.googleapis.com",
    "monitoring.googleapis.com",
  ]

  labels = {
    env         = "prod"
    team        = "data-platform"
    cost_center = "cc-4471"
    data_class  = "confidential"
  }

  # No user Owners; the owning team's *group* gets editor at the project
  # (broader roles are bound higher, on the folder, to the same group)
}

The equivalent confirming gcloud (you read with the CLI; you create with Terraform):

# Confirm the project landed in the right folder and on the right billing account
gcloud projects describe data-prod-7f3a \
  --format="value(projectId, parent.id, parent.type)"
gcloud billing projects describe data-prod-7f3a \
  --format="value(billingAccountName, billingEnabled)"

# Confirm it is a Shared VPC service project attached to the host
gcloud compute shared-vpc get-host-project data-prod-7f3a

Why CI/CD, not the console

The factory’s power is that it runs as a service account in a pipeline, which gives you four things the console cannot:

Property Console-created project Factory (CI/CD) project
Reviewability A click no one sees A pull request with diff + approval
Repeatability Each one is bespoke Every one is identical by construction
Auditability “Who created this?” is a log dig The PR + commit is the record
Drift control Drifts immediately, silently terraform plan shows drift; pipeline corrects it
Least privilege Creator usually has Owner The SA has exactly project-creation + billing-link

The project-creator service account is granted a deliberately narrow set of roles at the folder (or org) level — roles/resourcemanager.projectCreator, roles/billing.user (to link the billing account), and roles/resourcemanager.projectIamAdmin scoped to set baseline IAM — and not Owner anywhere. Humans never hold these; the pipeline does.

Shared VPC: one network, many service projects

Shared VPC is the networking spine of the blueprint and the place where “centralized control without central bottleneck” actually happens. One host project owns every network resource; service projects attach to it and run workloads on its subnets.

Host and service project roles

The split of responsibilities is exact and worth memorizing, because it is also the IAM boundary:

Concern Owned by host project Owned by service project
VPC network, subnets, secondary ranges Yes No
Routes, Cloud Router, Cloud NAT Yes No
Firewall rules (network) + firewall policies Yes No
Cloud DNS private zones, peering Yes (typically) No
VMs / GKE nodes / Cloud Run (VPC egress) No Yes (placed on host subnets)
Workload IAM, data, app config No Yes
compute.networkUser grants (who may use a subnet) Granted by host admins Held by service-project principals

Enabling the host and attaching a service project

The host project is enabled for Shared VPC, then each service project is associated with it. With gcloud:

# 1) Designate the host project (network team owns this project)
gcloud compute shared-vpc enable host-net-prod

# 2) Associate a service project with the host
gcloud compute shared-vpc associated-projects add data-prod-7f3a \
  --host-project host-net-prod

# 3) List what's attached, to confirm
gcloud compute shared-vpc list-associated-resources host-net-prod

The Terraform equivalent, which is how you actually manage it:

# Enable the host project for Shared VPC
resource "google_compute_shared_vpc_host_project" "host" {
  project = var.host_project_id
}

# Attach a service project
resource "google_compute_shared_vpc_service_project" "data_prod" {
  host_project    = google_compute_shared_vpc_host_project.host.project
  service_project = module.prj_data_prod.project_id
}

Subnet-level IAM: the part everyone gets wrong

The reason Shared VPC is safe and not just shared is that you grant roles/compute.networkUser at the subnet level, not the project/network level. Grant it on the network and the service project can use every subnet (and see the whole topology); grant it on a single subnet and it can use only that one. Always grant at the subnet:

# Let the data-prod project's principals USE only the data-prod subnet
resource "google_compute_subnetwork_iam_member" "data_prod_subnet_user" {
  project    = var.host_project_id
  region     = var.region
  subnetwork = var.subnet_data_prod_name
  role       = "roles/compute.networkUser"
  member     = "group:gcp-data-platform-prod@example.com"  # a group, not a user
}

Two service accounts beyond your team’s group usually need networkUser on the subnet too — the GKE service agent and the Google APIs service agent — for GKE clusters and certain managed services to provision into the shared subnet. Forgetting these is the number-one Shared VPC failure.

The networkUser grant scopes, compared:

Grant scope What it allows Topology visibility When to use
Network-level (roles/compute.networkUser on the host project) Use any subnet in the network The whole topology Almost never — too broad
Subnet-level (on one subnetwork) Use only that subnet Just that subnet Always — the safe default
Plus GKE/Google-APIs service agents on the subnet Managed services can provision into it The granted subnet Whenever the project runs GKE or certain PaaS

Shared VPC vs VPC Peering vs standalone VPC

Shared VPC is not the only way to connect projects; choosing it deliberately matters:

Model Network ownership Central control Best for Limitation
Shared VPC One host owns it; service projects attach High — one team owns subnets/firewalls A landing zone with a central platform team All service projects share one admin domain per host
VPC Peering Each project owns its own VPC; peered Low — each side self-governs Connecting otherwise-independent VPCs Non-transitive; no central firewall; route/CIDR coordination
Standalone VPC per project Each project owns everything None central True isolation / tiny estates Sprawl; no central posture; the problem the LZ solves

For a landing zone the answer is Shared VPC for the estate, with Peering or Network Connectivity Center only where you must bridge separate hosts (e.g. prod and non-prod hosts kept deliberately apart).

Organization policies: disabling the dangerous defaults

Org policies are how the security team enforces “what configurations are allowed” across the whole estate from one place. This is the most leverage in the entire blueprint and also the most subtle, because of inheritance.

Constraint types and how they evaluate

There are two constraint shapes, and a handful of evaluation knobs that decide how a child policy interacts with a parent’s:

Constraint type Shape Example Values
Boolean Enforced or not constraints/compute.disableSerialPortAccess enforced: true/false
List Allow or deny a set constraints/gcp.resourceLocations allowedValues / deniedValues, or allowAll/denyAll

The evaluation knobs that trip people up:

Knob What it does The gotcha
inheritFromParent Whether a child policy merges with the parent’s Set false and the child replaces the parent entirely (a common accidental hole)
allowAll / denyAll A blanket allow or deny on a list constraint denyAll at the org, then a child allow of specifics is the safe pattern
mergePolicy semantics How allowed/denied sets combine up the tree Denies are powerful; an inherited deny can’t be un-denied by a child allow for some constraints
Dry-run mode Evaluate and log violations without enforcing Always dry-run a new constraint first; read the violations; then enforce
Conditions (CEL) Apply a constraint only to tagged resources Lets you exempt a labeled exception without disabling the whole policy

The high-value constraints every landing zone sets

You will not set every available constraint; you will set the dozen or so that close the dangerous defaults. These are the ones that earn their place at the org node (or a top-level folder), with the failure they prevent:

Constraint Type Recommended setting What it prevents
constraints/compute.skipDefaultNetworkCreation Boolean enforced: true New projects shipping a wide-open default VPC
constraints/compute.vmExternalIpAccess List denyAll (allow specific VMs by exception) VMs getting public IPs by default
constraints/sql.restrictPublicIp Boolean enforced: true Cloud SQL instances with public IPs
constraints/storage.publicAccessPrevention Boolean enforced: true Buckets becoming public (the classic data leak)
constraints/iam.disableServiceAccountKeyCreation Boolean enforced: true Long-lived exportable SA keys (use WIF/impersonation instead)
constraints/iam.allowedPolicyMemberDomains (domain restriction) List allow only your Cloud Identity customer ID(s) Granting access to external/personal Google accounts
constraints/gcp.resourceLocations List allow only your approved regions (e.g. in:eu-locations) Resources created outside your data-residency boundary
constraints/compute.requireOsLogin Boolean enforced: true SSH via metadata keys instead of IAM-controlled OS Login
constraints/compute.disableSerialPortAccess Boolean enforced: true Interactive serial console access to VMs
constraints/iam.automaticIamGrantsForDefaultServiceAccounts Boolean enforced: true Default SAs auto-getting the broad Editor role
constraints/compute.restrictVpcPeering List restrict to approved orgs/networks Unsanctioned cross-org VPC peering
constraints/essentialcontacts.allowedContactDomains List your domains only Security/billing notices going to outside addresses

Setting and testing a constraint

Set a boolean constraint at the org with gcloud, but dry-run it first:

# Dry-run: log what WOULD be denied, enforce nothing yet
gcloud org-policies set-policy --organization=$ORG_ID dryrun-policy.yaml
# ... read violations in Cloud Logging, fix exceptions, THEN enforce ...
gcloud org-policies set-policy --organization=$ORG_ID enforce-policy.yaml

# Inspect the effective policy on a project (what it actually sees after inheritance)
gcloud org-policies describe constraints/compute.vmExternalIpAccess \
  --project=data-prod-7f3a --effective

In Terraform, the org-policy module (v2 API) is the durable way:

# Deny external IPs on VMs across the whole org (allow specific VMs by exception lower)
resource "google_org_policy_policy" "deny_vm_external_ip" {
  name   = "organizations/${var.org_id}/policies/compute.vmExternalIpAccess"
  parent = "organizations/${var.org_id}"

  spec {
    rules {
      deny_all = "TRUE"
    }
  }
}

# A list constraint: restrict resource locations to approved regions
resource "google_org_policy_policy" "resource_locations" {
  name   = "organizations/${var.org_id}/policies/gcp.resourceLocations"
  parent = "organizations/${var.org_id}"

  spec {
    rules {
      values {
        allowed_values = ["in:asia-south1-locations", "in:europe-west1-locations"]
      }
    }
  }
}

Inheritance and override patterns

The whole reason inheritance is subtle is that you often want a global rule with a narrow exception. The pattern that keeps you safe is deny broadly high, allow narrowly low, and prefer denyAll + explicit allows over allowAll + denies:

Goal Set at org Override at folder/project Why this is safe
No public IPs anywhere, except one bastion vmExternalIpAccess: denyAll On the bastion’s project: allowedValues: [<bastion VM>] The default is closed; the hole is explicit and reviewable
EU-only, except one global-marketing project resourceLocations: allow EU On that project: add the extra region The exception is scoped to one project, not the company
No SA keys, except a legacy system disableServiceAccountKeyCreation: enforced On that project: enforced: false The legacy hole is visible and time-boxed
Block external members, except a partner folder allowedPolicyMemberDomains: [yours] On partner folder: add the partner’s customer ID External access is contained to one folder

The decision table for which way to write a list policy:

If you see… It’s probably… Do this
A constraint where the safe state is “nothing allowed” A security boundary (public IPs, external domains) denyAll at org, explicit allow exceptions low
A constraint where the safe state is “a known set” A residency/region or approved-image rule allowedValues: [the known set] at org
A child that mysteriously has more than its parent inheritFromParent: false somewhere Set inheritFromParent: true unless you truly mean to replace
A policy “not taking effect” on a project An override lower in the tree gcloud org-policies describe ... --effective to see the merged result

Hierarchical firewall policies: central network guardrails

Network firewall rules live in a single VPC and only protect that VPC. Hierarchical firewall policies sit on the organization or folder and are inherited by every VPC in every project beneath — so you can mandate a baseline (deny inbound 0.0.0.0/0:22, allow health-check ranges, allow your IAP range) once, high up, and let teams add specifics low.

How the layers and priorities stack

A packet is evaluated against the concatenation of the policies it inherits, in hierarchy order, then the network’s own rules. Understanding the order is everything:

Layer Where it’s defined Evaluated Typical use
Org-level firewall policy Organization node First Company-wide hard denies (e.g. block RDP/SSH from the internet) and must-allows (health checks, IAP)
Folder-level firewall policy A folder After org, before network Environment-specific baseline (prod stricter than dev)
Network firewall rules A single VPC (host project) After hierarchical policies Workload-specific allows the team manages
Implied rules Every VPC Last Implied allow-egress, deny-ingress

Within a hierarchical policy, each rule has a priority and an action of allow, deny, or goto_next (delegate the decision to the next layer down). goto_next is the mechanism that lets a high policy enforce some things and explicitly hand the rest to teams.

A baseline org firewall policy in Terraform

# An org-level firewall policy: deny inbound SSH/RDP from the internet,
# allow GCP health-check + IAP ranges, then delegate the rest downward.
resource "google_compute_firewall_policy" "org_baseline" {
  parent      = "organizations/${var.org_id}"
  short_name  = "org-baseline"
  description = "Company-wide network guardrails"
}

resource "google_compute_firewall_policy_rule" "deny_ssh_rdp_internet" {
  firewall_policy = google_compute_firewall_policy.org_baseline.id
  priority        = 1000
  direction       = "INGRESS"
  action          = "deny"
  match {
    layer4_configs { ip_protocol = "tcp"  ports = ["22", "3389"] }
    src_ip_ranges = ["0.0.0.0/0"]
  }
  enable_logging = true
}

resource "google_compute_firewall_policy_rule" "allow_iap" {
  firewall_policy = google_compute_firewall_policy.org_baseline.id
  priority        = 900     # lower number = higher priority than the deny above
  direction       = "INGRESS"
  action          = "allow"
  match {
    layer4_configs { ip_protocol = "tcp" ports = ["22", "3389"] }
    src_ip_ranges = ["35.235.240.0/20"]   # IAP TCP-forwarding range
  }
}

resource "google_compute_firewall_policy_rule" "delegate_rest" {
  firewall_policy = google_compute_firewall_policy.org_baseline.id
  priority        = 2147483000
  direction       = "INGRESS"
  action          = "goto_next"    # hand everything else to folder/network rules
  match {
    layer4_configs { ip_protocol = "all" }
    src_ip_ranges = ["0.0.0.0/0"]
  }
}

# Associate the policy with the org so it's inherited
resource "google_compute_firewall_policy_association" "org_assoc" {
  firewall_policy = google_compute_firewall_policy.org_baseline.id
  attachment_target = "organizations/${var.org_id}"
  name = "org-baseline-assoc"
}

The hierarchical-vs-network firewall decision, summarized:

Put a rule in… When the rule is… Example
Org firewall policy A non-negotiable, company-wide guardrail Deny 0.0.0.0/0:22; allow IAP range
Folder firewall policy Environment-wide and uniform Prod denies all egress except approved CIDRs
Network firewall rule (host VPC) Workload-specific, team-owned Allow app tier → DB tier on 5432
A tag/SA-targeted rule Scoped to specific instances Allow only web SA instances on 443

Centralized logging and billing

Two pieces of central plumbing turn a scattered estate into an answerable one: an aggregated log sink and a single billing account with export.

The aggregated log sink

An aggregated sink at the org (or folder) level captures matching log entries from every project beneath it and routes them to one destination. You set it once, and every current and future project is covered — including projects the factory hasn’t created yet. Destinations:

Sink destination Best for Cost profile Query story
BigQuery dataset Interactive query, dashboards, alerting on logs Storage + query SQL over logs; great for investigation
Cloud Storage bucket Long-term, cheap retention / compliance Cheapest at rest Not directly queryable; archival
Pub/Sub topic Streaming to a SIEM (Chronicle, Splunk) Per-message Real-time fan-out to external tooling
Log bucket (Cloud Logging) Keep in Logging with controlled retention Logging storage Logs Explorer + Log Analytics

The reference design routes audit logs to a dedicated logging project in the Common folder, locked down so even platform admins can’t quietly delete the trail. Create an org aggregated sink to BigQuery in Terraform:

# Org-level aggregated sink: every project's audit logs → central BigQuery dataset
resource "google_logging_organization_sink" "audit_to_bq" {
  name             = "org-audit-logs"
  org_id           = var.org_id
  include_children = true      # the whole subtree, recursively
  destination      = "bigquery.googleapis.com/projects/${var.logging_project_id}/datasets/${var.audit_dataset_id}"

  # Capture admin-activity + data-access audit logs across the org
  filter = "logName:\"logs/cloudaudit.googleapis.com\""
}

# Grant the sink's writer identity permission to write to the dataset
resource "google_bigquery_dataset_iam_member" "sink_writer" {
  project    = var.logging_project_id
  dataset_id = var.audit_dataset_id
  role       = "roles/bigquery.dataEditor"
  member     = google_logging_organization_sink.audit_to_bq.writer_identity
}

The audit-log categories you must understand, because two of them are off by default and cost money:

Audit log type What it records Default state Cost note
Admin Activity Config/metadata changes (create/delete/setIamPolicy) Always on, free, can’t be disabled The baseline trail
Data Access Reads of data/config (e.g. who read a bucket object) Off by default (except BigQuery) Can be high-volume; enable selectively + budget for it
System Event Google-initiated actions on your resources Always on, free Useful for “GCP did X to my VM”
Policy Denied A request denied by org policy / VPC-SC On when applicable Gold for debugging denied actions

Centralized billing and cost allocation

The billing failure is subtle: nothing breaks if projects are on different billing accounts — you just lose the ability to see total spend and allocate it. The fix is one billing account linked to the org, every project drawing on it, a BigQuery billing export so cost is queryable, and budgets with alerts per project.

Billing control What it gives you How to set it
Single billing account on the org One bill; estate-wide total Link at org; factory links every project to it
BigQuery billing export SQL over daily cost by project/label/SKU Enable export in billing settings → a dataset
Budgets + threshold alerts Pager when a project overspends google_billing_budget per project, alert at 50/90/100%
Labels (cost-center, team, env) Cost rollup by any dimension Factory applies them; export carries them
Billing IAM split Only FinOps manages the account roles/billing.admin to a FinOps group only

A budget with alerts, in Terraform:

resource "google_billing_budget" "data_prod" {
  billing_account = var.billing_account_id
  display_name    = "budget-data-prod"

  budget_filter {
    projects = ["projects/${module.prj_data_prod.number}"]
  }
  amount {
    specified_amount { currency_code = "INR"  units = "300000" }   # ₹3,00,000/mo
  }
  threshold_rules { threshold_percent = 0.5 }
  threshold_rules { threshold_percent = 0.9 }
  threshold_rules { threshold_percent = 1.0 }

  all_updates_rule {
    monitoring_notification_channels = [var.finops_pagerduty_channel]
    disable_default_iam_recipients   = false
  }
}

IAM and separation of duties

The landing zone’s IAM model is built on two rules: bind groups, never users, and split the powerful roles so no one principal holds the whole kingdom. Groups (managed in Cloud Identity / Workspace) keep policy bindings small and stable — you change membership in the directory, not the IAM policy — and SoD is the control auditors look for first.

The privileged roles and who holds them

These are the high-blast-radius roles. Each goes to a distinct group, at the highest scope that role legitimately needs:

Role What it can do Scope it’s bound at Group (example)
roles/resourcemanager.organizationAdmin Manage org-level IAM and the hierarchy Organization gcp-org-admins
roles/orgpolicy.policyAdmin Set/modify org policy constraints Organization gcp-org-policy-admins
roles/resourcemanager.folderAdmin Manage folders + their IAM Org or a top folder gcp-folder-admins
roles/resourcemanager.projectCreator Create projects A folder (often the factory SA) gcp-project-creators
roles/compute.networkAdmin Manage networks, subnets, firewalls Host project / network folder gcp-network-admins
roles/compute.securityAdmin Manage firewall + SSL policies Host project gcp-security-admins
roles/billing.admin Manage the billing account Billing account gcp-billing-admins
roles/logging.admin Manage sinks, log buckets Logging project / org gcp-logging-admins
roles/iam.securityReviewer Read all IAM (audit, no change) Organization gcp-security-auditors

Why separation of duties matters, made concrete

The point of splitting is that the intersection of two roles is where danger lives. Some combinations one principal must not hold:

Don’t let one principal hold… …together with… Because they could…
projectCreator orgPolicyAdmin Create a project and relax the policy that would have constrained it
billingAdmin projectCreator Spin up unbounded spend on a billing account they also control
networkAdmin securityReviewer over data Open a network path and see they have access to the data behind it
organizationAdmin everything This is root — guard it with break-glass, MFA, and few members
logging.admin the workloads being logged Delete the trail of their own actions

The organizationAdmin group is break-glass: a tiny membership, hardware-MFA enforced, normally empty of standing access (members elevate via an approval workflow / PAM), and every use alerts. You should be able to count its day-to-day members on one hand and have them all be process, not people-by-default.

Binding a group at folder scope (Terraform)

# The data-platform team gets editor on the Production *folder* (inherited by their projects)
resource "google_folder_iam_member" "data_team_editor" {
  folder = var.folder_production_id
  role   = "roles/editor"
  member = "group:gcp-data-platform-prod@example.com"
}

# Auditors get read-only across the whole org
resource "google_organization_iam_member" "auditors" {
  org_id = var.org_id
  role   = "roles/iam.securityReviewer"
  member = "group:gcp-security-auditors@example.com"
}

A note on custom roles: prefer predefined roles; create a custom role only when no predefined role fits and you need to narrow permissions (e.g. a “deployer” that can deploy but not read secrets). Custom roles are powerful for least privilege but are a maintenance burden (Google adds permissions to services; your custom role won’t track them), so use them surgically.

Role choice Use when Trade-off
Predefined role A Google role matches the job Easy, tracks new permissions; may be slightly broad
Custom role (org/project) You must narrow to specific permissions Precise least privilege; you maintain it as services evolve
Basic role (owner/editor/viewer) Almost never in prod Far too broad; editor especially is a common over-grant

Assured Workloads: the compliance overlay

For most estates the controls above are the whole story. For regulated ones — public sector, financial services with residency mandates, healthcare — Assured Workloads adds a folder type that enforces a compliance regime (data residency, personnel access controls, specific product restrictions) rather than merely labeling it. You create an Assured Workloads folder for a regime (e.g. a regional sovereignty package, FedRAMP, CJIS, HIPAA-aligned controls), and projects created inside it inherit hard constraints — which regions are usable, which products are permitted, and operational-access guarantees — that you cannot accidentally weaken with an ordinary org-policy override.

Aspect Ordinary folder + org policies Assured Workloads folder
Data residency You enforce via gcp.resourceLocations (can be overridden) Enforced by the program; harder to subvert
Product allow-list You curate manually The compliance package defines permitted products
Personnel/support access Standard Google support model Controls on who (and from where) can access
Monitoring of violations Your own log analysis Built-in compliance monitoring + violation surfacing
When to use The default for non-regulated workloads Only when a named regime requires it

Assured Workloads is a constraint, not a feature you bolt on for comfort — it can limit which services and regions you may use, so adopt it where a regulation demands it and not as a blanket “extra security.” Where it applies, it slots into the hierarchy as one or more dedicated folders, and the project factory targets those folders for the regulated workloads.

Architecture at a glance

The blueprint’s structure is best read as a tree with control attached at every level. At the root sits the organization node, where the company-wide org policies (no default networks, no public IPs, EU-only locations, domain-restricted members), the break-glass org-admin IAM, the aggregated log sink, and the org-level hierarchical firewall policy all attach and inherit downward. Beneath it the top-level folders cut the estate by environment — Common, Production, NonProduction, Development, and Bootstrap — each a blast-radius boundary where environment-specific policy and the owning teams’ groups are bound. The Common folder holds the platform projects every environment leans on: the central logging project (locked-down destination of the sink), the Shared VPC host projects, the org KMS/secrets project, and the seed/Terraform project. Follow any environment folder down to its service projects — the workload leaves the project factory mints — each attached to a Shared VPC host and granted compute.networkUser on just its subnet.

Trace a single new workload through it: a pull request invokes the factory, which creates a project in the Production folder, links it to the one billing account, enables only the APIs it needs, attaches it as a service project to the host, grants its group subnet-level network access, wires its logs into the aggregated sink, and sets a budget — and the org-node policies it inherits mean it cannot have a default network or a public IP even if someone tried. The diagram shows exactly this: the hierarchy on the left with controls attached at each level, the Shared VPC host-and-service relationship in the middle, and the org policies enforcing downward across the whole tree.

GCP Foundation Blueprint resource hierarchy: an organization node carrying company-wide org policies, break-glass IAM, an aggregated log sink and a hierarchical firewall policy, branching into top-level environment folders (Common, Production, NonProduction, Development, Bootstrap); the Common folder holding the central logging project, the Shared VPC host projects and the seed project; service projects under each environment attached to a Shared VPC host with subnet-level networkUser access; and org policy constraints enforced inheriting down the entire tree

The project factory’s runtime sequence — what actually happens, in order, when a new project is requested — is the operational heart of the blueprint. A merged pull request triggers the CI/CD pipeline running as the project-creator service account; the factory creates the project in the target folder, links billing, enables APIs, attaches the project to the Shared VPC host as a service project, grants the owning group compute.networkUser on its designated subnet, applies the baseline IAM and labels, ensures the project’s logs flow to the aggregated sink, and sets the budget — all as one terraform apply, so the project is either fully compliant or not created at all.

Project factory sequence: a merged pull request triggers a CI/CD pipeline running as a project-creator service account, which creates the project in the target folder, links the central billing account, enables only the required APIs, attaches the project to the Shared VPC host as a service project, grants the owning group subnet-level networkUser, applies baseline IAM and labels, wires logs to the aggregated sink, and sets a budget — atomically, so the project is compliant by construction

Finally, the org-policy decision flow shows how a configuration request is evaluated against the inherited stack of constraints. When a principal tries to create a resource — say a VM with an external IP — GCP evaluates the effective policy for that resource: it walks from the org node down to the project, merging constraints according to inheritFromParent and the allow/deny semantics, and either permits the action or denies it with a Policy Denied audit-log entry naming the constraint. The same path explains why a policy “isn’t taking effect”: an override lower in the tree changed the effective result, which --effective reveals.

Org policy decision flow: a configuration request (e.g. create a VM with an external IP) is evaluated against the effective organization policy, walking the hierarchy from the org node down to the project, merging boolean and list constraints per inheritFromParent and allow/deny semantics, then either allowing the action or denying it with a Policy Denied audit-log entry that names the violated constraint and points at the override that produced the effective result

Real-world scenario

FinCloud Analytics is a fintech building a regulated data platform on GCP: ~18 engineers across a data-platform team, an app team, and a two-person platform/security function. They process payments telemetry and are subject to data-residency and audit requirements. When the platform lead joined, GCP was a sprawl: 23 projects created ad hoc, 9 of them on three different billing accounts (two on a founder’s personal account), every project with its own default VPC and the wide-open default firewall rules, no central log sink, and roles/owner granted to eleven individuals across various projects. An auditor’s data-residency and access-control questions had no clean answer. Monthly GCP spend was about ₹14,00,000 and finance could not break it down by team.

The remediation was a Foundation Blueprint rollout in phases, deliberately not a big-bang. Phase 0 (bootstrap): they verified the domain in Cloud Identity to get an organization node (previously they had none — projects were under “No organization”), created a Bootstrap folder with a seed project holding the Terraform state bucket and the project-creator service account, and bound the privileged roles to fresh groups (gcp-org-admins with two members and hardware MFA, gcp-org-policy-admins, gcp-network-admins, gcp-billing-admins, gcp-security-auditors). Phase 1 (org): they set the high-value org policies in dry-run first — skipDefaultNetworkCreation, vmExternalIpAccess: denyAll, storage.publicAccessPrevention, disableServiceAccountKeyCreation, allowedPolicyMemberDomains to their customer ID, and resourceLocations to their two approved regions. The dry-run violation logs were illuminating: 9 buckets were public and 31 SA keys existed. They remediated those before flipping to enforce.

Phase 2 (environments + central plumbing): top-level folders (Common, Production, NonProduction, Development), a Common Shared VPC host per environment, a locked-down logging project with an org aggregated sink to BigQuery (and a GCS bucket for cheap long retention), the single billing account linked to the org with BigQuery billing export enabled, and an org hierarchical firewall policy denying inbound 0.0.0.0/0:22/3389 and allowing the IAP range. Phase 3 (workloads): they stood up the project factory and, project by project, re-created each workload as a factory-minted service project (rather than trying to retrofit the old ones), migrated data, and decommissioned the originals — so every project in the new estate was compliant by construction.

What went wrong mid-rollout, and the lesson: when they enforced vmExternalIpAccess: denyAll org-wide, a Cloud Build worker pool that legitimately needed egress broke, and a GKE cluster failed to create because they had forgotten to grant the GKE service agent compute.networkUser on the shared subnet. Both were caught because they had dry-run first and rolled out per folder — the breakage was contained to one non-prod folder, fixed with a scoped exception (a labeled bastion allow, and the missing service-agent grant), and never touched production. The end state: one billing account, spend now broken down by team/cost-center label in a BigQuery dashboard (which surfaced ₹2,10,000/month of idle dev resources they then killed), an org-wide audit trail answerable in SQL, and zero public buckets or default networks. Net monthly spend fell to ₹11,20,000 and the next audit’s access-control section was a terraform plan and one query. The lesson on the wall: “Dry-run every policy, roll out per folder, and re-create rather than retrofit.”

The rollout as a timeline, because the order is the lesson:

Phase What they did Why this order What it prevented
0 — Bootstrap Org node, seed project, groups + break-glass Nothing else can attach without an org node and a place to run Terraform Building on sand; root held by individuals
1 — Org policies (dry-run) High-value constraints in dry-run, then enforce See the violations before enforcing breaks things Enforcing denyAll and taking down Cloud Build
2 — Environments + plumbing Folders, Shared VPC, sink, billing, firewall Workloads need a place to land and central plumbing to wire into Re-doing project wiring later
3 — Workloads (factory) Factory; re-create each project compliant Retrofitting old projects is slower and leaves drift Carrying forward 23 bespoke configurations

Advantages and disadvantages

The blueprint’s centralization is its strength and, if misapplied, its risk. Weigh it honestly:

Advantages (why this model helps you) Disadvantages (why it bites)
One place (the org node) to enforce policy across the whole company — security writes a guardrail once The org node and host projects become critical: an outage or misconfig there has the widest blast radius
The project factory makes every project compliant by construction — sprawl becomes structurally impossible Up-front design and engineering cost is real; it does not pay off for a single-project startup
Shared VPC gives one network posture with subnet-level least privilege — central control without central bottleneck Cross-project IAM (service agents, subnet networkUser) is fiddly; a forgotten grant breaks GKE/PaaS in confusing ways
Centralized logging + billing make “who did what” and “what did we spend” single queries The logging and billing projects need careful access control or they become a single point of information failure
Org policies disable the dangerous defaults (public buckets, default VPCs, SA keys) estate-wide Inheritance/override semantics are subtle; a wrong inheritFromParent silently opens a hole
The hierarchy lets cost and audit roll up cleanly by environment/team The hierarchy is expensive to re-cut; moving projects between folders re-evaluates inherited policy and can break workloads
Phased rollout + dry-run + Terraform make the whole thing reviewable and reversible Done as a big-bang without dry-run, an org-wide enforce can take down legitimate workloads instantly

The model is right whenever you have multiple teams, regulated or sensitive workloads, or a need for central network/billing/audit governance — which is to say, essentially every organization past its first few projects. It is over-engineering for a genuine single-project, single-team experiment; the overhead of Shared VPC, org policies, a factory and central plumbing only earns out past a handful of workloads. The disadvantages are all manageable — dry-run policies, roll out per folder, guard the critical projects with tight IAM and break-glass, and treat the hierarchy as a deliberate design — but only if you know they exist, which is the point of building it consciously rather than letting it accrete.

Hands-on lab

You will stand up a miniature foundation in a sandbox: a folder, an org policy set in dry-run then enforced, a Shared VPC host with a service project on a subnet, and an aggregated log sink — all with gcloud and Terraform, all tear-downable. This requires an organization and the privileged roles on it (Org Admin, Org Policy Admin, Project Creator, Billing User); if you only have a single project you can do the project-factory and Shared VPC parts but not the org-policy/folder parts. Run in Cloud Shell.

Step 1 — Set context variables.

export ORG_ID=$(gcloud organizations list --format="value(ID)" | head -n1)
export BILLING=$(gcloud billing accounts list --format="value(ACCOUNT_ID)" | head -n1)
export REGION=asia-south1
echo "Org: $ORG_ID  Billing: $BILLING"

Expected: a numeric org ID and a billing account ID print. If ORG_ID is empty you have no organization — the folder/policy steps won’t apply.

Step 2 — Create a sandbox folder (the safe place to test policy).

gcloud resource-manager folders create \
  --display-name="lz-sandbox" --organization=$ORG_ID
export FOLDER=$(gcloud resource-manager folders list \
  --organization=$ORG_ID --filter="displayName=lz-sandbox" --format="value(ID)")
echo "Folder: $FOLDER"

Expected: a folder is created and its numeric ID prints.

Step 3 — Create two projects (a host and a service project) via gcloud. (In production the factory does this; here we do it directly to keep the lab self-contained.)

export HOST=lz-host-$RANDOM
export SVC=lz-svc-$RANDOM
gcloud projects create $HOST --folder=$FOLDER
gcloud projects create $SVC  --folder=$FOLDER
gcloud billing projects link $HOST --billing-account=$BILLING
gcloud billing projects link $SVC  --billing-account=$BILLING
gcloud services enable compute.googleapis.com --project=$HOST
gcloud services enable compute.googleapis.com --project=$SVC

Expected: two projects created in the sandbox folder, both linked to billing, Compute API enabled.

Step 4 — Set an org policy in DRY-RUN on the sandbox folder, then read violations. We disable default network creation, but in dry-run so it only logs:

cat > dryrun.yaml <<EOF
name: ${FOLDER}/policies/compute.skipDefaultNetworkCreation
spec:
  rules:
  - enforce: true
EOF
# Apply as a DRY-RUN policy (logs would-be violations, enforces nothing)
gcloud org-policies set-policy dryrun.yaml --update-mask=dryRunSpec
# Inspect the effective policy on the service project
gcloud org-policies describe constraints/compute.skipDefaultNetworkCreation \
  --project=$SVC --effective

Expected: the dry-run policy is accepted; --effective shows the constraint as it would apply. This is the habit to internalize — never enforce a new constraint blind.

Step 5 — Enforce the policy on the folder.

cat > enforce.yaml <<EOF
name: ${FOLDER}/policies/compute.skipDefaultNetworkCreation
spec:
  rules:
  - enforce: true
EOF
gcloud org-policies set-policy enforce.yaml

Expected: the constraint now enforces — new projects under this folder won’t get a default network.

Step 6 — Build a Shared VPC: host network, subnet, attach the service project.

# Create a VPC + subnet in the host project (custom mode — no auto subnets)
gcloud compute networks create lz-vpc --subnet-mode=custom --project=$HOST
gcloud compute networks subnets create lz-subnet-$REGION \
  --network=lz-vpc --range=10.10.0.0/24 --region=$REGION --project=$HOST

# Enable Shared VPC on the host and attach the service project
gcloud compute shared-vpc enable $HOST
gcloud compute shared-vpc associated-projects add $SVC --host-project=$HOST

# Confirm the attachment
gcloud compute shared-vpc list-associated-resources $HOST

Expected: a custom VPC + subnet exist in the host; the host is Shared-VPC-enabled; the service project appears in the associated-resources list.

Step 7 — Grant subnet-level networkUser to a principal (least privilege). Replace the member with your own user or a group:

gcloud compute networks subnets add-iam-policy-binding lz-subnet-$REGION \
  --region=$REGION --project=$HOST \
  --member="user:$(gcloud config get-value account)" \
  --role="roles/compute.networkUser"

Expected: the binding is added on the subnet (not the whole network) — this is the safe Shared VPC grant.

Step 8 — (Optional) Create an aggregated log sink on the folder to a GCS bucket.

gcloud storage buckets create gs://lz-audit-$RANDOM --location=$REGION --project=$HOST
export LOGBUCKET=$(gcloud storage buckets list --project=$HOST --format="value(name)" | grep lz-audit | head -n1)
gcloud logging sinks create lz-folder-sink \
  storage.googleapis.com/$LOGBUCKET \
  --folder=$FOLDER --include-children \
  --log-filter='logName:"logs/cloudaudit.googleapis.com"'

Expected: a sink is created capturing audit logs from the whole folder subtree into the bucket. (You’d then grant the sink’s writer identity roles/storage.objectCreator on the bucket for it to actually write.)

Validation checklist. You created a hierarchy node (folder), set a constraint in dry-run first then enforced it, built a Shared VPC with subnet-level least-privilege access, and wired an aggregated sink — the four pillars of the blueprint in miniature. The lab steps mapped to what each proves:

Step What you did What it proves Real-world analogue
2 Create a sandbox folder The hierarchy is the surface controls attach to Every environment folder
4 Org policy in dry-run You can see violations before enforcing How you safely ship any constraint
5 Enforce the policy A constraint inherits down to projects The org-wide guardrails
6–7 Shared VPC + subnet networkUser Central network with least-privilege access The networking spine of the LZ
8 Aggregated sink One trail captures a whole subtree The central audit log

Cleanup (avoid lingering charges and an orphaned folder).

# Delete the sink, projects (this releases their resources), then the folder
gcloud logging sinks delete lz-folder-sink --folder=$FOLDER --quiet
gcloud projects delete $SVC  --quiet
gcloud projects delete $HOST --quiet
gcloud resource-manager folders delete $FOLDER

Cost note. Empty projects, a custom VPC, one subnet and a near-empty GCS bucket cost essentially nothing for an hour (well under ₹50); deleting the projects and folder stops everything. A Shared VPC and org policies themselves are free — you pay for the resources inside the projects, of which this lab creates almost none.

Common mistakes & troubleshooting

The failures here are mostly IAM and inheritance subtleties — they confuse because the symptom is far from the cause. First as a scannable table, then the entries that bite hardest in full.

# Symptom Root cause Confirm (exact cmd / path) Fix
1 GKE cluster create fails on a Shared VPC with a permissions error GKE/Google-APIs service agent lacks compute.networkUser on the subnet gcloud projects get-iam-policy $HOST; check the subnet IAM Grant the GKE + Google-APIs service agents networkUser on the subnet
2 A new project still got a default network skipDefaultNetworkCreation not enforced at or above this project gcloud org-policies describe constraints/compute.skipDefaultNetworkCreation --project=$P --effective Enforce the constraint at the org/folder; delete the default VPC
3 An org policy “isn’t taking effect” on one project An override lower in the tree (project/folder policy) changed the effective result gcloud org-policies describe <constraint> --project=$P --effective Remove/adjust the lower override; check inheritFromParent
4 VM with an external IP was created despite vmExternalIpAccess: denyAll The project has an allow override, or the policy is set below this resource’s path --effective on the project shows the merged allow Remove the override, or scope the allow to specific VMs only
5 Service project can use subnets it shouldn’t networkUser granted at the network/project level, not the subnet gcloud compute networks get-iam-policy lz-vpc shows a project-level binding Revoke the broad grant; re-grant at the subnet only
6 Terraform terraform apply fails: cannot create project The project-creator SA lacks projectCreator on the target folder, or billing-link permission gcloud resource-manager folders get-iam-policy $FOLDER Grant the SA roles/resourcemanager.projectCreator + roles/billing.user
7 Logs aren’t arriving in the central BigQuery dataset The sink’s writer identity lacks write permission on the destination gcloud logging sinks describe <sink> → check writerIdentity + dataset IAM Grant the writer identity bigquery.dataEditor on the dataset
8 An external (gmail) account was granted a role allowedPolicyMemberDomains not set, or set below where the grant happened gcloud org-policies describe constraints/iam.allowedPolicyMemberDomains --effective Set domain restriction at the org; remove the external binding
9 Moving a project to another folder broke its access Inherited IAM/policy changed with the new parent Compare effective policy before/after; check folder IAM diff Re-grant needed bindings; avoid casual project moves
10 Data Access audit logs are empty for a service Data Access logs are off by default Check the org/project IAM audit config Enable Data Access logs for that service (and budget for volume)
11 Billing export dataset has no recent rows Export was enabled late or to the wrong dataset; or perms Billing → export settings; check the dataset Re-point/enable the export; it’s not retroactive
12 Hierarchical firewall rule seems ignored A higher-priority rule (lower number) or goto_next handled the packet first gcloud compute firewall-policies describe <policy> and rule priorities Reorder priorities; check the org→folder→network evaluation chain
13 Org policy change had no audit trail of who changed it Looking in the wrong project’s logs Query the org-level audit logs for SetOrgPolicy Use the aggregated sink / org logs, not a project’s
14 terraform plan shows huge drift on IAM bindings Used google_*_iam_policy (authoritative) instead of _iam_member Inspect the resource type in state Switch to _iam_member/_iam_binding; authoritative resources clobber out-of-band grants

The expanded form for the ones that cost the most time:

1. GKE (or a managed service) fails to create on a Shared VPC. Root cause: the GKE service agent (service-<hostnum>@container-engine-robot.iam.gserviceaccount.com) and/or the Google APIs service agent (<svcprojnum>@cloudservices.gserviceaccount.com) lack roles/compute.networkUser on the shared subnet — your team’s group having it is not enough; the platform’s agents provision the nodes. Confirm: inspect the subnet’s IAM (gcloud compute networks subnets get-iam-policy <subnet> --region=<r> --project=$HOST) and look for the agents. Fix: grant both service agents networkUser on the subnet; for GKE also grant the GKE agent roles/container.hostServiceAgentUser on the host project. This single omission is the most common Shared VPC failure in the wild.

2. A new project still got a default network. Root cause: constraints/compute.skipDefaultNetworkCreation was not enforced at or above the project when it was created — a dry-run, or a policy set only on a sibling folder, doesn’t help. Confirm: gcloud org-policies describe constraints/compute.skipDefaultNetworkCreation --project=$P --effective shows it not enforced. Fix: enforce it at the org (or the environment folder), and delete the stray default VPC and its permissive default firewall rules.

3. An org policy “isn’t taking effect.” Root cause: something lower in the hierarchy overrode it — a project- or folder-level policy with a different rule, or inheritFromParent: false that replaced the parent’s policy rather than merging. Confirm: always reach for --effective, which shows the merged result GCP actually evaluates, not just what you set at one node. Fix: remove or correct the lower override; set inheritFromParent: true unless you genuinely intend to replace the inherited policy.

5. A service project can use subnets it shouldn’t. Root cause: networkUser was granted at the network or host-project level (which grants every subnet) instead of on a single subnet. Confirm: gcloud compute networks get-iam-policy <network> --project=$HOST shows the over-broad binding. Fix: revoke it and re-grant roles/compute.networkUser on the specific subnetwork resource only — the whole safety property of Shared VPC depends on this.

14. Terraform IAM drift / clobbering. Root cause: using the authoritative google_*_iam_policy (or _iam_binding for a role) resource, which owns the entire policy (or the whole role’s member set) and overwrites any binding made outside Terraform — including ones other teams or Google added. Confirm: the resource type in your state is _iam_policy/_iam_binding. Fix: prefer the non-authoritative google_*_iam_member resources, which add/remove a single member without disturbing others. In a multi-team landing zone, authoritative IAM resources are a foot-gun.

Best practices

Security notes

The security constraints that do the most, mapped to the threat they close:

Control Constraint / mechanism Closes Also enables
Domain restriction iam.allowedPolicyMemberDomains Access granted to external/personal accounts Clean “only our people” audit answer
No SA keys iam.disableServiceAccountKeyCreation + WIF Leaked long-lived credentials Forces modern short-lived auth
No public buckets storage.publicAccessPrevention The classic open-bucket data leak Compliance posture by default
No public IPs compute.vmExternalIpAccess: denyAll Internet-exposed VMs Forces IAP/bastion access patterns
Locked audit trail Aggregated sink + restricted logging project Tampering / hiding tracks Tamper-evident, SQL-queryable audit
Subnet-level network access compute.networkUser on subnet Lateral network reach Least-privilege networking
Data perimeter VPC Service Controls Exfiltration with a valid credential Service-level data boundary

Cost & sizing

The landing-zone constructs are almost all free — you pay for the resources inside the projects, plus a little for the central plumbing:

A rough monthly picture for the foundation itself (excluding workload compute/storage), for a mid-size estate:

Cost driver What you pay for Rough INR / month Notes
Hierarchy + org policies + Shared VPC Nothing ₹0 Governance constructs are free
Aggregated log sink → GCS (retention) Cheap object storage ~₹2,000–8,000 Scales with log volume; coldline for old logs
Aggregated log sink → BigQuery (query) Storage + query on logs ~₹8,000–40,000 Data Access logs dominate; filter aggressively
Billing BigQuery export Storage + light query ~₹500–2,000 Pays for itself via waste it surfaces
Cloud NAT (host project) Hourly + per-GB ~₹3,000–15,000 Belongs to workloads; billed in host
Central security tooling (SCC, optional) Tier-dependent varies Security Command Center premium is separate

The sizing rule: the foundation’s own cost is small and dominated by log volume; spend your optimization effort on filtering logs (don’t ingest everything to BigQuery — route noise to GCS or drop it) rather than on the free governance constructs. The real savings the blueprint unlocks are on the workload side — the cost visibility it gives you is what lets you find and kill waste.

Interview & exam questions

1. Why must a GCP landing zone start with an organization node, and how do you get one? Because the organization node is the only place company-wide IAM and org policies can attach and inherit down — without it, projects are under “No organization” and ungovernable centrally. You obtain it by verifying a domain through Cloud Identity or Google Workspace, which automatically creates the org node tied to that domain.

2. Explain how IAM and org policy inheritance flow through the hierarchy, and why folder design matters. Both flow downward and accumulate: a role granted at a folder applies to every project beneath it, and a constraint set at the org applies everywhere unless overridden lower. So folder design decides blast radius — attach a control at the highest level where it’s universally true. Cutting top-level folders by environment puts the strictest (prod) boundary highest, where the tightest policy/IAM inherit cleanly.

3. What is the project factory and why run it in CI/CD rather than the console? The project factory is a Terraform module (terraform-google-modules/project-factory) that mints projects with a complete baseline — folder placement, billing link, only-needed APIs, Shared VPC attachment, baseline IAM, log sink, budget and labels. Running it in CI/CD as a narrow service account makes project creation a reviewable pull request, identical by construction, audited, and drift-controlled — none of which the console gives you.

4. In Shared VPC, at what level do you grant compute.networkUser, and what’s the consequence of getting it wrong? At the subnet level — so a service project can use only its designated subnet and can’t see the rest of the topology. Granting it at the network or host-project level lets the project use every subnet, breaking least privilege. The most common failure is forgetting that the GKE and Google-APIs service agents also need it on the subnet, which makes cluster/PaaS creation fail.

5. What’s the difference between a boolean and a list org-policy constraint, with an example of each? A boolean constraint is on/off — e.g. constraints/compute.disableSerialPortAccess (enforced or not). A list constraint allows or denies a set of values — e.g. constraints/gcp.resourceLocations (allow only approved regions) or constraints/compute.vmExternalIpAccess (denyAll, or allow specific VMs). List constraints support allowedValues/deniedValues and allowAll/denyAll.

6. How do you safely introduce a new org policy across a live estate? Dry-run first: apply it in dry-run mode so violations are logged but not enforced, read those violations, remediate the legitimate exceptions, and only then flip to enforce — rolling out per folder before org-wide. This is exactly how you avoid an org-wide denyAll instantly breaking a legitimate workload (e.g. a Cloud Build pool that needs egress).

7. A policy you set at the org node “isn’t taking effect” on one project. What’s happening and how do you diagnose it? Something lower in the hierarchy overrode it — a project/folder policy with a different rule, or inheritFromParent: false that replaced rather than merged the parent’s policy. Diagnose with gcloud org-policies describe <constraint> --project=<p> --effective, which shows the merged effective result GCP actually evaluates. Fix by removing/correcting the lower override.

8. What does an aggregated log sink do, and where should its destination live? An aggregated sink at the org or folder level captures matching logs from every project beneath it (with include_children) and routes them to one destination — covering current and future projects automatically. The destination should be a dedicated, locked-down logging project (BigQuery for query, GCS for cheap retention, Pub/Sub for SIEM streaming), with delete access tightly restricted so the trail can’t be tampered with.

9. Describe a separation-of-duties model for the privileged landing-zone roles. Bind each high-blast-radius role to a distinct Google Group at the appropriate scope: organizationAdmin, orgPolicyAdmin, folderAdmin, projectCreator, networkAdmin, securityAdmin, billingAdmin, loggingAdmin, plus read-only securityReviewer for auditors. No one principal should hold combinations like project-creator + org-policy-admin (create a project and relax its constraints) or billing-admin + project-creator (unbounded spend). Org-admin is break-glass.

10. When would you use Assured Workloads, and how is it different from just setting org policies? Use it when a named regulatory regime (sovereignty/residency, FedRAMP, HIPAA-aligned, CJIS) requires enforced controls. Unlike ordinary org policies — which you set and could accidentally override — an Assured Workloads folder enforces data residency, a permitted-product allow-list, and personnel-access controls as a program, harder to subvert, with built-in compliance monitoring. It’s a constraint regime, not a label, so adopt it only where regulation demands it.

11. Why prefer google_*_iam_member over google_*_iam_policy in a landing zone’s Terraform? Because _iam_policy (and _iam_binding) are authoritative — they own the whole policy (or a role’s entire member set) and overwrite any binding made outside that Terraform, including ones other teams or Google’s own automation added. In a multi-team estate that’s a foot-gun. _iam_member is non-authoritative: it adds/removes a single member without disturbing others.

12. What are hierarchical firewall policies and how do they relate to network firewall rules? They’re firewall policies attached to the org or folder and inherited by every VPC beneath, evaluated before a network’s own rules. They let you mandate a baseline (deny 0.0.0.0/0:22, allow IAP/health-check ranges) high in the hierarchy and use goto_next to delegate the rest to folder policies and per-VPC rules. Evaluation order is org policy → folder policy → network rules → implied rules.

These map to the Google Cloud Professional Cloud Architect and Professional Cloud Security Engineer certifications (resource hierarchy, org policies, Shared VPC, IAM/SoD, logging) and the Professional Cloud Network Engineer for Shared VPC and hierarchical firewall specifics. A compact cert-mapping:

Question theme Primary cert Objective area
Hierarchy, folders, inheritance Cloud Architect Designing resource management
Org policies, constraints, dry-run Cloud Security Engineer Configuring access + org policy
Shared VPC, subnet networkUser Cloud Network Engineer Designing/implementing VPC networks
IAM, SoD, groups, custom roles Cloud Security Engineer Managing identity + access
Centralized logging, audit logs Cloud Security Engineer / DevOps Engineer Logging, monitoring, audit
Project factory, Terraform/CFT Cloud DevOps Engineer / Architect Automating infrastructure
Assured Workloads, residency Cloud Security Engineer Compliance + data protection

Quick check

  1. You set constraints/compute.skipDefaultNetworkCreation to enforce at a single dev folder, but a new project in the production folder still got a default network. Why?
  2. A GKE cluster fails to create on a Shared VPC even though your team’s group has compute.networkUser on the subnet. What’s the most likely missing grant?
  3. True or false: granting roles/compute.networkUser on the host network is the recommended way to let a service project use a subnet.
  4. You set an org policy at the organization node but it “isn’t taking effect” on one project. What single command shows you why, and what’s the usual cause?
  5. Why do you bind Google Groups rather than individual users in landing-zone IAM, and name one role combination no single principal should hold.

Answers

  1. Org policy inherits downward; a constraint set only on the dev folder applies to that folder’s subtree, not to the production folder. To cover the whole estate, enforce it at the organization node (or on every environment folder). Then delete the stray default VPC the prod project received.
  2. The GKE service agent (and often the Google APIs service agent) needs roles/compute.networkUser on the subnet too — your team’s group having it is not enough, because the platform’s agents provision the nodes. For GKE also grant the GKE agent roles/container.hostServiceAgentUser on the host project.
  3. False. Granting networkUser on the network lets the service project use every subnet and see the whole topology. The least-privilege, recommended way is to grant it on the specific subnetwork only.
  4. gcloud org-policies describe <constraint> --project=<p> --effective shows the merged effective policy GCP actually evaluates. The usual cause is an override lower in the hierarchy (a folder/project policy, or inheritFromParent: false replacing the parent’s policy).
  5. Groups keep IAM policy bindings small and stable — you change membership in the directory, not the policy — and they’re the basis of clean separation of duties. One combination to avoid: projectCreator + orgPolicyAdmin (a principal could create a project and relax the policy meant to constrain it). Others: billingAdmin + projectCreator, or loggingAdmin over the workloads it logs.

Glossary

Next steps

You can now design and sequence a governed GCP foundation. Build outward:

GCPLanding ZoneShared VPCOrg PolicyIAMTerraformProject FactoryGovernance
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading