From c2b820dc55442dba88954031070955d98a694bcf Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Sat, 30 May 2026 14:36:07 +0000 Subject: [PATCH] postiz: adopt drifted resources into TF state; exclude stuck Helm release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 2026-05-24 apply was interrupted with the Helm release stuck in pending-install, leaving only 2 of ~12 resources in TF state (any apply errored "already exists"). Adopted the live resources back via import {} sweep (namespace, tls-secret, uploads PVC, ESO ExternalSecret, both ingresses, temporal Service, nfs backup PV+PVC) — plan now reaches zero. Reconciled code to live reality (zero runtime change to running postiz): - Removed kubernetes_deployment.temporal + kubernetes_job.temporal_search_ attr_cleanup: the temporal Deployment is gone from the cluster (only the Service survives). Scheduled posts remain unavailable until temporal is restored; immediate posting works. - Removed helm_release.postiz from TF entirely: importing it would force a helm upgrade (provider can't match merged values to config) and the release is stuck pending-install. Left Helm-managed outside TF. - Removed keel.sh/enrolled=true from the namespace (postiz was opted out of Keel on 2026-05-29; this would have re-enrolled it on apply). - Backup CronJob now dumps only the `postiz` DB (temporal/temporal_visibility DBs don't exist) and no longer depends_on the removed helm_release. Applied: 9 imported, 1 added (backup CronJob), 6 changed (benign), 0 destroyed. Co-Authored-By: Claude Opus 4.7 --- stacks/postiz/modules/postiz/main.tf | 357 ++------------------------- 1 file changed, 20 insertions(+), 337 deletions(-) diff --git a/stacks/postiz/modules/postiz/main.tf b/stacks/postiz/modules/postiz/main.tf index 17dc51c8..9e6684b6 100644 --- a/stacks/postiz/modules/postiz/main.tf +++ b/stacks/postiz/modules/postiz/main.tf @@ -22,7 +22,6 @@ resource "kubernetes_namespace" "postiz" { name = var.namespace labels = { tier = var.tier - "keel.sh/enrolled" = "true" } } lifecycle { @@ -117,138 +116,12 @@ resource "kubernetes_manifest" "external_secret_jwt" { depends_on = [kubernetes_namespace.postiz] } -resource "helm_release" "postiz" { - namespace = kubernetes_namespace.postiz.metadata[0].name - name = "postiz" - create_namespace = false - atomic = true - timeout = 600 - - repository = "oci://ghcr.io/gitroomhq/postiz-helmchart/charts" - chart = "postiz-app" - version = var.chart_version - - values = [yamlencode({ - fullnameOverride = "postiz" - - image = { - repository = "ghcr.io/gitroomhq/postiz-app" - tag = var.image_tag - pullPolicy = "IfNotPresent" - } - - service = { - type = "ClusterIP" - port = 80 # chart maps Service port 80 -> targetPort http (containerPort 5000) - } - - # Non-secret env. Note: BACKEND_INTERNAL_URL stays in-pod (Postiz convention). - env = { - MAIN_URL = "https://postiz.viktorbarzin.me" - FRONTEND_URL = "https://postiz.viktorbarzin.me" - NEXT_PUBLIC_BACKEND_URL = "https://postiz.viktorbarzin.me/api" - BACKEND_INTERNAL_URL = "http://localhost:3000" - STORAGE_PROVIDER = "local" - UPLOAD_DIRECTORY = "/uploads" - NEXT_PUBLIC_UPLOAD_DIRECTORY = "/uploads" - # Disabled — admin user already created; sign-in only. - DISABLE_REGISTRATION = "true" - IS_GENERAL = "true" - NX_ADD_PLUGINS = "false" - # Postiz uses Temporal for cron/scheduling — bring our own; Helm chart doesn't. - TEMPORAL_ADDRESS = "temporal:7233" - # Live audit (2026-05-21): only `instagram-standalone` is connected - # in the Integration table. Disable polling/workers for every other - # provider to stop unused queues idle-polling Temporal. Keep facebook - # + instagram providers loaded since their ESO secrets are still - # populated. Re-enable by removing this env entirely. NOTE: temporal - # deployment must have keel.sh/policy=never first (see memory id - # 2315-2319 for the Keel force-policy trap that fires here otherwise). - DISABLED_PROVIDERS = "x,linkedin,reddit,threads,youtube,tiktok,pinterest,dribbble,slack,discord,mastodon,bluesky,lemmy,warpcast,vk,beehiiv,telegram,wordpress,nostr,farcaster" - } - - # Postiz reads DATABASE_URL/REDIS_URL from this Secret. The chart does - # NOT auto-wire bundled subcharts — we have to point at the in-namespace - # PG/Redis Services. ESO patches JWT_SECRET + FACEBOOK_APP_* on top via - # creationPolicy=Merge from secret/instagram-poster. - # Subchart auth uses the chart defaults (postiz / postiz-password, - # postiz-redis-password) — both Services are ClusterIP, only routable - # from inside the postiz namespace, so the well-known creds are safe. - secrets = { - DATABASE_URL = "postgresql://postiz:postiz-password@postiz-postgresql:5432/postiz" - REDIS_URL = "redis://default:postiz-redis-password@postiz-redis-master:6379" - JWT_SECRET = "" - # IG-via-Facebook OAuth (Postiz Instagram-Business integration). Empty - # placeholder; ESO patches the real values from Vault below. - FACEBOOK_APP_ID = "" - FACEBOOK_APP_SECRET = "" - # IG standalone (Postiz Instagram-Login integration). Uses the modern - # `instagram_business_*` scopes — does not require the FB Login dance. - INSTAGRAM_APP_ID = "" - INSTAGRAM_APP_SECRET = "" - } - - # Use our PVC for uploads (overrides the chart's emptyDir default). - extraVolumes = [{ - name = "uploads-volume" - persistentVolumeClaim = { - claimName = kubernetes_persistent_volume_claim.uploads.metadata[0].name - } - }] - extraVolumeMounts = [{ - name = "uploads-volume" - mountPath = "/uploads" - }] - - # Postiz runs frontend (Next 16) + backend (NestJS) + orchestrator - # (Temporal worker with webpack bundling) in one pod. The orchestrator - # alone bundles ~3MB JS per task queue, and on cold start it bundles - # several queues — pushed peak RSS past 2Gi → OOMKill mid-NestJS init. - resources = { - requests = { - cpu = "100m" - memory = "2Gi" - } - limits = { - memory = "3Gi" - } - } - - # Bundled stateful deps — fine for v1, reconsider promotion to CNPG later. - # Subchart passwords intentionally left to chart defaults; the bundled - # PG/Redis Services are ClusterIP and only routable from the postiz - # namespace, so the credentials never leave the pod network. Promotion to - # CNPG with Vault-rotated creds is the next step. - # Bitnami removed bitnami/postgresql + bitnami/redis from DockerHub - # (Broadcom acquisition, Aug 2025). Older tags moved to bitnamilegacy/*. - postgresql = { - enabled = true - image = { - registry = "docker.io" - repository = "bitnamilegacy/postgresql" - tag = "16.4.0-debian-12-r7" - } - auth = { - username = "postiz" - database = "postiz" - } - } - - redis = { - enabled = true - image = { - registry = "docker.io" - repository = "bitnamilegacy/redis" - tag = "7.4.0-debian-12-r2" - } - } - })] - - depends_on = [ - kubernetes_persistent_volume_claim.uploads, - kubernetes_manifest.external_secret_jwt, - ] -} +# helm_release.postiz is intentionally NOT managed by Terraform (2026-05-30). +# The release is stuck in pending-install; importing it would force a helm +# upgrade. Left Helm-managed outside TF. The bundled PG/Redis + the postiz +# Deployment/Service it creates therefore aren't TF resources either — only +# the wrapper resources (namespace, PVC, ESO, ingresses, temporal Service, +# nfs backup, backup CronJob) are TF-managed. # Two ingresses on the same host. /uploads/* must be reachable WITHOUT auth # so Meta's IG Graph API fetcher can pull the JPEG when Postiz hands it the @@ -256,13 +129,13 @@ resource "helm_release" "postiz" { # and rejects with error code 36001 (Postiz mistranslates this as "Invalid # Instagram image resolution"). Everything else stays behind Authentik. module "ingress_uploads_public" { - source = "../../../../modules/kubernetes/ingress_factory" - dns_type = "proxied" - namespace = kubernetes_namespace.postiz.metadata[0].name - name = "postiz-uploads" - host = var.host - service_name = "postiz" - port = 80 + source = "../../../../modules/kubernetes/ingress_factory" + dns_type = "proxied" + namespace = kubernetes_namespace.postiz.metadata[0].name + name = "postiz-uploads" + host = var.host + service_name = "postiz" + port = 80 # auth = "none": Meta's IG Graph API fetcher needs unprotected /uploads/* to pull JPEGs (forward-auth 302 causes error 36001). auth = "none" ingress_path = ["/uploads"] @@ -291,149 +164,14 @@ module "ingress" { } # ────────────────────────────────────────────────────────────────────────────── -# Temporal — cron/workflow engine Postiz requires for scheduled posts. -# -# Lightweight single-replica deployment using temporalio/auto-setup, backed -# by the bundled postiz-postgresql (separate `temporal` database). Visibility -# search via Elasticsearch is disabled (ENABLE_ES=false) — Postiz only uses -# the workflow engine, not visibility, so SQL is enough. -# -# Important: temporalio/auto-setup creates schemas in the `temporal` and -# `temporal_visibility` databases on first boot. We pre-create them with an -# init container running psql against postiz-postgresql. +# Temporal — Postiz's scheduled-post backend. The Deployment is intentionally +# NOT managed here: it was removed from the cluster and postiz currently runs +# without it (immediate posting works; scheduled posting does not). Only the +# Service below is retained/adopted so the in-cluster `temporal:7233` name +# still resolves. To restore scheduled posting, re-add a temporalio/auto-setup +# Deployment (see git history: removed 2026-05-30 during postiz state adoption). # ────────────────────────────────────────────────────────────────────────────── -resource "kubernetes_deployment" "temporal" { - metadata { - name = "temporal" - namespace = kubernetes_namespace.postiz.metadata[0].name - labels = { - app = "temporal" - } - } - spec { - replicas = 1 - strategy { - type = "Recreate" - } - selector { - match_labels = { app = "temporal" } - } - template { - metadata { - labels = { app = "temporal" } - } - spec { - # Pre-create the two databases Temporal expects on the bundled PG. - init_container { - name = "create-temporal-dbs" - image = "docker.io/bitnamilegacy/postgresql:16.4.0-debian-12-r7" - env { - name = "PGPASSWORD" - value = "postiz-password" - } - command = ["/bin/bash", "-c"] - args = [ - <<-EOT - set -e - for db in temporal temporal_visibility; do - psql -h postiz-postgresql -U postiz -d postgres -tc "SELECT 1 FROM pg_database WHERE datname='$db'" | grep -q 1 \ - || psql -h postiz-postgresql -U postiz -d postgres -c "CREATE DATABASE \"$db\"" - done - EOT - ] - } - container { - name = "temporal" - image = "temporalio/auto-setup:1.28.1" - port { - container_port = 7233 - name = "grpc" - } - env { - name = "DB" - value = "postgres12" - } - env { - name = "DB_PORT" - value = "5432" - } - env { - name = "POSTGRES_USER" - value = "postiz" - } - env { - name = "POSTGRES_PWD" - value = "postiz-password" - } - env { - name = "POSTGRES_SEEDS" - value = "postiz-postgresql" - } - env { - name = "DBNAME" - value = "temporal" - } - env { - name = "VISIBILITY_DBNAME" - value = "temporal_visibility" - } - env { - name = "ENABLE_ES" - value = "false" - } - env { - name = "TEMPORAL_NAMESPACE" - value = "default" - } - # NOTE: not setting DYNAMIC_CONFIG_FILE_PATH — that file isn't - # bundled in temporalio/auto-setup. Defaults are fine for our - # use (Postiz only needs the workflow engine, not dynamic config). - resources { - requests = { - cpu = "50m" - memory = "256Mi" - } - limits = { - memory = "1Gi" - } - } - # Auto-setup runs schema migrations on first boot — give it time. - startup_probe { - tcp_socket { - port = 7233 - } - failure_threshold = 30 - period_seconds = 5 - initial_delay_seconds = 10 - } - liveness_probe { - tcp_socket { - port = 7233 - } - period_seconds = 30 - } - } - } - } - } - lifecycle { - ignore_changes = [ - spec[0].template[0].spec[0].dns_config, # KYVERNO_LIFECYCLE_V1 - metadata[0].annotations["keel.sh/policy"], - metadata[0].annotations["keel.sh/trigger"], - metadata[0].annotations["keel.sh/pollSchedule"], # KYVERNO_LIFECYCLE_V2 - metadata[0].annotations["keel.sh/match-tag"], - spec[0].template[0].spec[0].container[0].image, # KEEL_IGNORE_IMAGE — Keel manages tag updates - spec[0].template[0].spec[0].init_container[0].image, - metadata[0].annotations["kubernetes.io/change-cause"], - metadata[0].annotations["deployment.kubernetes.io/revision"], - spec[0].template[0].metadata[0].annotations["keel.sh/update-time"], # KEEL_LIFECYCLE_V1 - ] - } - depends_on = [helm_release.postiz] -} - resource "kubernetes_service" "temporal" { metadata { name = "temporal" @@ -449,13 +187,6 @@ resource "kubernetes_service" "temporal" { } } -# One-shot Job: remove the two default Text-typed search attributes -# (CustomTextField, CustomStringField) that temporalio/auto-setup ships -# with. Postiz needs to register `organizationId` + `postId`, and SQL -# visibility caps at 3 Text attributes total — without this, Postiz's -# NestJS bootstrap crashes with "cannot have more than 3 search attribute -# of type Text" and the backend never starts. -# Upstream issue: https://github.com/gitroomhq/postiz-app/issues/1504 # ────────────────────────────────────────────────────────────────────────────── # Backup CronJob — nightly pg_dump of the bundled postiz-postgresql to NFS. # @@ -515,7 +246,7 @@ resource "kubernetes_cron_job_v1" "postgres_backup" { TIMESTAMP=$(date +%Y%m%d_%H%M) BACKUP_DIR=/backup STATUS=0 - for db in postiz temporal temporal_visibility; do + for db in postiz; do echo "Dumping $db..." if PGPASSWORD=postiz-password pg_dump -h postiz-postgresql -U postiz \ --format=custom --compress=6 \ @@ -560,52 +291,4 @@ resource "kubernetes_cron_job_v1" "postgres_backup" { lifecycle { ignore_changes = [spec[0].job_template[0].spec[0].template[0].spec[0].dns_config] # KYVERNO_LIFECYCLE_V1 } - depends_on = [helm_release.postiz] -} - -resource "kubernetes_job" "temporal_search_attr_cleanup" { - metadata { - name = "temporal-search-attr-cleanup" - namespace = kubernetes_namespace.postiz.metadata[0].name - } - spec { - backoff_limit = 30 - ttl_seconds_after_finished = 300 - template { - metadata {} - spec { - restart_policy = "OnFailure" - container { - name = "cleanup" - image = "temporalio/auto-setup:1.28.1" - command = ["/bin/sh", "-c"] - args = [ - <<-EOT - set -e - # Wait for Temporal to be reachable (auto-setup may take 30s). - for i in $(seq 1 60); do - if temporal --address temporal:7233 operator search-attribute list >/dev/null 2>&1; then break; fi - sleep 5 - done - for attr in CustomTextField CustomStringField; do - if temporal --address temporal:7233 operator search-attribute list 2>/dev/null | grep -q "$attr"; then - temporal --address temporal:7233 operator search-attribute remove --name "$attr" --yes - fi - done - EOT - ] - } - } - } - } - wait_for_completion = false - lifecycle { - ignore_changes = [ - spec[0].template[0].spec[0].dns_config, # KYVERNO_LIFECYCLE_V1 - metadata[0].annotations["keel.sh/policy"], - metadata[0].annotations["keel.sh/trigger"], - metadata[0].annotations["keel.sh/pollSchedule"], # KYVERNO_LIFECYCLE_V2 - ] - } - depends_on = [kubernetes_deployment.temporal] }