3d581368c3
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).
266 lines
9.9 KiB
Terraform
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
|
|
}
|