A Managed Service End-to-End
This topic wires one managed service the way it actually ships — Cloud Run v2 for the Hatch app frontend — with everything a real deployment carries: its own runtime service account, private connectivity to Cloud SQL, deletion_protection, and the IAM that lets it talk to the rest of the org. The point is the wiring, not the single resource.
A google_cloud_run_v2_service is three lines; a production one is the service account, the secret access, the VPC egress, and the database connection that surround it. Everything Chapter 9 built so far — the subnet, the firewall, the private connectivity — converges here into one coherent stack, which is what "deploy the app" actually means in Terraform.
The Service and Its Runtime Identity
The google_cloud_run_v2_service runs the Hatch app container. You create a dedicated google_service_account — hatch-app-run@hatch-app-prod — and set it as the service's service_account, so the app runs as a least-privileged identity instead of the broadly-privileged default compute SA. A compromise of the app then inherits only what you granted that one SA, not the wide permissions the default account carries.
This is the first and most important piece of wiring. The default compute service account exists for convenience and is the wrong choice for anything real; a purpose-built runtime SA is the difference between blast radius measured in one secret and blast radius measured across the whole project.
resource "google_service_account" "run" { project = "hatch-app-prod" account_id = "hatch-app-run" display_name = "Hatch frontend runtime" } resource "google_cloud_run_v2_service" "frontend" { project = "hatch-app-prod" name = "hatch-frontend" location = "us-central1" template { service_account = google_service_account.run.email # least-privileged, not the default compute SA vpc_access { network_interfaces { subnetwork = "projects/hatch-net-host/regions/us-central1/subnetworks/app-uc1" } egress = "PRIVATE_RANGES_ONLY" # outbound to the VPC, reaches Cloud SQL privately } } }
Private Egress to the VPC
The service's vpc_access — with a connector or direct VPC egress — puts its outbound traffic onto hatch-net-host's subnet. That is what lets a serverless product reach Cloud SQL's private IP and respect the no-external-IP posture from Topic 59. Without it, Cloud Run egresses to the public internet and cannot see the private database at all.
This is the bridge between the managed service and the private network the chapter built. Cloud Run by default has no presence in your VPC; the vpc_access block is what gives it one, and it is required for the private Cloud SQL path to work.
Connecting to Cloud SQL Privately
The app reaches the google_sql_database_instance over its private IP — the Private Service Access path from Topic 59 — and the runtime SA holds roles/cloudsql.client. Nothing about this connection touches a public endpoint: the egress goes through the VPC, the database answers on its private address, and the SA's grant authorizes the connection.
Forget the roles/cloudsql.client grant on the runtime SA and the service deploys cleanly but fails at runtime the moment it tries to reach the database — a class of bug that passes plan and apply and only shows up when real traffic hits it. The IAM grant is as load-bearing as the network path.
deletion_protection
Both the Cloud Run service and, especially, the Cloud SQL instance set deletion_protection = true. A stray terraform destroy or an aggressive refactor then cannot delete the database holding Hatch's data without a deliberate two-step removal — first disabling protection, then destroying. It is the cheap insurance you set once and are grateful for the day someone runs destroy in the wrong directory.
Set deletion_protection = false on a production database and you are one careless command away from losing it — Cloud SQL defaults the flag to true for exactly this reason, so never flip it off to make a destroy convenient. Set protection explicitly on every stateful resource, not just the obvious one, so a destroy can never silently take data with it.
Tying Back to Hatch
The frontend service, its runtime SA, its secret access (Topic 62), its private DB connection, and the firewall and subnet it egresses through are one coherent stack — not five unrelated resources. This is what deploying the app means in Terraform, and it is the sum of the chapter's earlier pieces assembled around a single service.
The lesson generalizes past Cloud Run. Any managed service in production is the resource plus its identity plus its connectivity plus its IAM; the resource block alone is a toy. When you deploy the next managed service, you wire the same four things around it.
Cloud Run v2 — scales to zero, needs no instance management, and charges per request. Choose it for the stateless HTTP frontend, which is exactly what Hatch's frontend is.
Compute Engine MIG — gives you the full VM, persistent local state, and any runtime. Choose it when the workload needs a daemon, GPUs, or software Cloud Run cannot host. For Hatch's frontend, Cloud Run is the default and the MIG is the exception.
- Running the service as the default compute service account, which is broadly privileged, instead of a dedicated least-privileged SA — a compromise of the app then inherits far more than it needs.
- Setting
deletion_protection = falseon the Cloud SQL instance to make a destroy go through, then losing the production database to a laterterraform destroyduring a refactor — leave the defaulttruein place; protection is cheap insurance. - Connecting the app to Cloud SQL over a public IP because it is simpler, defeating the private posture and tripping the no-external-IP org policy.
- Forgetting the
roles/cloudsql.clientor secret-accessor grant on the runtime SA, so the service deploys cleanly but fails at runtime when it tries to reach the database. - Hardcoding the database connection string or password into the service's env vars instead of referencing Secret Manager — the secret then lives in plan output and state (Topic 62).
- Give every managed service a dedicated runtime service account with only the roles it needs, never the default compute SA.
- Set
deletion_protection = trueon the Cloud SQL instance and any stateful resource so a destroy cannot silently take data with it. - Route the service's egress through the VPC and reach Cloud SQL over its private IP so the whole path stays internal.
- Grant the runtime SA
roles/cloudsql.clientand secret access on the specific resources, wiring the full stack rather than the service alone. - Treat every production managed service as resource plus identity plus connectivity plus IAM, not as the bare resource block.
Knowledge Check
Why give the Cloud Run service a dedicated runtime SA instead of the default compute service account?
- The default SA is broadly privileged, so a compromise of the app would inherit far more than it needs; a dedicated SA limits the blast radius
- The default compute SA cannot be attached to a Cloud Run revision at all, so a dedicated runtime account is the only way the service will deploy
- A dedicated SA makes the service deploy faster
- Cloud Run bills extra when using the default account
The service deploys and applies cleanly but fails when it tries to query the database at runtime. What is the likely cause?
- The runtime SA was never granted
roles/cloudsql.client, which plan and apply do not catch - The Cloud SQL instance has
deletion_protectionenabled - Cloud Run cannot open a connection to Cloud SQL under any configuration, so the query is bound to fail no matter how the IAM is set up
- The service is missing a public IP
What does deletion_protection = true on the Cloud SQL instance guard against?
- A stray
terraform destroyor refactor deleting the production database without a deliberate two-step removal - An attacker reading the database over the network by sniffing the private connection between Cloud Run and the Cloud SQL instance
- The autoscaler scaling the database to zero
- Schema changes being applied without review
Why does the Cloud Run service need a vpc_access block to reach Cloud SQL?
- It puts the service's egress on the host subnet so it can reach Cloud SQL's private IP, respecting the no-external-IP posture
- It is what grants the runtime SA the cloudsql.client role
- Cloud Run requires it to scale to zero
- It assigns the service its own public IP and routes the database connection out through that address rather than over the private subnet
You got correct