Google Cloud · Terraform example

Terraform Example: Cloud Run with Cloud SQL PostgreSQL on Google Cloud

A complete Terraform example for Google Cloud: a Cloud Run service connected to Cloud SQL PostgreSQL, with the database password in Secret Manager and a dedicated service account.

Updated Nov 2026

This example deploys a containerized app to Google Cloud Run, connected to a Cloud SQL PostgreSQL database. Cloud Run scales from zero to ten instances with traffic, and the database password never appears in the service definition.

What it creates

ResourcePurpose
google_project_serviceTurns on the Cloud Run, Cloud SQL Admin, and Secret Manager APIs
google_sql_database_instancePostgreSQL 16 with backups, point-in-time recovery, and deletion protection
google_sql_database, google_sql_userThe app's database and user
random_passwordGenerates the database password
google_secret_manager_secret and versionStores the password
google_service_account and IAM bindingsThe service's own identity, with access to Cloud SQL and this one secret
google_cloud_run_v2_serviceThe app, with the database socket mounted and the password injected from Secret Manager
google_cloud_run_v2_service_iam_memberAllows public access to the service

The Terraform

Save as main.tf. It works unchanged with OpenTofu.

terraform {
  required_version = ">= 1.5"
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 6.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.6"
    }
  }
}

provider "google" {
  project = var.project_id
  region  = var.region
}

variable "project_id" {
  type = string
}

variable "region" {
  type    = string
  default = "us-central1"
}

variable "name" {
  type    = string
  default = "web"
}

variable "image" {
  type        = string
  description = "Container image in Artifact Registry."
}

resource "google_project_service" "apis" {
  for_each           = toset(["run.googleapis.com", "sqladmin.googleapis.com", "secretmanager.googleapis.com"])
  service            = each.value
  disable_on_destroy = false
}

resource "google_sql_database_instance" "db" {
  name             = "${var.name}-db"
  database_version = "POSTGRES_16"
  region           = var.region

  settings {
    tier = "db-custom-1-3840"

    backup_configuration {
      enabled                        = true
      point_in_time_recovery_enabled = true
    }
  }

  deletion_protection = true
  depends_on          = [google_project_service.apis]
}

resource "google_sql_database" "app" {
  name     = "app"
  instance = google_sql_database_instance.db.name
}

resource "random_password" "db" {
  length  = 32
  special = false
}

resource "google_sql_user" "app" {
  name     = "app"
  instance = google_sql_database_instance.db.name
  password = random_password.db.result
}

# Store the password in Secret Manager, not in the service definition.
resource "google_secret_manager_secret" "db_password" {
  secret_id = "${var.name}-db-password"
  replication {
    auto {}
  }
  depends_on = [google_project_service.apis]
}

resource "google_secret_manager_secret_version" "db_password" {
  secret      = google_secret_manager_secret.db_password.id
  secret_data = random_password.db.result
}

# Dedicated identity for the service.
resource "google_service_account" "run" {
  account_id = "${var.name}-run"
}

resource "google_project_iam_member" "sql_client" {
  project = var.project_id
  role    = "roles/cloudsql.client"
  member  = "serviceAccount:${google_service_account.run.email}"
}

resource "google_secret_manager_secret_iam_member" "db_password" {
  secret_id = google_secret_manager_secret.db_password.id
  role      = "roles/secretmanager.secretAccessor"
  member    = "serviceAccount:${google_service_account.run.email}"
}

resource "google_cloud_run_v2_service" "web" {
  name     = var.name
  location = var.region

  template {
    service_account = google_service_account.run.email

    scaling {
      min_instance_count = 0
      max_instance_count = 10
    }

    volumes {
      name = "cloudsql"
      cloud_sql_instance {
        instances = [google_sql_database_instance.db.connection_name]
      }
    }

    containers {
      image = var.image

      env {
        name  = "DB_HOST"
        value = "/cloudsql/${google_sql_database_instance.db.connection_name}"
      }
      env {
        name  = "DB_NAME"
        value = google_sql_database.app.name
      }
      env {
        name  = "DB_USER"
        value = google_sql_user.app.name
      }
      env {
        name = "DB_PASSWORD"
        value_source {
          secret_key_ref {
            secret  = google_secret_manager_secret.db_password.secret_id
            version = "latest"
          }
        }
      }

      volume_mounts {
        name       = "cloudsql"
        mount_path = "/cloudsql"
      }
    }
  }

  depends_on = [
    google_project_iam_member.sql_client,
    google_secret_manager_secret_iam_member.db_password,
    google_secret_manager_secret_version.db_password,
  ]
}

# Public website: allow unauthenticated requests.
resource "google_cloud_run_v2_service_iam_member" "public" {
  name     = google_cloud_run_v2_service.web.name
  location = google_cloud_run_v2_service.web.location
  role     = "roles/run.invoker"
  member   = "allUsers"
}

output "url" {
  value = google_cloud_run_v2_service.web.uri
}

Deploy it

terraform init
terraform plan \
  -var="project_id=my-project" \
  -var="image=us-central1-docker.pkg.dev/my-project/app/web:1.0.0" \
  -out=tfplan
terraform apply tfplan
curl "$(terraform output -raw url)"

Creating the Cloud SQL instance takes several minutes. Your container must listen on the port in the PORT environment variable (8080 by default).

Choices this example makes

  • A dedicated service account. Cloud Run otherwise uses the default compute service account, which usually has broad project permissions.
  • Secret Manager for the password. The service reads it at startup, and only this service account can access that one secret.
  • Deletion protection on the database. terraform destroy fails until you set deletion_protection = false. That is deliberate.
  • Scale to zero. No instances run when there is no traffic. Set min_instance_count = 1 if cold starts matter.
  • Public service. The last resource lets anyone call the service. Remove it for internal services and grant roles/run.invoker to specific callers instead.

Cost

Cloud SQL is the main fixed cost, because the instance runs all the time. Cloud Run charges for request time and has a free tier, so a quiet service costs little. Check current pricing for the instance tier and region you choose.

Before production

  • Use a private IP for Cloud SQL and a Serverless VPC connector or Direct VPC egress if your policy requires no public database IP.
  • Map a custom domain with a load balancer or Cloud Run domain mapping.
  • Store state in a GCS bucket with versioning and restricted access, since it contains the password.
  • Review every plan that touches the database. A change to some settings replaces the instance. Approval workflows can require a second reviewer for those plans.

FAQ

How does Cloud Run connect to Cloud SQL here?

Through the built-in Cloud SQL connection. The cloud_sql_instance volume mounts a Unix socket at /cloudsql, and the app connects to /cloudsql/PROJECT:REGION:INSTANCE. The service account needs the Cloud SQL Client role.

Is the database password stored in Terraform state?

Yes. random_password and the secret version are both recorded in state, so state must be stored in a private, encrypted backend with restricted access. To keep it out of state entirely, use IAM database authentication instead of a password.

Should I use google_cloud_run_service or google_cloud_run_v2_service?

Use google_cloud_run_v2_service for new work. It maps directly to the Cloud Run Admin API v2 and has a simpler schema. The older resource uses the Knative-style v1 API.

Platform evaluation

See OrchesTerra on your infrastructure

Request access to generate architecture from a repository, review the plan, and run governed reconciliation across AWS, Azure, GCP, and OCI.