b572f5720b
- Replace GKE_CLUSTER_ENDPOINT placeholder with real endpoint (34.63.89.52) in all three Argo CD Application manifests - Fix gitea GCP SA account_id from 'gitea' (5 chars, too short) to 'gitea-gke' to satisfy GCP's 6-30 char constraint - Update serviceaccount.yaml annotation to match new SA email (gitea-gke@...)
248 lines
9.1 KiB
Terraform
248 lines
9.1 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}"
|
|
}
|
|
|
|
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
|
|
}
|