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/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​