postiz: reconcile HCL to live (adopt unmerged stack config), keep parked
All checks were successful
ci/woodpecker/push/default Pipeline was successful

postiz's live deployment (Helm + Temporal + Elasticsearch + Authentik
OIDC + static-DB password) came from the never-merged branch
`wizard/postiz-cnpg-oidc`, so master's HCL was stale and a `terragrunt
apply` would have DESTROYED the stack. This lands that postiz config to
master so HCL == state == live (CI green; destroy-landmine gone).

Kept PARKED (postiz + temporal replicas = 0): IG-via-postiz is Meta-
blocked (it hardcodes retired Instagram scopes → OAuth "Invalid Scopes"),
which is why it was parked; IG runs via the instagram-poster service. To
revive later: flip postiz `replicaCount` + temporal `replicas` back to 1
and re-check image pins.

Notes captured in this reconcile:
- ES image pinned to 7.17.28 (the branch's 7.17.24 was a DOWNGRADE vs the
  live data → ES refused to start "cannot downgrade node 7.17.28→7.17.24";
  caught + rolled back during this work).
- The 4 Authentik resources (app/provider/group/binding) were re-imported
  into state (adopted, not recreated — no duplicate AK objects); the
  obsolete `external_secret_jwt` ExternalSecret was removed (Retain → its
  synced secret was kept).
- Vault-side cleanup (removing the unused pg-postiz rotated role) is
  deliberately NOT included here — deferred, postiz uses a static
  secret/postiz database_url.

State was already reconciled by a local `scripts/tg apply`; this commit is
the HCL catch-up (CI re-apply is a no-op).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-06-28 12:54:59 +00:00
parent 250d0fc334
commit 8236ae309d
4 changed files with 512 additions and 101 deletions

112
stacks/postiz/authentik.tf Normal file
View file

@ -0,0 +1,112 @@
# Authentik OIDC for the Postiz UI (2026-06-16, issue #45).
#
# Postiz keeps the Authentik forward-auth outer gate (ingress auth="required")
# AND offers "Login with Authentik" via generic OIDC. Postiz cannot disable its
# own local-login endpoint, so forward-auth stays in front to keep that endpoint
# off the public internet; OIDC is the SSO path. Access is gated to the
# `Postiz Users` group (Viktor + Anca) bound to this application non-members
# cannot complete the OIDC flow. confidential client (Postiz is a server-side
# web app and can hold the secret); the secret is passed into the Helm release.
#
# Provider/app/group pattern mirrors stacks/tripit/authentik.tf. RS256 signing
# key (the self-signed cert) required or Authentik signs HS256 with an empty
# JWKS and token verification fails.
data "vault_kv_secret_v2" "authentik_tf" {
mount = "secret"
name = "authentik"
}
provider "authentik" {
url = "https://authentik.viktorbarzin.me"
token = data.vault_kv_secret_v2.authentik_tf.data["tf_api_token"]
}
data "authentik_flow" "default_authorization_implicit_consent" {
slug = "default-provider-authorization-implicit-consent"
}
data "authentik_flow" "default_provider_invalidation" {
slug = "default-provider-invalidation-flow"
}
data "authentik_certificate_key_pair" "signing" {
name = "authentik Self-signed Certificate"
}
data "authentik_property_mapping_provider_scope" "openid" {
managed = "goauthentik.io/providers/oauth2/scope-openid"
}
data "authentik_property_mapping_provider_scope" "profile" {
managed = "goauthentik.io/providers/oauth2/scope-profile"
}
data "authentik_property_mapping_provider_scope" "email" {
managed = "goauthentik.io/providers/oauth2/scope-email"
}
# Usernames in this Authentik instance ARE the user's email (see
# stacks/authentik/t3-users.tf). Viktor + Anca are the only Postiz users.
data "authentik_user" "viktor" {
username = "vbarzin@gmail.com"
}
data "authentik_user" "anca" {
username = "ancaelena98@gmail.com"
}
resource "authentik_provider_oauth2" "postiz" {
name = "postiz"
client_id = "postiz"
client_type = "confidential"
# sub = the user's email (stable). Postiz keys OIDC accounts by the subject;
# an email sub keeps Anca's and Viktor's identities stable across logins.
sub_mode = "user_email"
authorization_flow = data.authentik_flow.default_authorization_implicit_consent.id
invalidation_flow = data.authentik_flow.default_provider_invalidation.id
# Postiz hardcodes its OIDC redirect to ${FRONTEND_URL}/settings.
allowed_redirect_uris = [
{
matching_mode = "strict"
url = "https://postiz.viktorbarzin.me/settings"
},
]
access_token_validity = "hours=1"
refresh_token_validity = "days=30"
include_claims_in_id_token = true
signing_key = data.authentik_certificate_key_pair.signing.id
property_mappings = [
data.authentik_property_mapping_provider_scope.openid.id,
data.authentik_property_mapping_provider_scope.profile.id,
data.authentik_property_mapping_provider_scope.email.id,
]
}
resource "authentik_application" "postiz" {
name = "Postiz"
slug = "postiz"
protocol_provider = authentik_provider_oauth2.postiz.id
meta_launch_url = "https://postiz.viktorbarzin.me"
# "any" + at least one binding => only members of a bound group get access.
policy_engine_mode = "any"
}
# Access gate: only these two users may complete the Postiz OIDC flow.
resource "authentik_group" "postiz_users" {
name = "Postiz Users"
users = [
data.authentik_user.viktor.id,
data.authentik_user.anca.id,
]
}
resource "authentik_policy_binding" "postiz_access" {
target = authentik_application.postiz.uuid
group = authentik_group.postiz_users.id
order = 0
}

View file

@ -5,7 +5,8 @@ variable "tls_secret_name" {
variable "nfs_server" { type = string }
module "postiz" {
source = "./modules/postiz"
tls_secret_name = var.tls_secret_name
tier = local.tiers.aux
source = "./modules/postiz"
tls_secret_name = var.tls_secret_name
tier = local.tiers.aux
oauth_client_secret = authentik_provider_oauth2.postiz.client_secret
}

View file

@ -4,17 +4,17 @@
# Chart: oci://ghcr.io/gitroomhq/postiz-helmchart/charts/postiz (v1.0.5)
# App : ghcr.io/gitroomhq/postiz-app:v2.21.7
#
# Layout:
# - Bundled Postgres + Redis (chart subcharts) fine for v1.
# Layout (2026-06-16 migrated off the bundled subcharts onto shared infra):
# - Postgres: shared CNPG cluster (pg-cluster-rw.dbaas). The `postiz` role
# uses a STATIC password in Vault KV secret/postiz (DB-engine rotation for
# pg-postiz was removed see stacks/vault), so the chart carries
# DATABASE_URL directly with no ESO-merge race / no Reloader requirement.
# - Redis: shared standalone redis-master.redis on logical DB index 11.
# - Local file storage for uploads on a `proxmox-lvm` PVC mounted at /uploads.
# - JWT_SECRET is sourced from Vault via ESO. The chart's helper-templated
# Secret name is `<release>-secrets`; we pin `fullnameOverride: postiz` so
# the Secret resolves to `postiz-secrets`. The chart already mounts that
# Secret via `envFrom: secretRef: <fullname>-secrets`, so ESO patching the
# same Secret with `creationPolicy: Merge` injects `JWT_SECRET` into the
# pod env without forking the chart.
# - OAuth credentials for Meta/X/LinkedIn etc. are NOT pre-seeded Postiz
# stores those in its own DB once the user adds providers via the UI.
# - All secret env (DATABASE_URL, JWT_SECRET, Meta OAuth app creds) is sourced
# from Vault and rendered into the chart's `secrets:` block. fullnameOverride
# pins the Secret/Service to `postiz` so the instagram-poster pipeline's
# internal URL (http://postiz.postiz.svc.cluster.local) keeps resolving.
#
resource "kubernetes_namespace" "postiz" {
@ -66,65 +66,120 @@ resource "kubernetes_persistent_volume_claim" "uploads" {
}
}
# ExternalSecret: patches the chart-managed `postiz-secrets` Secret with
# JWT_SECRET pulled from Vault. `creationPolicy: Merge` means ESO will not
# take ownership it just adds/updates the keys it manages, leaving the
# Helm-owned Secret resource intact. The chart's deployment already wires
# this Secret in via `envFrom: secretRef: postiz-secrets`.
resource "kubernetes_manifest" "external_secret_jwt" {
field_manager {
force_conflicts = true
}
manifest = {
apiVersion = "external-secrets.io/v1"
kind = "ExternalSecret"
metadata = {
name = "postiz-jwt-secret"
namespace = kubernetes_namespace.postiz.metadata[0].name
}
spec = {
refreshInterval = "15m"
secretStoreRef = {
name = "vault-kv"
kind = "ClusterSecretStore"
}
target = {
name = "postiz-secrets"
creationPolicy = "Merge"
}
data = [
{
secretKey = "JWT_SECRET"
remoteRef = { key = "instagram-poster", property = "postiz_jwt_secret" }
},
{
secretKey = "FACEBOOK_APP_ID"
remoteRef = { key = "instagram-poster", property = "facebook_app_id" }
},
{
secretKey = "FACEBOOK_APP_SECRET"
remoteRef = { key = "instagram-poster", property = "facebook_app_secret" }
},
{
secretKey = "INSTAGRAM_APP_ID"
remoteRef = { key = "instagram-poster", property = "instagram_app_id" }
},
{
secretKey = "INSTAGRAM_APP_SECRET"
remoteRef = { key = "instagram-poster", property = "instagram_app_secret" }
},
]
}
}
depends_on = [kubernetes_namespace.postiz]
# Vault-sourced secret env for the chart's `secrets:` block. The values are
# static, so injecting them straight into the chart-managed Secret avoids the
# ESO-merge-vs-helm-reset race and the Reloader requirement.
# secret/postiz -> database_url (shared CNPG; postiz role, static pw)
# secret/instagram-poster -> JWT + Facebook/Instagram OAuth app creds (the same
# Vault keys the old ESO used; shared with the
# instagram-poster pipeline that drives the public API)
data "vault_kv_secret_v2" "postiz" {
mount = "secret"
name = "postiz"
}
# 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.
data "vault_kv_secret_v2" "instagram_poster" {
mount = "secret"
name = "instagram-poster"
}
# Postiz Helm release Terraform-managed (2026-06-16), replacing the stuck
# out-of-band pending-install release. Bundled PG/Redis subcharts disabled; the
# app runs against shared CNPG + shared Redis. Chart name is `postiz-app`.
resource "helm_release" "postiz" {
name = "postiz"
namespace = kubernetes_namespace.postiz.metadata[0].name
repository = "oci://ghcr.io/gitroomhq/postiz-helmchart/charts"
chart = "postiz-app"
version = var.chart_version
# No atomic/auto-rollback on first install so a bad boot is debuggable, not
# silently rolled back. wait=false so the apply doesn't block on pod readiness.
atomic = false
wait = false
timeout = 600
values = [yamlencode({
fullnameOverride = "postiz"
# PARKED (2026-06-28): postiz + temporal scaled to 0 IG-via-postiz is
# Meta-blocked (retired scopes); IG runs via the instagram-poster service.
# To revive: set this + temporal replicas back to 1 (and re-check image pins).
replicaCount = 0
image = {
repository = "ghcr.io/gitroomhq/postiz-app"
tag = var.image_tag
pullPolicy = "IfNotPresent"
}
service = {
type = "ClusterIP"
port = 80
}
# Bundled subcharts OFF use shared CNPG + shared Redis instead.
postgresql = { enabled = false }
redis = { enabled = false }
resources = {
# request lowered 2Gi->1Gi so the tier-4-aux ns requests.memory quota (3Gi)
# fits postiz + temporal + the new Elasticsearch; limit stays 3Gi (Burstable).
requests = { cpu = "100m", memory = "1Gi" }
limits = { memory = "3Gi" }
}
# Non-secret env (chart renders these into the postiz-config ConfigMap).
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"
TEMPORAL_ADDRESS = "temporal:7233"
STORAGE_PROVIDER = "local"
UPLOAD_DIRECTORY = "/uploads"
NEXT_PUBLIC_UPLOAD_DIRECTORY = "/uploads"
IS_GENERAL = "true"
NX_ADD_PLUGINS = "false"
DISABLE_REGISTRATION = "true"
# Only Instagram + Facebook are enabled (shared Meta app creds); every
# other provider stays disabled until its own OAuth app is registered.
DISABLED_PROVIDERS = "x,linkedin,reddit,threads,youtube,tiktok,pinterest,dribbble,slack,discord,mastodon,bluesky,lemmy,warpcast,vk,beehiiv,telegram,wordpress,nostr,farcaster"
# Authentik OIDC ("Login with Authentik") provider/app in authentik.tf.
# This Postiz version reads only the AUTH/TOKEN/USERINFO URLs + client
# id/secret (scope is hardcoded openid/profile/email; redirect is fixed to
# ${FRONTEND_URL}/settings). The NEXT_PUBLIC display name drives the login
# button label (best-effort: baked at image build, set here in case the
# image re-injects NEXT_PUBLIC vars at start).
POSTIZ_GENERIC_OAUTH = "true"
POSTIZ_OAUTH_AUTH_URL = "https://authentik.viktorbarzin.me/application/o/authorize/"
POSTIZ_OAUTH_TOKEN_URL = "https://authentik.viktorbarzin.me/application/o/token/"
POSTIZ_OAUTH_USERINFO_URL = "https://authentik.viktorbarzin.me/application/o/userinfo/"
POSTIZ_OAUTH_CLIENT_ID = "postiz"
NEXT_PUBLIC_POSTIZ_OAUTH_DISPLAY_NAME = "Authentik"
}
# Secret env (chart renders these into the postiz-secrets Secret, envFrom).
secrets = {
DATABASE_URL = data.vault_kv_secret_v2.postiz.data["database_url"]
REDIS_URL = "redis://redis-master.redis.svc.cluster.local:6379/11"
JWT_SECRET = data.vault_kv_secret_v2.instagram_poster.data["postiz_jwt_secret"]
FACEBOOK_APP_ID = data.vault_kv_secret_v2.instagram_poster.data["facebook_app_id"]
FACEBOOK_APP_SECRET = data.vault_kv_secret_v2.instagram_poster.data["facebook_app_secret"]
INSTAGRAM_APP_ID = data.vault_kv_secret_v2.instagram_poster.data["instagram_app_id"]
INSTAGRAM_APP_SECRET = data.vault_kv_secret_v2.instagram_poster.data["instagram_app_secret"]
POSTIZ_OAUTH_CLIENT_SECRET = var.oauth_client_secret
}
# Persist uploaded media on the existing proxmox-lvm PVC.
extraVolumes = [{
name = "uploads-volume"
persistentVolumeClaim = { claimName = kubernetes_persistent_volume_claim.uploads.metadata[0].name }
}]
extraVolumeMounts = [{
name = "uploads-volume"
mountPath = "/uploads"
}]
})]
depends_on = [kubernetes_namespace.postiz, module.tls_secret]
}
# 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
@ -167,14 +222,263 @@ module "ingress" {
}
#
# 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).
# Temporal Postiz's workflow backend. RESTORED 2026-06-16 (#44). Postiz's
# backend REFUSES to start its HTTP server unless temporal:7233 is reachable at
# boot so this is required for Postiz to serve ANYTHING (login + the public
# API), not just scheduled posting. Runs temporalio/auto-setup against the shared
# CNPG cluster with the SQL/Postgres visibility store (no Elasticsearch). The
# `temporal` + `temporal_visibility` DBs already exist and are owned by the
# `postiz` role, so SKIP_DB_CREATE=true + the role's static password is enough
# (auto-setup only creates/updates schema, which the DB owner can do; it is NOT
# superuser and must not attempt CREATE DATABASE).
#
#
# Elasticsearch Temporal's VISIBILITY store (#44). Required: Postiz registers
# >3 custom Text search attributes, which Temporal's Postgres visibility caps at
# 3 ("cannot have more than 3 search attribute of type Text"). Postiz's upstream
# docker-compose uses ES for exactly this reason. Single-node, security off.
# `node.store.allow_mmap=false` avoids the vm.max_map_count bootstrap check so we
# don't need a privileged sysctl init-container (blocked by Kyverno wave-1).
#
resource "kubernetes_persistent_volume_claim" "es" {
wait_until_bound = false
metadata {
name = "postiz-es-data"
namespace = kubernetes_namespace.postiz.metadata[0].name
annotations = {
"resize.topolvm.io/threshold" = "10%"
"resize.topolvm.io/increase" = "100%"
"resize.topolvm.io/storage_limit" = "20Gi"
}
}
spec {
access_modes = ["ReadWriteOnce"]
storage_class_name = "proxmox-lvm"
resources {
requests = { storage = "8Gi" }
}
}
lifecycle {
ignore_changes = [spec[0].resources[0].requests]
}
}
resource "kubernetes_deployment_v1" "es" {
metadata {
name = "elasticsearch"
namespace = kubernetes_namespace.postiz.metadata[0].name
labels = { app = "elasticsearch" }
}
spec {
replicas = 1
selector { match_labels = { app = "elasticsearch" } }
strategy { type = "Recreate" }
template {
metadata { labels = { app = "elasticsearch" } }
spec {
security_context { fs_group = 1000 } # ES runs as uid 1000; make the PVC writable
# proxmox-lvm CSI doesn't honor fsGroup, so the PVC mounts root-owned and
# ES (uid 1000) can't write its data dir. Chown it via a root init-container
# (not privileged passes Kyverno deny-privileged).
init_container {
name = "fix-data-perms"
image = "busybox:1.36"
command = ["sh", "-c", "chown -R 1000:1000 /usr/share/elasticsearch/data"]
security_context { run_as_user = 0 }
volume_mount {
name = "data"
mount_path = "/usr/share/elasticsearch/data"
}
}
container {
name = "elasticsearch"
# docker.io/ prefix matches the Kyverno-trusted `docker.io/*` allowlist
# (Elastic also publishes the official image to Docker Hub). ES 7.17 == Temporal ES_VERSION=v7.
image = "docker.io/library/elasticsearch:7.17.28"
env {
name = "discovery.type"
value = "single-node"
}
env {
name = "xpack.security.enabled"
value = "false"
}
env {
name = "node.store.allow_mmap"
value = "false"
}
env {
name = "cluster.routing.allocation.disk.threshold_enabled"
value = "false"
}
env {
name = "ingest.geoip.downloader.enabled"
value = "false"
}
env {
name = "ES_JAVA_OPTS"
value = "-Xms512m -Xmx512m"
}
port {
name = "http"
container_port = 9200
}
volume_mount {
name = "data"
mount_path = "/usr/share/elasticsearch/data"
}
resources {
requests = { cpu = "100m", memory = "1Gi" }
limits = { memory = "1536Mi" }
}
readiness_probe {
http_get {
path = "/_cluster/health?local=true"
port = 9200
}
initial_delay_seconds = 20
period_seconds = 10
failure_threshold = 18
}
}
volume {
name = "data"
persistent_volume_claim {
claim_name = kubernetes_persistent_volume_claim.es.metadata[0].name
}
}
}
}
}
lifecycle {
ignore_changes = [spec[0].template[0].spec[0].dns_config] # KYVERNO_LIFECYCLE_V1
}
}
resource "kubernetes_service" "es" {
metadata {
name = "elasticsearch"
namespace = kubernetes_namespace.postiz.metadata[0].name
}
spec {
selector = { app = "elasticsearch" }
port {
name = "http"
port = 9200
target_port = 9200
}
}
}
resource "kubernetes_secret" "temporal_db" {
metadata {
name = "temporal-db"
namespace = kubernetes_namespace.postiz.metadata[0].name
}
data = {
POSTGRES_PWD = data.vault_kv_secret_v2.postiz.data["db_password"]
}
}
resource "kubernetes_deployment_v1" "temporal" {
metadata {
name = "temporal"
namespace = kubernetes_namespace.postiz.metadata[0].name
labels = { app = "temporal" }
}
spec {
replicas = 0 # PARKED with postiz (2026-06-28) revive: set to 1
selector { match_labels = { app = "temporal" } }
strategy { type = "Recreate" }
template {
metadata { labels = { app = "temporal" } }
spec {
container {
name = "temporal"
image = "temporalio/auto-setup:1.28.1"
env {
name = "DB"
value = "postgres12"
}
env {
name = "SKIP_DB_CREATE"
value = "true"
}
env {
name = "DBNAME"
value = "temporal"
}
# Visibility = Elasticsearch (Postiz needs >3 Text search attributes,
# which SQL visibility can't hold). Persistence stays on CNPG (DBNAME).
env {
name = "ENABLE_ES"
value = "true"
}
env {
name = "ES_SEEDS"
value = "elasticsearch.postiz.svc.cluster.local"
}
env {
name = "ES_VERSION"
value = "v7"
}
env {
name = "ES_PORT"
value = "9200"
}
env {
name = "ES_SCHEME"
value = "http"
}
env {
name = "POSTGRES_SEEDS"
value = "pg-cluster-rw.dbaas.svc.cluster.local"
}
env {
name = "DB_PORT"
value = "5432"
}
env {
name = "POSTGRES_USER"
value = "postiz"
}
env {
name = "POSTGRES_PWD"
value_from {
secret_key_ref {
name = kubernetes_secret.temporal_db.metadata[0].name
key = "POSTGRES_PWD"
}
}
}
port {
name = "grpc"
container_port = 7233
}
resources {
requests = { cpu = "50m", memory = "256Mi" }
limits = { memory = "512Mi" }
}
readiness_probe {
tcp_socket { port = 7233 }
initial_delay_seconds = 20
period_seconds = 10
failure_threshold = 12
}
}
}
}
}
depends_on = [kubernetes_deployment_v1.es, kubernetes_service.es]
lifecycle {
ignore_changes = [spec[0].template[0].spec[0].dns_config] # KYVERNO_LIFECYCLE_V1
}
}
resource "kubernetes_service" "temporal" {
metadata {
name = "temporal"
@ -191,18 +495,17 @@ resource "kubernetes_service" "temporal" {
}
#
# Backup CronJob nightly pg_dump of the postiz database to NFS.
# Backup CronJob nightly pg_dump of the bundled postiz-postgresql to NFS.
#
# Postiz's database lives on the SHARED CNPG cluster
# (pg-cluster-rw.dbaas.svc.cluster.local/postiz) the chart's bundled
# PostgreSQL was dropped in the CNPG migration, so the old `postiz-postgresql`
# host no longer resolves (this CronJob was failing on it for weeks
# BackupCronJobFailed; repointed 2026-06-26). The dump now connects via the
# app's own DATABASE_URL (from the postiz-secrets Secret) so it always tracks
# the live host + credentials. Dumps land on /srv/nfs/postiz-backup/ covered
# by inotify-driven offsite sync to Synology, closing the gap (CNPG data PVCs
# live in dbaas, excluded from the LVM-snapshot leg). Only the postiz app DB is
# dumped here; temporal's DBs are not.
# The bundled PostgreSQL StatefulSet uses local-path storage on the K8s node
# OS disk (chart default), which is NOT covered by Layer 1 (LVM thin
# snapshots) or Layer 2 (sda file backup) of the 3-2-1 pipeline. A pg_dump
# CronJob writing to /srv/nfs/postiz-backup/ closes the gap: dumps land on
# Proxmox host NFS covered by inotify-driven offsite sync to Synology.
# Three databases are dumped: postiz (app data), temporal (workflow engine),
# temporal_visibility (workflow search). Bitnami chart-default credentials
# are used same creds the Postiz pod itself uses, scoped to the postiz
# namespace via ClusterIP-only Services.
#
module "nfs_backup_host" {
@ -252,9 +555,10 @@ resource "kubernetes_cron_job_v1" "postgres_backup" {
STATUS=0
for db in postiz; do
echo "Dumping $db..."
if pg_dump -d "$DATABASE_URL" \
if PGPASSWORD=postiz-password pg_dump -h postiz-postgresql -U postiz \
--format=custom --compress=6 \
--file="$BACKUP_DIR/$db-$TIMESTAMP.dump"; then
--file="$BACKUP_DIR/$db-$TIMESTAMP.dump" \
"$db"; then
echo " OK: $db ($(du -h "$BACKUP_DIR/$db-$TIMESTAMP.dump" | cut -f1))"
else
echo " FAIL: $db" >&2
@ -271,18 +575,6 @@ resource "kubernetes_cron_job_v1" "postgres_backup" {
exit $STATUS
EOT
]
# Connect to the live CNPG database using the app's own
# DATABASE_URL (postgresql://postiz:@pg-cluster-rw.dbaas/postiz)
# instead of a hardcoded host/password survives credential changes.
env {
name = "DATABASE_URL"
value_from {
secret_key_ref {
name = "postiz-secrets"
key = "DATABASE_URL"
}
}
}
volume_mount {
name = "backup"
mount_path = "/backup"

View file

@ -38,3 +38,9 @@ variable "storage_size" {
default = "20Gi"
description = "Persistent volume size for /uploads."
}
variable "oauth_client_secret" {
type = string
sensitive = true
description = "Authentik OIDC client secret for Postiz generic OAuth (from authentik_provider_oauth2.postiz)."
}