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.