Files
infrastructure/servers/gcp/cloud-sql.tf
T
Will Anderson d4c65d5857 Expand GCP infra: accounts + API services, Cloud SQL, Artifact Registry
Architecture: intelligence stays on Legion; only compiled artifacts cross
to GCP. Source code and Neuron's knowledge base never leave the system.

Artifact Registry:
- neuron-marketing, neuron-accounts, neuron-api repos in us-central1
- Keep-last-10 cleanup policy; ci-pusher SA with writer access
- Legion CI runners authenticate via GCP_SA_KEY Gitea secret

Cloud SQL (cloud-sql.tf):
- postgres-15 on db-g1-small, us-central1 (scale up to REGIONAL HA at 1k users)
- Point-in-time recovery, 14-day backup retention
- Accounts DB + user; password generated and stored in Secret Manager
- JWT signing key in Secret Manager (shared by accounts + api)
- Cloud Run connects via built-in Auth Proxy (Unix socket volume mount)

Accounts Cloud Run (cloud-run-accounts.tf):
- 3 regions (us-central1, europe-west1, asia-northeast1), min:1 max:50
- Cloud SQL proxy volume mount; secrets via Secret Manager
- Stripe + JWT env vars; health probe on /health

API Cloud Run (cloud-run-api.tf):
- 3 regions, min:1 max:100, cpu_idle=false (always-hot)
- Validates JWTs from accounts service; no direct DB connection
- License admin token from Secret Manager

Load balancer (host-based routing):
- Same global anycast IP for all three services
- URL map routes by Host: neurontechnologies.ai→marketing,
  api.neurontechnologies.ai→api, accounts.neurontechnologies.ai→accounts
- New managed SSL certs for api.* and accounts.* added to HTTPS proxy
- Cloud Armor (WAF + rate limit) applied to all backends

Service accounts + IAM:
- neuron-accounts-sa: secretmanager.secretAccessor + cloudsql.client
- neuron-api-sa: secretmanager.secretAccessor
- allUsers invoker on all prod Cloud Run services (LB health checks)

bootstrap.sh:
- One-shot setup: pulls Stripe secrets from Vault → Secret Manager,
  creates CI SA JSON key, prints DNS + next-step instructions
2026-04-25 22:54:18 -05:00

151 lines
5.5 KiB
Terraform

# ── Cloud SQL — PostgreSQL 15 ─────────────────────────────────────────────────
# Single instance in us-central1 for the accounts service.
# Cloud Run services connect via the built-in Cloud SQL Auth Proxy
# (no direct IP exposure, encrypted, IAM-authenticated).
#
# Sizing: db-g1-small (1 shared vCPU, 1.7 GB) for launch. Scale up to
# db-n1-standard-2 once traffic warrants it — zero-downtime restart.
resource "google_sql_database_instance" "main" {
name = "neuron-prod-pg15"
database_version = "POSTGRES_15"
region = "us-central1"
project = var.project_id
# Prevent accidental deletion via terraform destroy
deletion_protection = true
settings {
tier = "db-g1-small"
availability_type = "ZONAL" # Upgrade to REGIONAL (HA) once > 1k users
backup_configuration {
enabled = true
point_in_time_recovery_enabled = true
start_time = "03:00" # 3am UTC — before backup window on Legion
transaction_log_retention_days = 7
backup_retention_settings {
retained_backups = 14
}
}
maintenance_window {
day = 7 # Sunday
hour = 4 # 4am UTC
update_track = "stable"
}
ip_configuration {
# No public IP — Cloud Run uses the Auth Proxy via private service connect
# Flip ipv4_enabled=true + authorized_networks if you ever need direct access
# for migrations/seeding from a bastion.
ipv4_enabled = true # Required for Cloud Run Auth Proxy (until VPC SC configured)
ssl_mode = "ENCRYPTED_ONLY"
}
database_flags {
name = "max_connections"
value = "100" # Cloud Run can burst many instances; cap connections early
}
database_flags {
name = "log_min_duration_statement"
value = "500" # Log queries taking > 500ms
}
insights_config {
query_insights_enabled = true
query_string_length = 1024
record_application_tags = true
record_client_address = false
}
}
}
# ── Databases ─────────────────────────────────────────────────────────────────
resource "google_sql_database" "accounts" {
name = "accounts"
instance = google_sql_database_instance.main.name
project = var.project_id
}
# ── Users ─────────────────────────────────────────────────────────────────────
# Passwords are stored in Secret Manager. The accounts service reads them
# at runtime via the ACCOUNTS_DATABASE_URL secret.
# Cloud IAM DB users are the preferred path long-term (no password rotation);
# using password auth here for compatibility with pgx/stdlib.
resource "google_sql_user" "accounts" {
name = "accounts"
instance = google_sql_database_instance.main.name
project = var.project_id
password = random_password.accounts_db.result
}
resource "random_password" "accounts_db" {
length = 32
special = true
}
# ── Secret Manager — Database URL ─────────────────────────────────────────────
# Stored as a full DSN so the app just reads one env var.
# Uses /cloudsql/ Unix socket path — Cloud Run Auth Proxy mounts it there.
resource "google_secret_manager_secret" "accounts_database_url" {
secret_id = "accounts-database-url"
project = var.project_id
replication {
auto {}
}
}
resource "google_secret_manager_secret_version" "accounts_database_url" {
secret = google_secret_manager_secret.accounts_database_url.id
secret_data = "host=/cloudsql/${google_sql_database_instance.main.connection_name} user=accounts password=${random_password.accounts_db.result} dbname=accounts sslmode=disable"
}
# ── Secret Manager — JWT signing key ─────────────────────────────────────────
# Shared between accounts (issues tokens) and neuron-rest (validates tokens).
resource "random_password" "jwt_secret" {
length = 64
special = false # URL-safe; used as HMAC key
}
resource "google_secret_manager_secret" "jwt_secret" {
secret_id = "jwt-signing-key"
project = var.project_id
replication {
auto {}
}
}
resource "google_secret_manager_secret_version" "jwt_secret" {
secret = google_secret_manager_secret.jwt_secret.id
secret_data = random_password.jwt_secret.result
}
# ── Secret Manager — License admin token ─────────────────────────────────────
# Used by neuron-rest to authorize license validation calls.
# Currently held in Vault under neuron-technologies/marketing.license_admin_token.
# Create this secret manually from Vault:
# gcloud secrets create license-admin-token --data-file=<(vault kv get \
# -field=license_admin_token secret/neuron-technologies/marketing)
resource "google_secret_manager_secret" "license_admin_token" {
secret_id = "license-admin-token"
project = var.project_id
replication {
auto {}
}
# Version populated manually or via bootstrap script — not managed by Terraform
# to avoid exposing the value in state.
lifecycle {
ignore_changes = [id]
}
}