# ── 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] } } # ── Gitea — database + user on neuron-prod-pg15 ─────────────────────────────── # Gitea on GKE uses the existing Cloud SQL instance with a Cloud SQL Auth Proxy # sidecar. Connection via unix socket — no public IP exposure. resource "google_sql_database" "gitea" { name = "gitea" instance = google_sql_database_instance.main.name project = var.project_id } resource "random_password" "gitea_db" { length = 32 special = false # Cloud SQL unix socket path DSN; keep alphanumeric for simplicity } resource "google_sql_user" "gitea" { name = "gitea" instance = google_sql_database_instance.main.name project = var.project_id password = random_password.gitea_db.result } # ── Gitea service account (for Workload Identity → Cloud SQL) ───────────────── resource "google_service_account" "gitea" { account_id = "gitea-gke" display_name = "Gitea GKE SA" description = "Service account for the Gitea pod on GKE. Used by the Cloud SQL Auth Proxy sidecar via Workload Identity." project = var.project_id } resource "google_project_iam_member" "gitea_sql_client" { project = var.project_id role = "roles/cloudsql.client" member = "serviceAccount:${google_service_account.gitea.email}" } # ── Secret Manager — Gitea database URL ─────────────────────────────────────── # Full DSN using the Cloud SQL Auth Proxy unix socket path. # The proxy sidecar mounts the socket at /cloudsql/. resource "google_secret_manager_secret" "gitea_database_url" { secret_id = "gitea-database-url" project = var.project_id replication { auto {} } } resource "google_secret_manager_secret_version" "gitea_database_url" { secret = google_secret_manager_secret.gitea_database_url.id secret_data = "host=/cloudsql/${google_sql_database_instance.main.connection_name} user=gitea password=${random_password.gitea_db.result} dbname=gitea sslmode=disable" } # gitea-db-password — the raw password only, for use in Gitea's GITEA__database__PASSWD. # The Cloud SQL Auth Proxy provides the unix socket; Gitea uses standard postgres # password auth through the proxy socket. resource "google_secret_manager_secret" "gitea_db_password" { secret_id = "gitea-db-password" project = var.project_id replication { auto {} } } resource "google_secret_manager_secret_version" "gitea_db_password" { secret = google_secret_manager_secret.gitea_db_password.id secret_data = random_password.gitea_db.result } # Allow the Gitea GCP SA to access its secrets resource "google_secret_manager_secret_iam_member" "gitea_database_url_accessor" { project = var.project_id secret_id = google_secret_manager_secret.gitea_database_url.secret_id role = "roles/secretmanager.secretAccessor" member = "serviceAccount:${google_service_account.gitea.email}" } resource "google_secret_manager_secret_iam_member" "gitea_db_password_accessor" { project = var.project_id secret_id = google_secret_manager_secret.gitea_db_password.secret_id role = "roles/secretmanager.secretAccessor" member = "serviceAccount:${google_service_account.gitea.email}" } # ESO Workload Identity — allows the external-secrets controller SA on GKE # to impersonate the gitea GCP SA for Secret Manager access. resource "google_service_account_iam_member" "eso_gitea_wi" { service_account_id = google_service_account.gitea.name role = "roles/iam.workloadIdentityUser" member = "serviceAccount:${var.project_id}.svc.id.goog[external-secrets/external-secrets]" depends_on = [google_container_cluster.neuron_platform] } resource "google_service_account_iam_member" "eso_gitea_token_creator" { service_account_id = google_service_account.gitea.name role = "roles/iam.serviceAccountTokenCreator" member = "serviceAccount:${var.project_id}.svc.id.goog[external-secrets/external-secrets]" depends_on = [google_container_cluster.neuron_platform] } output "gitea_service_account_email" { description = "Gitea GKE SA email — used in the Workload Identity annotation" value = google_service_account.gitea.email } output "gitea_database_secret_id" { description = "Secret Manager secret ID for the Gitea database URL" value = google_secret_manager_secret.gitea_database_url.secret_id }