GCP organization and folder structure for a startup after Series A

Recently I had a case where a startup closed their Series A and was still running everything out of one GCP project tied to the founder’s personal Google account. Works fine until it doesn’t — no cost attribution, no IAM boundary between prod and whatever someone is testing, no policy enforcement anywhere. GCP governance isn’t AWS accounts + SCPs, it’s Organization -> Folders -> Projects, and you build it in that order.

Org and folders

Start with folders, one per usage. Everything else inherits from here.

gcloud resource-manager folders create --display-name="Prod" --organization=123456789012
gcloud resource-manager folders create --display-name="Non-Prod" --organization=123456789012
gcloud resource-manager folders create --display-name="Shared" --organization=123456789012

Projects go inside, split by concern (compute, data, networking), not dumped together:

gcloud projects create acme-prod-compute --folder=FOLDER_ID --organization=123456789012
gcloud projects create acme-prod-data --folder=FOLDER_ID --organization=123456789012

Cost attribution, IAM inheritance and org policies all hang off this hierarchy, so get it right before anything else exists.

IAM baseline

No individual user bindings, ever. Bind roles to Google Groups and let group membership do the access management:

gcloud projects add-iam-policy-binding acme-prod-compute \
  --member="group:[email protected]" \
  --role="roles/compute.networkAdmin"

One service account per workload, not a shared key everyone reuses:

gcloud iam service-accounts create api-worker \
  --project=acme-prod-compute \
  --display-name="api-worker workload identity"

GKE workload identity mapping is a later problem, don’t try to solve it in week one.

Shared VPC

Networking lives in a single Host Project. Nothing else gets its own VPC.

gcloud compute shared-vpc enable acme-shared-network

gcloud compute shared-vpc associated-projects add acme-prod-compute \
  --host-project=acme-shared-network

Three subnets per environment — public, private, data — and firewall rules only in the host project, never scattered across service projects:

gcloud compute firewall-rules create deny-all-ingress \
  --project=acme-shared-network \
  --network=prod-vpc \
  --direction=INGRESS \
  --action=DENY \
  --rules=all \
  --priority=65534

Org policies

This is GCP’s version of SCPs — set at the org, cascades down to every folder and project automatically.

gcloud resource-manager org-policies enable-enforce \
  constraints/compute.vmExternalIpAccess --organization=123456789012

gcloud resource-manager org-policies enable-enforce \
  constraints/compute.requireOsLogin --organization=123456789012

Region restriction needs a list constraint, so it’s a YAML policy rather than a one-liner:

name: organizations/123456789012/policies/gcp.resourceLocations
spec:
  rules:
    - values:
        allowedValues:
          - in:europe-west1-locations
gcloud org-policies set-policy region-restriction.yaml

VPC Service Controls

The part AWS doesn’t have an equivalent for. Perimeters block API-level exfiltration even when IAM would otherwise allow it — nobody moves data out of the prod bucket to their personal GCS just because they have storage.objectViewer.

gcloud access-context-manager perimeters create prod_perimeter \
  --title="Prod perimeter" \
  --resources=projects/111111 \
  --restricted-services=storage.googleapis.com,sqladmin.googleapis.com,compute.googleapis.com,container.googleapis.com \
  --policy=ACCESS_POLICY_ID \
  --perimeter-type=PERIMETER_TYPE_REGULAR \
  --dry-run

Watch the dry-run violations for a few days before you flip it to enforce, or you’ll break something a service quietly depended on:

gcloud access-context-manager perimeters dry-run enforce prod_perimeter \
  --policy=ACCESS_POLICY_ID

Budget alerts

Per-project budgets are less hassle than trying to filter by label later:

gcloud billing budgets create \
  --billing-account=BILLING_ACCOUNT_ID \
  --display-name="acme-prod-compute budget" \
  --budget-amount=5000USD \
  --threshold-rule=percent=0.5 \
  --threshold-rule=percent=0.9 \
  --threshold-rule=percent=1.0

Turn on anomaly detection too. A misconfigured Cloud Run loop will find your billing account faster than any of the above will stop it.

Terraform comes after all of this exists, not before — you’re encoding a structure you already decided on, not discovering it as you write .tf files.