โ† GCP Landing Zone Builder

Setup guide

Everything you must provision before this skill can run โ€” the exact credentials, permissions, and where to click. Links go to the official consoles and docs, which stay current.

Fiddly setup โ€” follow closelyโ‰ˆ 60-120 min once the organization node exists (add 1-3 business days if the Cloud Identity domain still needs DNS verification)gcp

You need an existing Google Cloud organization node (a verified Cloud Identity or Workspace domain), org-level IAM roles on the caller, an open billing account, and a hand-created 'seed' project holding the Terraform state bucket and a Workload Identity Federation pool for CI โ€” the skill refuses to guess org IDs, billing IDs, or CIDRs and org policy blocks service account keys, so CI must be federated rather than key-based.

You need to already have

  • A Google Cloud organization node already exists โ€” verify with `gcloud organizations list`. If projects sit under 'No organization', you must first sign up for Cloud Identity Free or Workspace, verify the DNS domain, and migrate projects; that is separate work the skill will not fake.
  • The caller is an Organization Administrator (roles/resourcemanager.organizationAdmin) on the org node, or can get an org admin to grant the role set below. On a brand-new org the initial Cloud Identity super admin must claim roles/resourcemanager.organizationAdmin at Cloud Console > IAM & Admin > IAM with the organization selected in the resource picker.
  • An open, non-closed Cloud Billing account the caller can link projects to (roles/billing.admin on the billing account, or roles/billing.user plus roles/resourcemanager.projectBillingManager).
  • Google Cloud CLI 480.0.0 or newer installed and authenticated (`gcloud auth login` and `gcloud auth application-default login`), with an ADC quota project set โ€” org-level APIs bill quota to a project.
  • Terraform >= 1.9 with the hashicorp/google provider pinned to ~> 6.0.
  • Google Groups already created in Cloud Identity for every role binding (the skill binds groups only, never individual users) โ€” and created BEFORE iam.allowedPolicyMemberDomains is enforced, or you lock out your own automation.
  • A hand-created seed/bootstrap project (Terraform cannot create its own state backend) linked to billing, with a versioned GCS state bucket using uniform bucket-level access and public access prevention.
  • Cloud Identity customer ID on hand for the iam.allowedPolicyMemberDomains constraint: `gcloud organizations describe ORG_ID --format='value(owner.directoryCustomerId)'`.
  • Approved region list, Google Group naming scheme, non-overlapping on-prem/peer CIDRs from the network team, and the monthly budget target from finance.

How the pieces connect

Credentials this skill needs

Set these as environment variables. Never paste secrets into a chat or commit them.

VariableWhat it isWhere to get it
ORG_ID
Google Cloud organization ID
format: 12-digit numeric string, e.g. 123456789012 (no 'organizations/' prefix)
Run `gcloud organizations list` and read the ID column, or Cloud Console > IAM & Admin > Settings with the organization selected in the top resource picker (Organization ID field).
BILLING_ID
Cloud Billing account ID
format: XXXXXX-XXXXXX-XXXXXX (three 6-char uppercase hex groups)
Run `gcloud billing accounts list` and read ACCOUNT_ID, or Cloud Console > Billing > Manage billing accounts > click the account > the ID is in the header and in the URL as ?billingAccount=.
SEED
Seed / bootstrap project ID
format: 6-30 char lowercase project ID, e.g. acme-seed-prod (project ID, not project number, not display name)
You create it: Cloud Console > IAM & Admin > Manage Resources > CREATE PROJECT, parent = the organization. Or `gcloud projects create "$SEED" --organization="$ORG_ID"`. This project holds Terraform state, the WIF pool, and nothing else.
REGION
Default region for the state bucket and initial subnets
format: GCP region string, e.g. us-central1 โ€” must be inside whatever gcp.resourceLocations will allow
Pick from Cloud Console > Compute Engine > VM instances > Create (Region dropdown), or `gcloud compute regions list`. Must match the approved-regions list from the business.
HOST
optional
Shared VPC host project ID for the environment being wired
format: lowercase project ID, e.g. acme-net-host-prod
Created by the skill's Terraform (google_project.net_host); read back with `gcloud compute shared-vpc organizations list-host-projects "$ORG_ID"` or Cloud Console > VPC network > Shared VPC.
GOOGLE_APPLICATION_CREDENTIALS
secretoptional
Path to the Workload Identity Federation credential configuration file (CI only)
format: Absolute filesystem path to an external_account JSON config produced by `gcloud iam workload-identity-pools create-cred-config` โ€” NOT a service account key (org policy iam.disableServiceAccountKeyCreation blocks those)
Generate with `gcloud iam workload-identity-pools create-cred-config projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/ci/providers/github --service-account=RUNNER_SA_EMAIL --output-file=cred.json --credential-source-file=$GITHUB_OIDC_TOKEN_FILE`. Pool and provider live at Cloud Console > IAM & Admin > Workload Identity Federation. Leave unset when running interactively โ€” `gcloud auth application-default login` is enough.
TF_VAR_org_id
Terraform input: organization ID
format: Same 12-digit value as ORG_ID; Terraform reads variable "org_id" from this env var
Set from ORG_ID in your shell or CI: `export TF_VAR_org_id="$ORG_ID"`.
TF_VAR_billing_account
Terraform input: billing account ID
format: Same XXXXXX-XXXXXX-XXXXXX value as BILLING_ID; feeds google_project.billing_account
Set from BILLING_ID: `export TF_VAR_billing_account="$BILLING_ID"`.
TF_VAR_seed_project_id
Terraform input: seed project ID
format: Same lowercase project ID as SEED; used as provider billing_project (quota project) and as the BigQuery log sink destination project
Set from SEED: `export TF_VAR_seed_project_id="$SEED"`.

