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.
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 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"
}
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.
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.
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.

Run in your terminal to get started locally.
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.
deletion_protection defaults to true on the v2 resource in recent provider versions, so set it deliberately before a terraform destroy.max_instance_count when the service talks to Cloud SQL, since each instance holds its own connection pool.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.
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.
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.
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.