Updated Nov 5, 20266 min read

Cloud Run with Terraform: Services, IAM, Secrets and Cloud SQL

How to deploy a Cloud Run service with Terraform using google_cloud_run_v2_service: the container, scaling, service account, public access, Secret Manager, VPC egress and Cloud SQL, plus how to handle image updates from CI and when to skip the Terraform.

Terraform is a common way to manage Cloud Run when the rest of a GCP project already lives in code. The google_cloud_run_v2_service resource covers the service itself, and a production deployment also needs a service account, IAM bindings, secrets, a database connection and a plan for how CI rolls out new images without fighting Terraform.

This guide walks through a complete configuration for a backend service, explains the choices in each block, and covers how Encore (infrastructure from code) deploys to Cloud Run when you would rather not maintain that configuration.

What a Cloud Run deployment needs

PieceTerraform resourcePurpose
Servicegoogle_cloud_run_v2_serviceContainer, scaling, env vars, networking
Service accountgoogle_service_accountIdentity the container runs as
Public accessgoogle_cloud_run_v2_service_iam_memberroles/run.invoker for allUsers
Secretsgoogle_secret_manager_secret and IAMValues injected as env vars
Image registrygoogle_artifact_registry_repositoryWhere CI pushes images
Databasegoogle_sql_database_instanceCloud SQL connected through a volume

The service account and registry

Cloud Run services run as the Compute Engine default service account unless you set one, and that account has broad project permissions. A dedicated account per service is the safer default:

resource "google_service_account" "api" { account_id = "api-runtime" display_name = "API Cloud Run runtime" } resource "google_artifact_registry_repository" "app" { location = "europe-west1" repository_id = "app" format = "DOCKER" } resource "google_secret_manager_secret" "stripe" { secret_id = "stripe-key" replication { auto {} } } resource "google_secret_manager_secret_iam_member" "api_stripe" { secret_id = google_secret_manager_secret.stripe.id role = "roles/secretmanager.secretAccessor" member = "serviceAccount:${google_service_account.api.email}" }

The secret value itself is added as a secret version outside this configuration, so it does not end up in Terraform state. Artifact Registry replaces the older Container Registry for new projects.

The Cloud Run service

The service block sets the image, resources, scaling, the secret as an environment variable, Direct VPC egress for private resources and a Cloud SQL volume:

resource "google_cloud_run_v2_service" "api" { name = "api" location = "europe-west1" ingress = "INGRESS_TRAFFIC_ALL" deletion_protection = true template { service_account = google_service_account.api.email scaling { min_instance_count = 0 max_instance_count = 10 } vpc_access { network_interfaces { network = "default" subnetwork = "default" } egress = "PRIVATE_RANGES_ONLY" } volumes { name = "cloudsql" cloud_sql_instance { instances = [google_sql_database_instance.main.connection_name] } } containers { image = "europe-west1-docker.pkg.dev/my-project/app/api:v1" ports { container_port = 8080 } resources { limits = { cpu = "1", memory = "512Mi" } } env { name = "STRIPE_KEY" value_source { secret_key_ref { secret = google_secret_manager_secret.stripe.secret_id version = "latest" } } } volume_mounts { name = "cloudsql" mount_path = "/cloudsql" } } } lifecycle { ignore_changes = [template[0].containers[0].image] } }

Direct VPC egress places instances on a VPC subnet without a Serverless VPC Access connector, which is how the service reaches Memorystore or a private Cloud SQL IP. The Cloud SQL volume exposes a Unix socket under /cloudsql, as described in connecting from Cloud Run, and the service account also needs the Cloud SQL Client role. Secrets referenced with version = "latest" are resolved when an instance starts, as covered in the Cloud Run secrets guide.

Public access is a separate IAM binding:

resource "google_cloud_run_v2_service_iam_member" "public" { name = google_cloud_run_v2_service.api.name location = google_cloud_run_v2_service.api.location role = "roles/run.invoker" member = "allUsers" }

Image updates from CI

The lifecycle block above hands image rollouts to CI, which builds, pushes and runs gcloud run deploy api --image ... on each merge, while Terraform owns everything else. The alternative is an image variable that CI passes to terraform apply, which keeps one source of truth at the cost of running Terraform on every deploy. Either works, and mixing them without ignore_changes makes each apply revert to the image in the file.

What maintaining it involves