Permissions to grant

roles/resourcemanager.organizationAdminIAM roleadmin consent required

Read and set IAM policy on the organization node, and create the org-level bindings the landing zone depends on.

โ†ณ For read-only discovery (`gcloud organizations get-iam-policy`, asset searches) roles/resourcemanager.organizationViewer plus roles/iam.securityReviewer is enough; only the apply phase needs organizationAdmin.

roles/resourcemanager.folderCreatorIAM roleadmin consent required

Create the /prod, /nonprod, /common, /bootstrap folder hierarchy at the org node.

โ†ณ roles/resourcemanager.folderAdmin also grants delete and setIamPolicy on folders; folderCreator is the narrower choice for a pure build-out.

roles/resourcemanager.projectCreatorIAM roleadmin consent required

Create the seed project, the per-environment Shared VPC host projects, and every workload service project.

โ†ณ Grant at the folder rather than the organization once the hierarchy exists, so the CI identity cannot create projects outside its own folder.

roles/billing.adminIAM roleadmin consent required

Link new projects to the billing account, turn on the BigQuery billing export, and create budgets.

โ†ณ Split it: roles/billing.user on the billing account plus roles/resourcemanager.projectBillingManager on the folder covers linking, and roles/billing.costsManager covers budget create/update without full admin.

roles/orgpolicy.policyAdminIAM roleadmin consent required

Set and dry-run the org policy baseline (iam.disableServiceAccountKeyCreation, compute.vmExternalIpAccess, gcp.resourceLocations, storage.publicAccessPrevention, and the rest).

โ†ณ Simulation and read-back only need roles/orgpolicy.policyViewer plus roles/policysimulator.admin; hold policyAdmin only for the enforcing apply.

roles/logging.configWriterIAM roleadmin consent required

Create the aggregated organization sink with include_children=true and its exclusion filters.

โ†ณ roles/logging.viewer suffices to audit existing sinks; configWriter is only needed for create/update.

roles/compute.xpnAdminIAM roleadmin consent required

Enable each environment's project as a Shared VPC host and attach service projects to it.

โ†ณ Must be granted at the organization or folder โ€” it cannot be granted at project scope. Once hosts exist, day-2 subnet grants only need roles/compute.networkAdmin on the host project.

roles/iam.organizationRoleAdminIAM roleadmin consent required

Define org-level custom roles for the few cases where the closest predefined role carries roughly twice the permissions needed.

โ†ณ Skip entirely if you can live with predefined roles โ€” the skill's own guidance is to prefer predefined roles.

roles/iam.denyAdminIAM roleadmin consent required

Create the org-node IAM deny policy that backstops service-account-key creation, since deny is evaluated before allow and no project-level grant can override it.

โ†ณ roles/iam.denyReviewer gives read-only visibility into existing deny policies for audit work.

roles/iam.workloadIdentityPoolAdminIAM role

Create the CI workload identity pool and the GitHub OIDC provider with its mandatory attribute condition, so CI never needs a service account key.

โ†ณ Grant on the seed project only, never at the org node.

roles/serviceusage.serviceUsageAdminIAM role

Batch-enable cloudresourcemanager, cloudbilling, iam, orgpolicy, compute, logging, storage, cloudasset, and billingbudgets APIs on the seed and host projects.

โ†ณ roles/serviceusage.serviceUsageConsumer is enough at runtime once the APIs are on.

roles/storage.adminIAM role

Create the versioned Terraform state bucket with uniform bucket-level access and public access prevention.

