GCP — projects, IAM inheritance, and Cloud Run
Assumes you have read: Azure — resource groups, Entra ID, and Cloud Run's closest cousin
Intuition
Section titled “Intuition”GCP’s core service vocabulary again maps onto what the last two pages already covered — Compute Engine is EC2/Azure VMs, Cloud Storage is S3/Blob Storage, Cloud SQL is RDS/Azure SQL. Two things are genuinely distinct enough to learn on their own terms: the project → folder → organization hierarchy that IAM permissions inherit down through by default, and Cloud Run, whose billing model sits at a point between Lambda’s pure per-invocation model and an always-on container.
Mechanics
Section titled “Mechanics”The resource hierarchy: organization, folders, projects, and IAM inheritance
Section titled “The resource hierarchy: organization, folders, projects, and IAM inheritance”Organization: example.com├── Folder: production│ ├── Project: orders-prod│ └── Project: billing-prod└── Folder: staging └── Project: orders-stagingEvery GCP resource belongs to a project — the closest analog to an AWS
account or Azure subscription, and the unit billing is metered against.
Projects can be grouped into folders, folders into an organization,
and IAM role bindings applied at any level inherit down to everything
below it by default: a role granted at the production folder applies to
every project inside it, and every resource inside those projects.
gcloud projects add-iam-policy-binding orders-prod \ --member="serviceAccount:orders-app@orders-prod.iam.gserviceaccount.com" \ --role="roles/storage.objectViewer"This inheritance is the GCP-specific thing worth internalizing: a grant made at the folder level is easy to make and easy to under-appreciate the reach of, because it applies to every project underneath — including ones created after the grant, with no additional step required.
Cloud Run: containers, billed like Lambda
Section titled “Cloud Run: containers, billed like Lambda”gcloud run deploy orders-api \ --image gcr.io/orders-prod/orders-api:v42 \ --region europe-west1 \ --min-instances 0 \ --max-instances 20Cloud Run runs an arbitrary container (not a function in a specific runtime,
the way Lambda requires) and bills per-request, scaling instances up and down
including to zero — the same idle-costs-nothing property as Lambda, but with
the flexibility of “any container that listens on a port” rather than a
handler function in a supported language runtime. --min-instances 1 trades
that idle-cost-nothing property away in exchange for eliminating cold starts,
which is the same tradeoff Lambda’s provisioned concurrency makes, phrased
differently.
BigQuery: the analytics warehouse with no cluster to size
Section titled “BigQuery: the analytics warehouse with no cluster to size”BigQuery is worth naming even outside a data-focused page because its billing model is distinctive: queries are billed by bytes scanned, not by a provisioned cluster size — there is no cluster to size at all. A query against a well-partitioned table that scans 2 GB costs a small, predictable amount regardless of how large the underlying table is; the same query against an unpartitioned table might scan the whole thing. This makes table partitioning and clustering a direct cost lever in a way that’s less visible in a system where compute is provisioned separately from storage.
Cost & limits
Section titled “Cost & limits”IAM inheritance means a grant’s actual reach is determined by where in the hierarchy it’s made, not just what role it grants — the same role bound at a project versus a folder versus the organization root has a reach that differs by orders of magnitude, and the binding itself looks identical in a quick review unless the scope is checked.
Cloud Run’s scale-to-zero is only free when traffic is genuinely idle — a
service with min-instances: 0 serving intermittent but never-fully-idle
traffic pays repeated cold-start latency on every gap longer than the
platform’s idle-instance retention window, the same tradeoff Lambda makes.
BigQuery cost scales with bytes scanned, so an unpartitioned table charges for scanning data a query doesn’t need — partitioning by the column most queries filter on (commonly a date) is often the single highest-leverage cost optimization available, more effective than tuning the query itself.
When NOT to use it
Section titled “When NOT to use it”Do not bind IAM roles at the organization or folder level as a default habit. It’s less typing than binding per-project, and it’s exactly the kind of grant that outlives its justification — bind at the narrowest level (ideally the resource itself, otherwise the project) unless there’s a specific reason a whole folder of projects needs the same access.
Do not choose Cloud Run for a workload that needs a guaranteed-warm,
consistently low-latency response and can’t tolerate any cold start — a
service with min-instances 0 will cold-start on the first request after an
idle gap; if that’s unacceptable, either set a nonzero minimum (paying for
the always-warm instances) or use a platform designed to stay warm by
default.
Real-world usage
Section titled “Real-world usage”Teams already using BigQuery for analytics frequently adopt the rest of GCP around it — Cloud Storage as BigQuery’s most natural ingestion source, Cloud Run or Cloud Functions for the pipelines that populate it — because keeping the analytics workload and its surrounding infrastructure on one platform avoids cross-cloud data-transfer costs and simplifies the IAM story to one hierarchy. Cloud Run specifically is a common choice for a containerized API that has genuinely variable traffic and no in-house Kubernetes operational capacity — the container flexibility of Kubernetes with none of the cluster management.
Failure modes
Section titled “Failure modes”The organization-level IAM grant nobody remembered was that broad. A role bound at the folder or organization root during initial setup, meant to be temporary, silently applies to every project created afterward too — discovered in a security review, or after an incident, as access nobody can account for.
The BigQuery bill that tracked a forgotten unpartitioned table. A table that grows large without ever being partitioned turns every query against it into a full-table scan, and the bill scales with the table’s growth even though most queries only ever wanted last week’s data — the fix (adding partitioning) is straightforward once identified, but the cost accrues undetected until someone looks at the billing breakdown by query.
The Cloud Run service that looked broken under a traffic spike. A service
scaling from zero under a sudden burst pays cold-start latency on every new
instance spinning up to meet demand, and if max-instances is set too low
for the burst, requests queue or fail — symptom: latency and error-rate
spikes that correlate with traffic spikes, easily misread as a code
regression.
Practice problems
Section titled “Practice problems”1. A role is bound at the organization root, granting roles/viewer to a
contractor’s service account. Six months later, a new project is created.
Does the contractor have access to it, and why might this be a problem?
Yes — IAM bindings at the organization level inherit down to every project underneath, including ones created after the binding, with no additional step required. This is a problem because access is granted implicitly and invisibly to future projects nobody explicitly decided the contractor should see; the fix is binding at the specific project level instead, so access has to be a deliberate decision each time.
2. A BigQuery table storing five years of event data isn’t partitioned. Most queries only need the last 7 days. What’s the cost consequence, and the fix?
Every query, regardless of the date range it actually needs, scans the whole table — since BigQuery bills by bytes scanned, this means paying to scan five years of data for a query that only needs a week. Partitioning the table by date lets a 7-day query scan only the relevant partitions, cutting the bytes scanned (and the bill) by roughly the same ratio as the date range narrows.
3. A Cloud Run service set to min-instances: 0 serves a support tool used
sporadically during business hours, with occasional multi-minute gaps between
requests. Users report the first request after a gap feels slow. Why, and
what’s one fix?
Each gap longer than the platform’s idle-instance retention triggers a cold
start on the next request — the container has to be provisioned and started
before it can serve. Setting min-instances: 1 keeps one instance warm at
all times, trading a small always-on cost for eliminating the cold-start
latency on the sporadic but time-sensitive requests.
Check yourself
A role is granted at the folder level in GCP's resource hierarchy. What does that binding apply to?
GCP’s IAM bindings inherit down the organization -> folder -> project hierarchy by default. A binding at the folder level applies to every project inside it and everything inside those projects, including projects created after the binding was made — which is exactly what makes folder- or organization-level grants easy to make and easy to under-appreciate the reach of.
Interview answers
Section titled “Interview answers”“How does GCP’s IAM model differ from AWS’s?” GCP has an explicit resource hierarchy — organization, folders, projects — and IAM bindings inherit down through it by default; AWS’s IAM is scoped to a single account unless you explicitly set up cross-account roles or an organization-wide service control policy. The caveat: this inheritance is powerful for managing access across many projects consistently, and it’s also the source of most GCP over-permissioning incidents, because a binding made at the folder level is easy to make without fully registering how far it reaches.
“When would you choose Cloud Run over Lambda or over a Kubernetes
deployment?” Cloud Run fits a containerized workload with variable traffic
that doesn’t justify the operational overhead of running Kubernetes, and
needs more flexibility than a function-runtime model like Lambda provides —
any container that listens on a port, not just a supported language handler.
The caveat: it makes the same idle-vs-cold-start tradeoff Lambda does
(min-instances: 0 is free when idle, costs cold-start latency on the first
request after a gap), so it’s not a free upgrade over either — it’s a
different point on the same tradeoff curve, chosen for the container
flexibility specifically.