Skip to main content

GCP (Cloud Run + Cloud SQL)

A serverless-first Terraform skeleton lives at infra/terraform/gcp/. It targets Google Cloud Run for the customer-visible services (API, web, ingest), Cloud SQL for PostgreSQL 16, and Memorystore for Redis 7.2 — all private-IP, peered through a dedicated VPC.

What you get

ComponentResource
API (FastAPI)Cloud Run v2 service
Web console (Next.js)Cloud Run v2 service
Ingest gateway (Go)Cloud Run v2 service
Application databaseCloud SQL for PostgreSQL 16, private
Queues / rate limit / fan-outMemorystore Redis 7.2, private
SecretsSecret Manager (auto-generated)
Container registryArtifact Registry (Docker)
NetworkingDedicated VPC + Serverless VPC Access
IdentityOne service account per Cloud Run svc

Prerequisites

  1. A GCP project with billing attached.
  2. gcloud authenticated as a principal that can create Cloud Run services, Cloud SQL instances, Secret Manager entries, and service accounts in that project. Project Owner is the simplest grant; the least-privilege bundle is roles/run.admin + roles/iam.serviceAccountAdmin
    • roles/secretmanager.admin + roles/cloudsql.admin + roles/compute.networkAdmin.
  3. Terraform 1.5+ and the Google provider 5.40+ (the lockfile pins exact versions on first init).

Quick start

cd infra/terraform/gcp
cp terraform.tfvars.example terraform.tfvars
$EDITOR terraform.tfvars # at minimum set project_id

terraform init
terraform plan -out tfplan # review the proposed plan
terraform apply tfplan

A full apply against an empty project takes ~12 minutes — most of that is the Cloud SQL instance and the service-networking peering. After the apply finishes, the api_url, web_url, and ingest_url outputs are reachable immediately if allow_unauthenticated is true (the default).

$ terraform output
api_url = "https://aisoc-api-xxxxxxxxxx-uc.a.run.app"
web_url = "https://aisoc-web-xxxxxxxxxx-uc.a.run.app"
ingest_url = "https://aisoc-ingest-xxxxxxxxxx-uc.a.run.app"

Container images

The defaults point at the public GHCR demo images (ghcr.io/beenuar/aisoc-{api,web,ingest}:latest) so the skeleton runs with zero CI work. For a real deployment, push your own images to the Artifact Registry repo this stack provisions:

gcloud auth configure-docker us-central1-docker.pkg.dev

REPO=$(terraform output -raw artifact_registry_repo)

docker build -t $REPO/api:$(git rev-parse --short HEAD) services/api
docker push $REPO/api:$(git rev-parse --short HEAD)
# repeat for web + ingest

terraform apply \
-var "api_image=$REPO/api:<sha>" \
-var "web_image=$REPO/web:<sha>" \
-var "ingest_image=$REPO/ingest:<sha>"

Connecting to Cloud SQL from your laptop

The instance has no public IP. Use the Cloud SQL Auth Proxy:

INSTANCE=$(terraform output -raw sql_connection_name)
PASSWORD=$(gcloud secrets versions access latest \
--secret=$(terraform output -raw secret_postgres_password_id))

cloud-sql-proxy --port 5432 "$INSTANCE" &
PGPASSWORD="$PASSWORD" psql -h 127.0.0.1 -U aisoc aisoc

Secrets

Five secrets are managed automatically:

Secret IDSourceConsumed by
aisoc-postgres-passwordrandom_passwordapi, ingest
aisoc-secret-keyrandom_password (64c)api, ingest
aisoc-credential-keyrandom (Fernet key)api (CredentialVault)
aisoc-redis-authMemorystore authapi, ingest
aisoc-openai-api-keyvar.openai_api_keyapi (optional)

Cloud Run mounts each as an environment variable via secret_key_ref { version = "latest" }, so rotating a secret value (without deleting the secret) is picked up on the next revision deploy.

Costs

Defaults are chosen so a fresh apply against a project that's never run a demo before fits inside the 90-day GCP startup-credit envelope:

ResourceDefault~Monthly cost (us-central1)
Cloud SQL Postgresdb-custom-2-7680~$95
Memorystore RedisBASIC 1 GB~$30
VPC Access connector2× e2-micro~$10
Cloud Run (idle)0–10 instances~$0 (scale-to-zero)
Artifact Registryempty repo~$0

Tune postgres_tier and redis_memory_size_gb in terraform.tfvars for a cheaper sandbox; the smallest sensible production tier is db-custom-2-7680 plus a STANDARD_HA Redis.

Limitations

This is a skeleton, not the full GCP migration:

  • No long-running services. services/agents, services/realtime, services/connectors, services/alert-fusion, services/threatintel, and services/fusion need persistent compute. The websocket fan-out and the APScheduler-driven connector polling don't fit Cloud Run's request lifecycle cleanly. The recommended follow-up is GKE Autopilot for those workloads, sharing the VPC and Cloud SQL provisioned here.
  • No HTTPS load balancer. Cloud Run gives every service a *.run.app certificate that's fine for the skeleton. Wire a Global External HTTPS Load Balancer + Cloud Armor in front of the API for a custom domain and WAF.
  • No Kafka. The buyer-value demo runs on Redis Streams alone. For higher throughput, swap in Confluent Cloud or run Kafka on GKE.
  • No CMEK. Secret Manager is the secret store; customer-managed encryption keys on Cloud SQL / Memorystore / Artifact Registry are a one-line addition (encryption_key_name = ...) and intentionally left out of the skeleton to keep the trust boundary small.
  • Demo image source. ghcr.io/beenuar/aisoc-* is the default for the zero-config experience; don't ship that to production.

Tear-down

terraform destroy

Cloud SQL refuses to delete unless deletion_protection=false. Either flip the variable and re-apply first, or destroy in two steps:

terraform apply -var deletion_protection=false
terraform destroy

See also