โ†ณ Grant on the seed project only. After creation, the CI runner needs just roles/storage.objectAdmin on that single bucket to read and write state.

roles/cloudasset.viewerIAM roleadmin consent required

Export the org-wide resource baseline and run `searchAllIamPolicies` to hunt residual roles/owner bindings.

โ†ณ Grant at the org node for full-estate visibility, or at a single folder if the review is scoped to one environment.

roles/recommender.iamViewerIAM role

Read IAM policy recommendations after 60-90 days of real traffic to trim over-broad bindings.

โ†ณ roles/recommender.iamAdmin is only needed if you want to mark recommendations as claimed/succeeded from automation.

roles/bigquery.dataEditorIAM role

Granted to the log sink's returned writer_identity on the destination dataset โ€” without it the sink silently drops every record.

โ†ณ Grant on the single destination dataset via google_bigquery_dataset_iam_member, never at project scope. For a GCS destination the equivalent is roles/storage.objectCreator on the one bucket.

roles/compute.networkUserIAM role

Granted to each workload's Google Group so service-project VMs can attach to the Shared VPC subnet.

โ†ณ Always grant at SUBNET scope (`gcloud compute networks subnets add-iam-policy-binding`), never at host-project scope โ€” project scope hands the team every subnet in the VPC.

https://www.googleapis.com/auth/cloud-platformAPI scope

The OAuth scope gcloud and the Terraform google provider request for every Resource Manager, Org Policy, Billing, Compute, Logging, and Asset Inventory call in this skill.

โ†ณ https://www.googleapis.com/auth/cloud-platform.read-only covers the discovery and audit phases (org list, asset search, policy read-back) but not any create or set operation.

