Files
Will Anderson 3d581368c3 fix(vault-gke): relax topology spread to ScheduleAnyway while SSD quota is exhausted
vault-1 and vault-2 are stuck Pending because GCE quota (SSD_TOTAL_GB 500/500)
prevents new nodes from provisioning. ScheduleAnyway lets them land in any zone
that has capacity while the quota increase request is pending.

Also adds ESO Workload Identity IAM bindings to Terraform state (previously applied
out-of-band via gcloud; now tracked in cloud-sql.tf).
2026-05-04 22:37:17 -05:00

266 lines
9.9 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]
}
}
# ── 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/<connection_name>.
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
}