← Back to Index
↳ Project /30GCP · Platform · Governance

GCP Landing Zone

An organization built as code: resource hierarchy, nine org policy constraints enforced at the org root with a deliberate folder-level override, a Shared VPC with no public SSH path, and an audit sink that covers projects created after it exists.

Role
Cloud / Platform
Cloud
GCP
Constraints
9 at the org root
Resources
52 (Terraform)
Organization
9 constraints enforced
core
network · logging
workloads
nonprod · prod
Project Factory
folder + billing + audit
nonprod override
locations widened to EU
Shared VPC
deny ingress · IAP SSH only
Org Sink
include_children
BigQuery
30d partitions · CMEK
SCC + Budget
findings to Pub/Sub
Placement is the policy — one factory vends every project, and inheritance does the enforcing

/01Problem

A cloud organization decays in a predictable direction. Projects get created outside any hierarchy, each with a default VPC nobody chose and firewall rules nobody reviewed. Service account keys accumulate. Audit logs exist per project and nowhere centrally, so the question "who read that" has no answer. Spend is discovered monthly.

None of this is caused by bad engineers. It is caused by the defaults being wrong and by every correct decision needing to be remade by each person who creates a project. A landing zone moves those decisions into the hierarchy, where inheritance does the enforcing.

/02The third version of the same idea

This is the same problem already solved with AWS Organizations and SCPs, and with an Azure Landing Zone and Azure Policy. The differences are the reason to build it a third time rather than the reason not to.

An SCP is a deny boundary evaluated against IAM at request time: the call is authorized or it is not. Azure Policy evaluates resources and can deny, audit, or mutate through effects. GCP org policy constrains the shape of the configuration itself, so the API rejects a violating resource. The violation cannot exist rather than being disallowed to whoever asked.

Exceptions run the opposite way too. An SCP deny cannot be un-denied further down the tree, so AWS exceptions mean moving an account to a different OU. GCP list constraints let a child policy widen an inherited one, which this build demonstrates on purpose: resource locations allow US at the org, and nonprod adds EU for a residency test without weakening anything elsewhere.

/03Placement is the policy

core holds platform-owned projects for network and logging. workloads splits into nonprod and prod. The two workload projects are byte-for-byte identical except for which folder they land in, which is the entire demonstration: they are governed differently without either one carrying policy code.

Constraints attach at the organization rather than a folder, because a policy attached to a folder is bypassed by creating a project somewhere else. IAM inherits the same way, which makes folder design a security decision rather than an org-chart decision.

/04The constraint that ships disabled

iam.allowedPolicyMemberDomains is written and defaults to off, and the default is the point. It blocks binding allUsers, which breaks any public Cloud Run service. Enabling it without knowing that is how a landing zone quietly blocks a workload the organization intends to run, and the surprise surfaces weeks later as an unexplained permission error.

Naming that trade-off in code is worth more than silently enforcing one side of it.

/05Network, telemetry, spend

  • One Shared VPC host project owns the network; workload projects attach as service projects and consume a subnet they do not own. Access is granted per subnet, not per project, which is the least-privilege form of the pattern.
  • Firewall is explicit default-deny plus SSH from the IAP forwarding range only, so there is no public SSH path and no VM carries an external IP.
  • An organization sink with include_children ships admin activity, data access, system event, and policy denial logs to a partitioned BigQuery dataset, covering projects created after the sink exists. Partition expiry bounds retention and cost together.
  • Security Command Center Standard, which is free, streams active unmuted findings to Pub/Sub, and a budget alerts on actual spend at 50, 90, and 100 percent plus a forecast rule.
  • One CMEK key covers the audit dataset and both topics, so a single disable revokes the org's entire audit trail and finding stream at once.

/06What a real organization taught

  • A new GCP organization is not greenfield. Google pre-applies a secure-by-default policy set, so three of the nine constraints already existed and the apply failed with 409 POLICY_ALREADY_EXISTS on each.
  • organizationAdmin grants almost none of the operational org permissions. Four more roles were needed, each found by an apply failing on exactly one resource: folderAdmin, orgpolicy.policyAdmin, compute.xpnAdmin, and logging.configWriter.
  • A self-serve billing account caps how many projects can be linked at once, and projects in DELETE_REQUESTED keep counting for 30 days, so the ceiling arrives sooner than a project list suggests.
  • A resource's arguments may be unknown at plan time; its count may not. The Shared VPC attachment had to key off a boolean rather than a host project ID generated in the same apply.

/07Two corrections worth more than the build

The first was a test that proved nothing. Creating a network named default and expecting a denial is the obvious check for compute.skipDefaultNetworkCreation, and it succeeds. The constraint suppresses the default VPC at project creation; it says nothing about the name default afterwards. The check was passing by creating a network rather than by being denied, and the validation step was rewritten to confirm a freshly vended project has zero networks instead.

The second was worse and more instructive. Importing the three pre-existing Google policies to resolve the 409s handed Terraform ownership of policies it never created, and destroy duly deleted them, leaving the organization less protected than before the landing zone was ever applied, with service account key creation newly permitted org-wide. They were restored by hand and the trap is documented in the teardown section. Adopting existing infrastructure is a two-way door only if you know which side you came in on.

/08Verified, then destroyed

Inheritance was proven the only way that counts: an effective-policy query returned US value groups only at prod, and US plus europe and EU at nonprod, from a child policy widening the inherited one rather than replacing it. The Shared VPC attachment resolved, and the org sink reported delivery to BigQuery with includeChildren true.

Two controls proved themselves by refusing: a service account key creation denied with the constraint named, and a bucket in asia-northeast1 refused with a 412 naming gcp.resourceLocations. Everything was then destroyed, leaving zero folders, the seed project in DELETE_REQUESTED with billing unlinked, and the three borrowed org policies put back.

↳ Run Receipt/30
Provision52 resources: 4 folders, 9 org policy constraints, 3 vended projects, Shared VPC with 2 firewall rules, org sink, BigQuery dataset, CMEK key, 2 topics, budget
ProofEffective policy at prod resolves US only, at nonprod US plus EU; Shared VPC attachment resolved; org sink delivering with includeChildren true
DeniedService account key creation refused; bucket in asia-northeast1 refused with 412 naming gcp.resourceLocations
Not deployedSCC notification config, blocked on org permissions beyond notificationConfigEditor
Destroy52 then 18 resources destroyed; zero folders, seed project DELETE_REQUESTED with billing unlinked, three pre-existing org policies restored by hand
Costunder $0.20 for the full deploy-demo-destroy cycle
Org PolicyResource ManagerShared VPCCloud LoggingBigQuerySecurity Command CenterCloud KMSPub/SubTerraform
StatusCheckingDeployed2026-09-20 13:33 UTCVisitsOriginPrivate S3 + CloudFront OACPipelineGitHub Actions OIDCCounterAPI Gateway + Lambda + DynamoDB