Step by step

  1. 1

    Confirm an organization node actually exists

    Run `gcloud organizations list`. If it returns nothing, or `gcloud projects list --format='table(projectId,parent.id,parent.type)'` shows projects with no parent, stop: you need a verified Cloud Identity or Workspace domain first, then a project migration. Record the 12-digit ID as ORG_ID and grab the Cloud Identity customer ID with `gcloud organizations describe "$ORG_ID" --format='value(owner.directoryCustomerId)'` โ€” the iam.allowedPolicyMemberDomains constraint needs it.

    Open in the console / docs โ†—
  2. 2

    Grant the caller the org-level role set

    In Cloud Console, switch the resource picker at the top of the page to the ORGANIZATION (not a project), go to IAM & Admin > IAM, click GRANT ACCESS, and add the caller's Google Group as principal with: Organization Administrator, Folder Creator, Project Creator, Organization Policy Administrator, Logs Configuration Writer, Compute Shared VPC Admin, Organization Role Administrator, Deny Admin, Cloud Asset Viewer. Only a Cloud Identity super admin can bootstrap the first Organization Administrator grant. Verify with `gcloud organizations get-iam-policy "$ORG_ID" --flatten='bindings[]' --filter='bindings.members:YOUR_PRINCIPAL' --format='value(bindings.role)'` and stop rather than half-applying if roles are missing.

    Open in the console / docs โ†—
  3. 3

    Grant billing access and note the billing account ID

    Cloud Console > Billing > Manage billing accounts > select the account > ACCOUNT MANAGEMENT tab > ADD PRINCIPAL, and grant Billing Account Administrator (or Billing Account User + Project Billing Manager on the folder, plus Billing Account Costs Manager for budgets). Copy the ACCOUNT_ID from `gcloud billing accounts list` into BILLING_ID and confirm OPEN is True โ€” a closed account fails every project link.

    Open in the console / docs โ†—
  4. 4

    Create the seed project by hand and enable APIs

    Terraform cannot create its own state backend, so this one project is manual: `gcloud projects create "$SEED" --organization="$ORG_ID"`, then `gcloud billing projects link "$SEED" --billing-account="$BILLING_ID"`, then `gcloud services enable cloudresourcemanager.googleapis.com cloudbilling.googleapis.com iam.googleapis.com orgpolicy.googleapis.com serviceusage.googleapis.com compute.googleapis.com logging.googleapis.com storage.googleapis.com cloudasset.googleapis.com billingbudgets.googleapis.com --project="$SEED"`. Also set the ADC quota project: `gcloud auth application-default set-quota-project "$SEED"`.

    Open in the console / docs โ†—
  5. 5

    Create the versioned, locked-down Terraform state bucket

    `gcloud storage buckets create "gs://${SEED}-tfstate" --project="$SEED" --location="$REGION" --uniform-bucket-level-access --public-access-prevention` then `gcloud storage buckets update "gs://${SEED}-tfstate" --versioning`. Use one bucket with a separate `prefix` per Terraform layer (bootstrap / org / network / projects) so one bad apply cannot take out the hierarchy.

    Open in the console / docs โ†—
  6. 6

    Federate CI with Workload Identity Federation instead of a key

    Create the pool and the GitHub OIDC provider on the seed project, then bind the runner service account. The `--attribute-condition` is MANDATORY when you map `assertion.repository` โ€” without it any repository on token.actions.githubusercontent.com can assume the identity: `gcloud iam workload-identity-pools create ci --location=global --project="$SEED"` then `gcloud iam workload-identity-pools providers create-oidc github --workload-identity-pool=ci --location=global --project="$SEED" --issuer-uri="https://token.actions.githubusercontent.com" --attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository" --attribute-condition="assertion.repository=='ACME/gcp-foundation'"`, then grant the runner SA roles/iam.workloadIdentityUser for principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/ci/attribute.repository/ACME/gcp-foundation. Browse the result at IAM & Admin > Workload Identity Federation.

    Open in the console / docs โ†—
  7. 7

    Point the Terraform google provider at a quota project

    Org-level API calls (Resource Manager v3, Org Policy v2, Cloud Asset) fail with quota-project errors unless the provider block sets `billing_project = var.seed_project_id` and `user_project_override = true`, with the provider pinned `version = "~> 6.0"` โ€” field names move across majors. Export TF_VAR_org_id, TF_VAR_billing_account, and TF_VAR_seed_project_id before `terraform init`.

    Open in the console / docs โ†—
  8. 8

    Create the Google Groups BEFORE enforcing domain restriction

    Every IAM binding in this skill targets a Google Group, never a user and never a raw service account email. Create them in the Google Admin console at Directory > Groups > Create group using the agreed naming scheme (e.g. [email protected]), and get the grants in place first โ€” enforcing iam.allowedPolicyMemberDomains before the groups and automation identities exist locks you out and instantly cuts off external partners.

    Open in the console / docs โ†—
  9. 9

    Dry-run the org policy baseline before enforcing anything

    Every constraint is a potential outage. Simulate first: `gcloud policy-intelligence simulate orgpolicy --organization="$ORG_ID" --policies=policies/vm-external-ip.yaml`, then after apply confirm with `gcloud org-policies describe constraints/compute.vmExternalIpAccess --organization="$ORG_ID" --effective`. Baseline set at the org node: iam.disableServiceAccountKeyCreation, iam.automaticIamGrantsForDefaultServiceAccounts, iam.allowedPolicyMemberDomains, compute.skipDefaultNetworkCreation, compute.requireShieldedVm, compute.disableSerialPortAccess, storage.publicAccessPrevention, storage.uniformBucketLevelAccess, sql.restrictPublicIp, essentialcontacts.allowedContactDomains, gcp.resourceLocations. Grant exceptions with a child policy carrying inherit_from_parent = true plus a rule, never by deleting the parent policy.

    Open in the console / docs โ†—
  10. 10

    Turn on the BigQuery billing export and Essential Contacts

    Cloud Console > Billing > select account > Billing export > BIGQUERY EXPORT tab > EDIT SETTINGS on both 'Standard usage cost' and 'Pricing', pointing at a dataset in the seed/logging project. The export is not retroactive, so enable it on day one or you have no data for the 90-day CUD analysis. Then register Essential Contacts for the billing and security categories at IAM & Admin > Essential Contacts on the org node.

    Open in the console / docs โ†—
  11. 11

    Create budgets with a forecasted-spend rule

    `gcloud billing budgets create --billing-account="$BILLING_ID" --display-name=org-monthly --budget-amount=50000USD --threshold-rule=percent=0.5 --threshold-rule=percent=0.9 --threshold-rule=percent=1.0,basis=forecasted-spend`, repeated per environment folder. Budgets DO NOT cap spend โ€” the only hard-cap mechanism is a Pub/Sub-triggered function calling projects.updateBillingInfo with an empty billing account, which destroys running resources. Get explicit written sign-off before building that.

    Open in the console / docs โ†—
  12. 12

    Wire the aggregated log sink's writer identity

    After `google_logging_organization_sink` with include_children = true is created, grant its returned writer_identity roles/bigquery.dataEditor on the destination dataset (google_bigquery_dataset_iam_member) or roles/storage.objectCreator on the destination bucket. Miss this and the sink drops every record silently. Inspect sinks at Cloud Console > Logging > Log Router with the organization selected.

    Open in the console / docs โ†—
  13. 13

    Baseline the estate and prove no residual owner grants

    `gcloud asset export --organization="$ORG_ID" --content-type=resource --output-path="gs://${SEED}-tfstate/asset-baseline.json"` then `gcloud asset search-all-iam-policies --scope="organizations/$ORG_ID" --query='policy:roles/owner'`. Any roles/owner outside the seed project is a finding. Schedule `terraform plan` in CI for drift detection and enable Security Command Center at the org node.

    Open in the console / docs โ†—

