Trust is earned, not given

A different perspective

2026-02-03 · Projects

DevOps on Google Cloud, part 6: One batch — the whole stack in Terraform

The finale of the three-series arc. MapleCart on Google Cloud — VPC, Serverless VPC connector, Cloud SQL (private IP), Memorystore, Artifact Registry, Cloud Run with identity — in one Terraform configuration. This is the newest-dated article of all three series; the provider here is google-beta-free, all stable resources.

Structure

maplecart-gcp/
├── provider.tf       # google provider + GCS state backend
├── network.tf        # VPC, subnet, Serverless VPC Access connector
├── data.tf           # Cloud SQL, Memorystore
├── compute.tf        # Artifact Registry, service account, Cloud Run
├── edge.tf           # global LB + Cloud CDN + Armor (abridged to LB essentials)
└── outputs.tf

provider.tf and network.tf

terraform {
  backend "gcs" { bucket = "maplecart-tfstate" prefix = "prod" }   # state in GCS, locked
}
provider "google" { project = "maplecart" region = "us-central1" }

resource "google_compute_network" "vpc" {
  name                    = "maplecart-vnet"
  auto_create_subnetworks = false              # explicit, always
}
resource "google_compute_subnetwork" "app" {
  name          = "app-useast1"
  region        = "us-east1"
  network       = google_compute_network.vpc.id
  ip_cidr_range = "10.0.2.0/24"
  private_ip_google_access = true              # outbound via Google APIs, no public IPs
}

# Serverless VPC Access: lets Cloud Run reach the private SQL IP
resource "google_vpc_access_connector" "connector" {
  name          = "run-connector"
  region        = "us-central1"
  network       = google_compute_network.vpc.name
  ip_cidr_range = "10.8.0.0/28"
}

data.tf — Cloud SQL and Memorystore

resource "google_sql_database_instance" "postgres" {
  name             = "maplecart-db"
  database_version = "POSTGRES_15"
  region           = "us-central1"
  deletion_protection = true                   # production: require explicit disabling

  settings {
    tier              = "db-custom-2-7680"
    availability_type = "REGIONAL"             # sync standby in another zone
    disk_autoresize   = true
    backup_configuration {
      enabled                        = true
      point_in_time_recovery_enabled = true
      start_time                     = "03:00"
    }
    ip_configuration {
      ipv4_enabled    = false                   # PRIVATE IP ONLY - the whole posture
      private_network = google_compute_network.vpc.id
    }
  }
}

resource "google_redis_instance" "cache" {
  name           = "maplecart-cache"
  tier           = "STANDARD_HA"
  memory_size_gb = 1
  region         = "us-central1"
  connect_mode   = "PRIVATE_SERVICE_ACCESS"
}

compute.tf — identity and Cloud Run

resource "google_artifact_registry_repository" "web" {
  repository_id = "web"
  format        = "DOCKER"
  location      = "us-central1"
}

resource "google_service_account" "web" { account_id = "maplecart-web" }

# Least privilege: this SA reads ONLY the db-password secret (the part-4 pattern)
resource "google_secret_manager_secret" "db_password" {
  secret_id = "maplecart-db-password"
  replication { user_managed { replicas { location = "us-central1" } } }
}
resource "google_secret_manager_secret_iam_member" "web_read" {
  secret_id = google_secret_manager_secret.db_password.id
  role      = "roles/secretmanager.secretAccessor"
  member    = "serviceAccount:${google_service_account.web.email}"
}

resource "google_cloud_run_v2_service" "web" {
  name     = "maplecart-web"
  location = "us-central1"
  ingress  = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER"   # LB-only: no direct URLs

  template {
    service_account = google_service_account.web.email
    vpc_access { connector = google_vpc_access_connector.connector.id
                 egress    = "PRIVATE_RANGES_ONLY" }     # SQL traffic stays private
    containers {
      image = "${google_artifact_registry_repository.web.location}-docker.pkg.dev/maplecart/web/web:v42"
      resources { limits = { cpu = "1", memory = "512Mi" } }
      env { name = "DB_HOST" value = google_sql_database_instance.postgres.private_ip_address }
    }
  }
}

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"                         # public site; LB + Armor still filter
}

Apply, verify, and the series takeaway

terraform init       # GCS backend
terraform plan -out plan.tfplan
terraform apply plan.tfplan    # ~10 minutes: VPC, SQL, Redis, Cloud Run, LB
gcloud run services describe maplecart-web --region us-central1 \
  --format 'value(status.url)'   # the URL, served over the global LB
terraform destroy              # the practice loop, as always

Three series, eighteen articles, one conclusion: the layered model — identity, compute, data, network, delivery, observability — is invariant. Learn it once on any cloud and the other two are vocabulary. Learn the Terraform pattern and the clouds become interchangeable targets for the same reviewed, reproducible, destroyable infrastructure. That is DevOps.