diff --git a/stacks/dbaas/modules/dbaas/main.tf b/stacks/dbaas/modules/dbaas/main.tf index f7222ca2..bbb4723e 100644 --- a/stacks/dbaas/modules/dbaas/main.tf +++ b/stacks/dbaas/modules/dbaas/main.tf @@ -1296,6 +1296,35 @@ resource "null_resource" "pg_fire_planner_db" { } } +# Create instagram_poster database for the IG-curation pipeline. Initial use: +# benchmark_score table written by `instagram_poster.benchmark` CLI (vision-LLM +# scoring per Immich asset). Future: migrate story_queue/decision/ig_posted_media +# off the pod's sqlite PVC into this DB so the pod is fully stateless. +# Role password is managed by Vault Database Secrets Engine +# (static role `pg-instagram-poster`, 7d rotation). +resource "null_resource" "pg_instagram_poster_db" { + depends_on = [null_resource.pg_cluster] + + triggers = { + db_name = "instagram_poster" + username = "instagram_poster" + } + + provisioner "local-exec" { + command = <<-EOT + PRIMARY=$(kubectl --kubeconfig ${var.kube_config_path} get cluster -n dbaas pg-cluster -o jsonpath='{.status.currentPrimary}') + kubectl --kubeconfig ${var.kube_config_path} exec -n dbaas $PRIMARY -c postgres -- \ + bash -c ' + psql -U postgres -tc "SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = '"'"'instagram_poster'"'"'" | grep -q 1 || \ + psql -U postgres -c "CREATE ROLE instagram_poster WITH LOGIN PASSWORD '"'"'changeme-vault-will-rotate'"'"'" + psql -U postgres -tc "SELECT 1 FROM pg_catalog.pg_database WHERE datname = '"'"'instagram_poster'"'"'" | grep -q 1 || \ + psql -U postgres -c "CREATE DATABASE instagram_poster OWNER instagram_poster" + psql -U postgres -c "GRANT ALL PRIVILEGES ON DATABASE instagram_poster TO instagram_poster" + ' + EOT + } +} + # Old PostgreSQL deployment — kept commented for rollback reference # resource "kubernetes_deployment" "postgres" { # metadata { diff --git a/stacks/instagram-poster/modules/instagram-poster/main.tf b/stacks/instagram-poster/modules/instagram-poster/main.tf index d9d80cf4..ed258a7d 100644 --- a/stacks/instagram-poster/modules/instagram-poster/main.tf +++ b/stacks/instagram-poster/modules/instagram-poster/main.tf @@ -129,6 +129,54 @@ resource "kubernetes_manifest" "external_secret" { depends_on = [kubernetes_namespace.instagram_poster] } +# Benchmark scoring DB — shared CNPG cluster, written by the +# `instagram_poster.benchmark` CLI (vision-LLM scores per Immich asset). +# Vault static role `pg-instagram-poster` rotates the password every 7 days; +# ESO refreshes the K8s Secret every 15m. `reloader.stakater.com/match` +# bounces the pod when the password changes. +resource "kubernetes_manifest" "benchmark_db_external_secret" { + manifest = { + apiVersion = "external-secrets.io/v1beta1" + kind = "ExternalSecret" + metadata = { + name = "instagram-poster-benchmark-db" + namespace = local.namespace + } + spec = { + refreshInterval = "15m" + secretStoreRef = { + name = "vault-database" + kind = "ClusterSecretStore" + } + target = { + name = "instagram-poster-benchmark-db" + template = { + metadata = { + annotations = { + "reloader.stakater.com/match" = "true" + } + } + data = { + BENCHMARK_PG_HOST = "pg-cluster-rw.dbaas.svc.cluster.local" + BENCHMARK_PG_PORT = "5432" + BENCHMARK_PG_DATABASE = "instagram_poster" + BENCHMARK_PG_USER = "instagram_poster" + BENCHMARK_PG_PASSWORD = "{{ .password }}" + } + } + } + data = [{ + secretKey = "password" + remoteRef = { + key = "static-creds/pg-instagram-poster" + property = "password" + } + }] + } + } + depends_on = [kubernetes_namespace.instagram_poster] +} + # Persistent state: SQLite + image cache. Sensitive (API tokens may end up # in cached images / debug logs), but the proxmox-lvm-encrypted SC is for # user-data DBs; this is a small app cache so plain proxmox-lvm fits the @@ -214,6 +262,15 @@ resource "kubernetes_deployment" "instagram_poster" { name = "instagram-poster-secrets" } } + # Vault-rotated benchmark Postgres creds. Sources BENCHMARK_PG_* + # env vars into the container; benchmark.py builds the SQLAlchemy + # URL from them. Schema bootstraps via Base.metadata.create_all + # on first use. + env_from { + secret_ref { + name = "instagram-poster-benchmark-db" + } + } env { name = "IMMICH_BASE_URL" @@ -302,6 +359,7 @@ resource "kubernetes_deployment" "instagram_poster" { depends_on = [ kubernetes_manifest.external_secret, + kubernetes_manifest.benchmark_db_external_secret, ] } diff --git a/stacks/vault/main.tf b/stacks/vault/main.tf index 66f60aac..0e84859a 100644 --- a/stacks/vault/main.tf +++ b/stacks/vault/main.tf @@ -540,7 +540,7 @@ resource "vault_database_secret_backend_connection" "postgresql" { "pg-affine", "pg-woodpecker", "pg-claude-memory", "pg-terraform-state", "pg-payslip-ingest", "pg-job-hunter", "pg-wealthfolio-sync", "pg-fire-planner", - "pg-postiz", + "pg-postiz", "pg-instagram-poster", ] postgresql { @@ -721,6 +721,14 @@ resource "vault_database_secret_backend_static_role" "pg_fire_planner" { rotation_period = 604800 } +resource "vault_database_secret_backend_static_role" "pg_instagram_poster" { + backend = vault_mount.database.path + db_name = vault_database_secret_backend_connection.postgresql.name + name = "pg-instagram-poster" + username = "instagram_poster" + rotation_period = 604800 +} + # ============================================================================= # Kubernetes Secrets Engine — Dynamic K8s Credentials # =============================================================================