Check it worked

gcloud organizations describe "$ORG_ID" --format='value(displayName)' && gcloud billing accounts describe "$BILLING_ID" --format='value(open)' && gcloud resource-manager folders list --organization="$ORG_ID" --format='value(displayName)' && gcloud org-policies describe constraints/compute.vmExternalIpAccess --organization="$ORG_ID" --effective && gcloud storage ls "gs://${SEED}-tfstate" && echo OK-ORG-BILLING-POLICY-STATE

If you hit an error

PERMISSION_DENIED: Permission 'resourcemanager.organizations.setIamPolicy' denied on resource '//cloudresourcemanager.googleapis.com/organizations/123456789012' (or it may not exist).

Cause: The caller holds project-level roles only; org-node bindings require roles/resourcemanager.organizationAdmin, which on a fresh org only a Cloud Identity super admin can grant to themselves first.

Fix: Have a Cloud Identity super admin open Cloud Console with the ORGANIZATION selected in the resource picker, IAM & Admin > IAM > GRANT ACCESS, and add Organization Administrator to your group. Re-check with `gcloud organizations get-iam-policy "$ORG_ID"`. Note the '(or it may not exist)' suffix is generic โ€” it usually means denied, not missing.

Your application is authenticating by using local Application Default Credentials. The cloudresourcemanager.googleapis.com API requires a quota project, which is not set by default.

Cause: ADC from `gcloud auth application-default login` has no quota project, and the Terraform google provider was not given one either.

Fix: Run `gcloud auth application-default set-quota-project "$SEED"` and set `billing_project = var.seed_project_id` with `user_project_override = true` in the provider block. This is the single most common Terraform failure on org-level resources.

Error 403: The caller does not have permission, forbidden โ€” on google_org_policy_policy

Cause: Missing roles/orgpolicy.policyAdmin, or the policy name/parent is a folder the caller only has projectCreator on.

Fix: Grant roles/orgpolicy.policyAdmin at the org node (it does not inherit downward from a project grant) and confirm the resource `parent` string is exactly `organizations/ORG_ID` or `folders/FOLDER_ID`, not a project.

FAILED_PRECONDITION: Key creation is not allowed on this service account. โ€” constraints/iam.disableServiceAccountKeyCreation

Cause: Step 5 of the skill enforces iam.disableServiceAccountKeyCreation org-wide, so any attempt to mint a JSON service account key for CI is rejected by design.

Fix: Do not create a key. Use Workload Identity Federation with `gcloud iam workload-identity-pools create-cred-config` and point GOOGLE_APPLICATION_CREDENTIALS at that external_account file. If you truly need a temporary exception, add a child org policy on the bootstrap folder with inherit_from_parent = true and an allow rule โ€” never delete the parent policy.

INVALID_ARGUMENT: The attribute condition must reference one of the provider's claims.

Cause: `gcloud iam workload-identity-pools providers create-oidc` was given an --attribute-mapping containing attribute.repository (or any non-subject attribute) without a matching --attribute-condition, which Google blocks because it would let any repo on the issuer assume the identity.

Fix: Add `--attribute-condition="assertion.repository=='ORG/REPO'"` (single quotes inside, matching a claim actually present in the mapping). See https://cloud.google.com/iam/help/workload-identity/attribute-condition.

The project to be billed is associated with an absent billing account.

Cause: The billing account is closed or the caller lost roles/billing.user, so `gcloud billing projects link` silently left the project unlinked and the next API call fails.

Fix: Confirm `gcloud billing accounts describe "$BILLING_ID" --format='value(open)'` returns True, re-run `gcloud billing projects link "$SEED" --billing-account="$BILLING_ID"`, and verify with `gcloud billing projects describe "$SEED"`.

Error 429: Quota exceeded for quota metric 'Project creation requests' and limit 'Project creation requests per minute' of service cloudresourcemanager.googleapis.com

Cause: Bulk-creating dozens of workload projects in one apply hits the per-minute project creation rate limit, and separately the billing account has a hard cap on total projects.

Fix: Request a project-creation quota increase at IAM & Admin > Quotas BEFORE the build, then throttle Terraform with `terraform apply -parallelism=5` and split project creation into its own state layer so a retry does not re-plan the org.

Request had insufficient authentication scopes. (generic::permission_denied)

Cause: Running gcloud or Terraform from a Compute Engine VM whose default service account was created with restricted OAuth scopes, so cloud-platform is not granted regardless of IAM roles.