The configuration above covers one service, and a second service repeats the service account, IAM bindings and service block, and each environment needs its own project or naming, state, Cloud SQL instance, secrets and a CI pipeline that builds the image. Local development is separate again, usually Docker Compose with a Postgres container and a .env file that mirrors the Secret Manager values.

Cloud Run from application code with Encore

Encore

Encore is infrastructure from code for TypeScript and Go, with a platform that provisions and deploys into your own AWS or GCP account. On GCP you choose Cloud Run or GKE for an environment, and Encore creates a dedicated GCP project per environment with a private VPC, a container registry, Cloud Run services with zero-downtime rollouts, Cloud SQL databases, Secret Manager secrets and a service account per deployment, as described in the GCP infrastructure docs.

The Infrastructure page in the Encore dashboard with a database's configuration panel open, showing fields for vCPUs, memory, storage, availability type, max connections and backups.
Each provisioned resource can be reviewed and its configuration edited per environment in the Encore dashboard. The screenshot shows a GCP Cloud SQL database.

Set up the app

Get started
brew install encoredev/tap/encore &&
encore app create
Copy

Run in your terminal to get started locally.

~
$ encore app create payments --example=ts/empty$ cd payments && mkdir checkout$ encore secret set --type dev,local StripeKey

The service, its database and its secret are declared in TypeScript, replacing the service block, Cloud SQL volume, secret resources and IAM bindings above:

// checkout/encore.service.ts import { Service } from "encore.dev/service"; export default new Service("checkout");
// checkout/checkout.ts import { api } from "encore.dev/api"; import { secret } from "encore.dev/config"; import { SQLDatabase } from "encore.dev/storage/sqldb"; const stripeKey = secret("StripeKey"); const db = new SQLDatabase("checkout", { migrations: "./migrations" }); export const start = api( { expose: true, method: "POST", path: "/checkout" }, async ({ cartId }: { cartId: string }): Promise<{ sessionId: number }> => { const row = await db.queryRow<{ id: number }>` INSERT INTO sessions (cart_id) VALUES (${cartId}) RETURNING id`; // Call the payment provider with stripeKey() here. return { sessionId: row!.id }; }, );
-- checkout/migrations/1_create_sessions.up.sql CREATE TABLE sessions (id BIGSERIAL PRIMARY KEY, cart_id TEXT NOT NULL);

encore run starts Postgres locally and provides the local secret value, so there is no Compose file or .env to keep in sync. The secret value lives in Encore and in Secret Manager in the cloud, never in the repository, so AI coding agents working in the repo cannot read or leak it, as covered in the secrets docs. On push, Encore builds the image and rolls out the Cloud Run service, and an existing Cloud SQL instance can be used instead with import a Cloud SQL instance.

When an agent adds a second service, it is another folder with an encore.service.ts, and the Cloud Run deployment for it comes with the same push.

Connecting the GCP project is a one-time step covered in the own-cloud deployment guide.

Best fit: TypeScript and Go teams who want their backend on Cloud Run in their own GCP project, deployed from the application code for each service and environment.

Good to know

  • deletion_protection defaults to true on the v2 resource in recent provider versions, so set it deliberately before a terraform destroy.
  • Pin max_instance_count when the service talks to Cloud SQL, since each instance holds its own connection pool.
  • Keep secret versions out of Terraform so values never land in state.

Frequently asked questions

Which Terraform resource should I use for Cloud Run?

Use google_cloud_run_v2_service, which maps to the Cloud Run Admin API v2. The older google_cloud_run_service resource uses the v1 Knative-style API, and new configurations should use the v2 resource.

How do I make a Cloud Run service public with Terraform?

Add a google_cloud_run_v2_service_iam_member resource that grants roles/run.invoker to allUsers on the service. Without it, every request needs an identity token from a principal with the invoker role.

How do I stop Terraform from reverting the image deployed by CI?

Either have CI update the image through Terraform with a variable, or add a lifecycle block with ignore_changes on template[0].containers[0].image and let CI deploy new revisions with gcloud run deploy. Mixing both without ignore_changes makes every terraform apply roll back to the old image.

How do I deploy a TypeScript backend to Cloud Run without Terraform?

With Encore, write the service with api() from encore.dev/api and declare resources such as new SQLDatabase() and secret() in code. Connect your GCP project and choose Cloud Run for the environment, and on git push Encore builds the image and creates the Cloud Run service, Cloud SQL database and service account in your project.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders