Terraform GCP Cloud Run service — and run.invoker IAM
A Cloud Run URL is not public until something holds roles/run.invoker.
<!-- hal:authoritative:yaml -->
*A Cloud Run URL is not a public API until something holds roles/run.invoker. Declare the service. Bind invokers on purpose.*
§I - Frame
09-20 filtered AWS ENI paths with separate security group rules. 09-17 wrote Azure passwords through write-only arguments. 09-14 imported a GCS bucket and moved its address. Those fires stay filed.
Today the cloud referent is GCP serverless HTTP. The concrete objects are google_cloud_run_v2_service plus google_cloud_run_v2_service_iam_member with role = "roles/run.invoker". Bootcamp PCA notes treat Cloud Run as container-shaped serverless HTTP, not a stateful VM substitute. The ACE/PCA cheatsheet maps it to serverless containers, HTTP-triggered.
PGA (08-30), Cloud SQL settings nests (08-24), and GKE Workload Identity (08-15) stay on their shelves. Tonight is service identity plus who may invoke.
§II - Three shelves
| Shelf | What it is | What it is not |
|---|---|---|
| Cloud Run service | Revisioned container listening on a port; URL minted by the platform | A GCE VM you SSH into |
roles/run.invoker | IAM permission to invoke the service endpoint | Editor on the project, or "the URL is enough" |
| Public invoker | Explicit member allUsers or allAuthenticatedUsers on run.invoker | The silent default of a fresh service you forgot to lock |
Serverless VPC Access (PCA notes Private Service Stuffs adjacency) is how a Cloud Run revision reaches VPC resources without a public IP on the workload. Keep that connector for a later fire. Tonight the claim is invoke IAM, not private egress.
Artifact Registry holds image bits. The service references an image URI. Do not collapse "image exists" into "service is callable."
§III - Mechanism
resource "google_cloud_run_v2_service" "api" {
name = "api"
location = var.region
ingress = "INGRESS_TRAFFIC_ALL"
template {
containers {
image = "${var.region}-docker.pkg.dev/${var.project}/apps/api:1.0.0"
ports {
container_port = 8080
}
}
}
}
# Intended callers only. Do not add allUsers unless the ticket says public.
resource "google_cloud_run_v2_service_iam_member" "ci_invoker" {
project = var.project
location = google_cloud_run_v2_service.api.location
name = google_cloud_run_v2_service.api.name
role = "roles/run.invoker"
member = "serviceAccount:${var.ci_runner_sa}"
}
Fact one. Creating the service does not grant the world invoke rights. Unauthenticated callers get 403 until an invoker binding exists for them.
Fact two. allUsers plus roles/run.invoker is the public-HTTP claim. If that member is absent, treat the service as private-to-IAM even when ingress allows traffic to reach the frontend.
Fact three. Plan should show the IAM member as its own address. Binding CI (or a gateway SA) is a different row from binding allUsers. Reviewers read those rows separately on purpose.
Fact four. ingress controls where traffic may arrive (all / internal / internal-and-cloud-load-balancing). IAM controls who may successfully invoke after arrival. Mixing those two shelves is a common exam and PR trap.
§III.b - Public versus private in the plan
A reviewer reading plan output should be able to answer three questions without opening the Console:
- Does a
google_cloud_run_v2_serviceaddress exist for the named service? - Which IAM member addresses grant
roles/run.invoker? - Is
allUsersorallAuthenticatedUsersamong those members?
If question three is yes and the ticket did not ask for a public API, that is a ship-stop. If question three is no and the ticket asked for public HTTP, that is also a ship-stop. Public is intentional, not habitual.
When Maghrib later writes quiz items, prefer stems that separate ingress from invoker IAM. A service with INGRESS_TRAFFIC_ALL and no public invoker is still private-to-identity. A service with internal ingress and allUsers as invoker is a confused shelf pairing. Score the shelves separately.
For CI, prefer a dedicated runner service account as invoker over personal user principals. Rotate the SA without rewriting the service resource. That is why the IAM member is its own address.
Do not expand this fire into Serverless VPC Access connectors, Cloud Load Balancing serverless NEGs, or Artifact Registry repository IAM. Those are real GCP shelves and they are not tonight's claim graph. PCA notes already name Serverless VPC Access beside Cloud Run; leave that adjacency for a later GCP TF Ops visit.
§IV - Ops drill
- Declare a throwaway
google_cloud_run_v2_servicepointing at a known Artifact Registry image (or a Cloud Run hello sample URI in a lab project). - Apply without any invoker member. Curl the service URL unauthenticated; expect deny.
- Add
google_cloud_run_v2_service_iam_memberfor your user or a CI SA withroles/run.invoker. Re-invoke with identity; expect allow. - Optional contrast only: temporarily bind
allUsersas run.invoker, confirm public 200, then destroy that member in the same PR. Leave public off by default. - Read plan: service resource and IAM member are distinct addresses. Confirm no leftover public member after cleanup.
Success criteria: unauthenticated deny without invoker; authenticated allow with the intended member; no leftover allUsers binding unless the ticket documents a public API; plan names both addresses.
§V - Close instruction
Apply the drill in a lab project. Paste the plan summary that names both the service and the invoker member. Pair: Dev censuses terraform providers schema -json for Google provider attribute inventory; Cert names data sources and the read-only edge (depends_on discipline) on Associate objectives. Maghrib owns quiz.html later.
Related
- Prior TF Ops: AWS SG vs NACL (09-20)
- Dev: providers schema JSON census
- Cert: data sources and depends_on
- Bootcamp: GCP PCA Notes (Cloud Run)