Fix: Recreate the runner VM with `--scopes=https://www.googleapis.com/auth/cloud-platform`, or better, run CI through Workload Identity Federation instead of a VM's default service account.

Constraint constraints/gcp.resourceLocations violated for projects/... attempted location us โ€” multi-region bucket creation fails

Cause: gcp.resourceLocations was scoped by enumerating individual regions, which blocks multi-region GCS buckets, some Cloud Build workers, and global load balancer components.

Fix: Replace the enumerated region list with a value group such as `in:us-locations` in the allowed_values list, then re-run `gcloud org-policies describe constraints/gcp.resourceLocations --organization="$ORG_ID" --effective`.

Error 400: Precondition check failed., failedPrecondition โ€” on google_project or google_project_service

Cause: Usually a missing enabled API on the quota/billing project, an unlinked billing account, or the org policy compute.skipDefaultNetworkCreation interacting with auto_create_network.

Fix: Enable cloudresourcemanager.googleapis.com, cloudbilling.googleapis.com, and serviceusage.googleapis.com on the seed project, confirm billing is linked, and set `auto_create_network = false` on every google_project resource.

Error creating Sink: googleapi: Error 403: The caller does not have permission โ€” google_logging_organization_sink

Cause: Missing roles/logging.configWriter at the org node; a project-level grant does not cover organizations.sinks.create.

Fix: Grant roles/logging.configWriter on the organization. Separately, after creation grant the sink's writer_identity roles/bigquery.dataEditor on the destination dataset or the sink will accept writes and drop every record.

Error 400: Cannot enable resource as a service project: the host project has reached the maximum number of attached service projects., badRequest

Cause: The default ceiling of roughly 100 service projects per Shared VPC host project was reached.

Fix: Add a second host project for that environment or region, or request a Shared VPC quota increase at IAM & Admin > Quotas well before you approach the limit โ€” moving a host project after service projects attach is disruptive.

Official documentation: https://cloud.google.com/architecture/landing-zones โ†—

API operations this skill uses (20)

Every call the skill makes, linked to its official reference page.

OperationCallPermissionRef
Search organizations (discover the org node)
Step 1 of the procedure: confirm an organization node exists and capture ORG_ID plus the Cloud Identity directoryCustomerId before anything else runs. Paired with `gcloud resource-manager folders list --organization=ORG_ID` and `gcloud projects list --format='table(projectId,parent.id,parent.type)'` for brownfield discovery of the existing hierarchy.
POST https://cloudresourcemanager.googleapis.com/v3/organizations:search (CLI: gcloud organizations list)resourcemanager.organizations.get (roles/resourcemanager.organizationViewer)docs โ†—
Get organization IAM policy (caller role check)
Enumerate the caller's org-level roles so the skill can list what is missing and stop rather than half-applying the landing zone.
POST https://cloudresourcemanager.googleapis.com/v3/organizations/{ORG_ID}:getIamPolicy (CLI: gcloud organizations get-iam-policy ORG_ID --flatten='bindings[]')resourcemanager.organizations.getIamPolicydocs โ†—
Create folder (prod / nonprod / common / bootstrap)
Builds the environment or business-unit hierarchy that every org policy, budget, and IAM binding is anchored to.
POST https://cloudresourcemanager.googleapis.com/v3/folders body: {"displayName":"prod","parent":"organizations/{ORG_ID}"} (Terraform: google_folder)resourcemanager.folders.create (roles/resourcemanager.folderCreator)docs โ†—
Create project (seed, network host, workload service projects)
One workload equals one project, with the env/owner/cost-center/data-class label contract applied at creation because labels are the only reliable cost-attribution axis in the billing export.
POST https://cloudresourcemanager.googleapis.com/v3/projects body: {"projectId":"acme-net-host-prod","parent":"folders/{FOLDER_ID}","labels":{...}} (CLI: gcloud projects create)resourcemanager.projects.create (roles/resourcemanager.projectCreator)docs โ†—
Link project to billing account
Links the seed and every created project to billing after `gcloud billing accounts list` confirms the account is OPEN. This is also the exact call a hard-spend-cap function would abuse with an empty billingAccountName โ€” which destroys running resources and needs explicit sign-off.
PUT https://cloudbilling.googleapis.com/v1/projects/{PROJECT_ID}/billingInfo body: {"billingAccountName":"billingAccounts/{BILLING_ID}"} (CLI: gcloud billing projects link)billing.resourceAssociations.create + resourcemanager.projects.createBillingAssignment (roles/billing.user + roles/resourcemanager.projectBillingManager)docs โ†—
Batch-enable APIs on a project
Turns on resourcemanager, billing, iam, orgpolicy, compute, logging, storage, cloudasset, and billingbudgets on the seed project so every later call in the skill has an enabled API and a quota project. Paired with storage.buckets.insert (`gcloud storage buckets create --uniform-bucket-level-access --public-access-prevention`) for the hand-created, versioned Terraform state bucket, since Terraform cannot create its own backend.
POST https://serviceusage.googleapis.com/v1/projects/{PROJECT_NUMBER}/services:batchEnable (CLI: gcloud services enable cloudresourcemanager.googleapis.com orgpolicy.googleapis.com cloudasset.googleapis.com ...)serviceusage.services.enable (roles/serviceusage.serviceUsageAdmin)docs โ†—
Create the CI workload identity pool and GitHub OIDC provider
Federates CI to token.actions.githubusercontent.com with a mandatory attributeCondition pinning assertion.repository, so the pipeline never needs a service account key (org policy iam.disableServiceAccountKeyCreation blocks those) and no other repo can assume the runner identity.
POST https://iam.googleapis.com/v1/projects/{SEED}/locations/global/workloadIdentityPools?workloadIdentityPoolId=ci then .../workloadIdentityPools/ci/providers?workloadIdentityPoolProviderId=github with oidc.issuerUri, attributeMapping, attributeCondition (CLI: gcloud iam workload-identity-pools create ci --location=global; gcloud iam workload-identity-pools providers create-oidc github ...)iam.workloadIdentityPools.create + iam.workloadIdentityPoolProviders.create (roles/iam.workloadIdentityPoolAdmin)docs โ†—
Set an organization policy (constraint baseline)
Applies the org-node baseline: disableServiceAccountKeyCreation, automaticIamGrantsForDefaultServiceAccounts, allowedPolicyMemberDomains, skipDefaultNetworkCreation, requireShieldedVm, disableSerialPortAccess, publicAccessPrevention, uniformBucketLevelAccess, restrictPublicIp, allowedContactDomains, resourceLocations.
POST https://orgpolicy.googleapis.com/v2/organizations/{ORG_ID}/policies body: {"name":"organizations/{ORG_ID}/policies/iam.disableServiceAccountKeyCreation","spec":{"rules":[{"enforce":true}]}} (Terraform: google_org_policy_policy)orgpolicy.policy.set (roles/orgpolicy.policyAdmin)docs โ†—
Simulate an org policy change before enforcing
Every constraint is a potential outage, so the skill dry-runs each policy against the live estate and reports would-be violations before any enforcing apply.
CLI gcloud policy-intelligence simulate orgpolicy --organization="$ORG_ID" --policies=policies/vm-external-ip.yamlpolicysimulator.orgPolicyViolationsPreviews.create + cloudasset.assets.searchAllResources (roles/policysimulator.admin + roles/cloudasset.viewer)docs โ†—
Read back the effective org policy
Confirms inheritance actually resolved as intended after child policies with inherit_from_parent = true add exceptions โ€” deleting a parent policy is never the right fix.
GET https://orgpolicy.googleapis.com/v2/organizations/{ORG_ID}/policies/compute.vmExternalIpAccess:getEffectivePolicy (CLI: gcloud org-policies describe constraints/compute.vmExternalIpAccess --organization=ORG_ID --effective)orgpolicy.policy.get (roles/orgpolicy.policyViewer)docs โ†—
Enable a project as a Shared VPC host
Creates one Shared VPC host project per environment so a prod VM can never land on a nonprod subnet, and collapses Cloud NAT gateways from per-project to per-environment-per-region.
POST https://compute.googleapis.com/compute/v1/projects/{HOST}/enableXpnHost (Terraform: google_compute_shared_vpc_host_project)compute.organizations.enableXpnHost (roles/compute.xpnAdmin at org or folder scope)docs โ†—
Attach a service project to the Shared VPC host
Attaches each workload project to its environment host. The default ceiling is roughly 100 service projects per host, so the skill plans a second host or a quota increase before approaching it.
POST https://compute.googleapis.com/compute/v1/projects/{HOST}/enableXpnResource body: {"xpnResource":{"id":"SERVICE_PROJECT_ID","type":"PROJECT"}} (Terraform: google_compute_shared_vpc_service_project)compute.organizations.enableXpnResource (roles/compute.xpnAdmin)docs โ†—
Create a subnet with sampled flow logs and secondary ranges
Allocates a /16 per region per environment, keeps GKE pod ranges out of RFC1918 space, and cuts VPC Flow Log ingestion roughly 90% โ€” the single largest logging cost lever.
POST https://compute.googleapis.com/compute/v1/projects/{HOST}/regions/{REGION}/subnetworks body includes ipCidrRange, secondaryIpRanges (GKE pods from 100.64.0.0/10), privateIpGoogleAccess, logConfig.flowSampling=0.1, logConfig.metadata=EXCLUDE_ALL_METADATA (Terraform: google_compute_subnetwork)compute.subnetworks.create (roles/compute.networkAdmin on the host project)docs โ†—
Grant networkUser at SUBNET scope
Gives each team exactly one subnet instead of the whole host project โ€” the checklist item that most often fails a review. Use additive google_compute_subnetwork_iam_member, never the authoritative _iam_policy resource.
POST https://compute.googleapis.com/compute/v1/projects/{HOST}/regions/{REGION}/subnetworks/{SUBNET}/setIamPolicy (CLI: gcloud compute networks subnets add-iam-policy-binding sn-app-us-central1-prod --region=us-central1 --project="$HOST" --member="group:[email protected]" --role=roles/compute.networkUser)compute.subnetworks.setIamPolicy (roles/compute.networkAdmin or roles/compute.xpnAdmin)docs โ†—
Create the aggregated organization log sink
Routes every child project's audit logs into one logging project with includeChildren=true, excluding healthy load balancer requests that dominate volume and prove nothing. The returned writerIdentity must then be granted write access on the destination or the sink drops everything.
POST https://logging.googleapis.com/v2/organizations/{ORG_ID}/sinks?uniqueWriterIdentity=true body: {"name":"org-audit-to-bq","destination":"bigquery.googleapis.com/projects/{SEED}/datasets/org_audit_logs","includeChildren":true,"filter":"logName:\"logs/cloudaudit.googleapis.com\"","exclusions":[...]} (Terraform: google_logging_organization_sink)logging.sinks.create on the organization (roles/logging.configWriter)docs โ†—
Create an IAM deny policy at the org node
Backstops the service-account-key org policy: deny rules are evaluated before allow rules, so no project-level grant anywhere in the hierarchy can override them.
POST https://iam.googleapis.com/v2/policies/cloudresourcemanager.googleapis.com%2Forganizations%2F{ORG_ID}/denypolicies?policyId=no-sa-keys (CLI: gcloud iam policies create no-sa-keys --kind=denypolicies --attachment-point="cloudresourcemanager.googleapis.com/organizations/$ORG_ID" --policy-file=deny.json)iam.denypolicies.create (roles/iam.denyAdmin)docs โ†—
Create billing budgets with a forecasted-spend threshold
Adds org-level and per-environment-folder budgets at 50/90/100% including a forecast rule. The skill states explicitly that budgets alert but do not cap spend.
POST https://billingbudgets.googleapis.com/v1/billingAccounts/{BILLING_ID}/budgets body includes budgetFilter (folder/project scope), amount.specifiedAmount, thresholdRules with basis=FORECASTED_SPEND (CLI: gcloud billing budgets create --threshold-rule=percent=1.0,basis=forecasted-spend)billing.budgets.create (roles/billing.admin or roles/billing.costsManager)docs โ†—
Export the Cloud Asset Inventory baseline
Captures a point-in-time snapshot of the whole estate for the hand-off package and for later drift comparison.
POST https://cloudasset.googleapis.com/v1/organizations/{ORG_ID}:exportAssets body: {"contentType":"RESOURCE","outputConfig":{"gcsDestination":{"uri":"gs://{SEED}-tfstate/asset-baseline.json"}}} (CLI: gcloud asset export --organization=ORG_ID --content-type=resource)cloudasset.assets.exportResource (roles/cloudasset.viewer at org scope)docs โ†—
Search all IAM policies for residual owner grants
Validation step: any roles/owner binding outside the seed project is a finding in the risk register, along with any binding targeting a user instead of a Google Group.
GET https://cloudasset.googleapis.com/v1/organizations/{ORG_ID}:searchAllIamPolicies?query=policy:roles/owner (CLI: gcloud asset search-all-iam-policies --scope="organizations/$ORG_ID" --query='policy:roles/owner')cloudasset.assets.searchAllIamPolicies (roles/cloudasset.viewer)docs โ†—
List IAM policy recommendations for right-sizing
Day-2 least-privilege pass: after 60-90 days of real traffic, harvest recommendations to shrink over-broad role bindings toward the permissions actually exercised.
GET https://recommender.googleapis.com/v1/projects/{PROJECT_ID}/locations/global/recommenders/google.iam.policy.Recommender/recommendations (CLI: gcloud recommender recommendations list --recommender=google.iam.policy.Recommender --location=global)recommender.iamPolicyRecommendations.list (roles/recommender.iamViewer)docs โ†—