add postiz + instagram-poster stacks for IG Stories pipeline
New stacks: - stacks/postiz/ — Postiz scheduler (Helm chart v1.0.5, image v2.21.7) with bundled PG/Redis, /uploads PVC on proxmox-lvm, JWT_SECRET via ESO from secret/instagram-poster. - stacks/instagram-poster/ — custom Python service that polls Immich for the 'instagram' tag, reformats photos to 9:16 with blurred-bg letterbox, exposes /image/<asset_id> publicly so Postiz can fetch. Image: forgejo.viktorbarzin.me/viktor/instagram-poster. n8n: 3 new workflows (discover, approval, post) for the Telegram inline-button approval UX. Adds ExternalSecret + env vars for TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, IMMICH_API_KEY, plus static URLs for the new service. Vault: seed secret/instagram-poster with telegram_bot_token, telegram_chat_id, immich_api_key, postiz_api_token, postiz_jwt_secret before applying.
This commit is contained in:
parent
badc341669
commit
73eb01f994
14 changed files with 1276 additions and 0 deletions
223
stacks/postiz/modules/postiz/main.tf
Normal file
223
stacks/postiz/modules/postiz/main.tf
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
# Postiz — social media post scheduler (Instagram Stories + others).
|
||||
#
|
||||
# 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.
|
||||
# - 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.
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
resource "kubernetes_namespace" "postiz" {
|
||||
metadata {
|
||||
name = var.namespace
|
||||
labels = {
|
||||
tier = var.tier
|
||||
}
|
||||
}
|
||||
lifecycle {
|
||||
# KYVERNO_LIFECYCLE_V1: goldilocks-vpa-auto-mode ClusterPolicy stamps this label on every namespace
|
||||
ignore_changes = [metadata[0].labels["goldilocks.fairwinds.com/vpa-update-mode"]]
|
||||
}
|
||||
}
|
||||
|
||||
module "tls_secret" {
|
||||
source = "../../../../modules/kubernetes/setup_tls_secret"
|
||||
namespace = kubernetes_namespace.postiz.metadata[0].name
|
||||
tls_secret_name = var.tls_secret_name
|
||||
}
|
||||
|
||||
# /uploads PVC — keeps user-uploaded media across pod restarts.
|
||||
resource "kubernetes_persistent_volume_claim" "uploads" {
|
||||
wait_until_bound = false
|
||||
metadata {
|
||||
name = "postiz-uploads"
|
||||
namespace = kubernetes_namespace.postiz.metadata[0].name
|
||||
annotations = {
|
||||
"resize.topolvm.io/threshold" = "80%"
|
||||
"resize.topolvm.io/increase" = "100%"
|
||||
"resize.topolvm.io/storage_limit" = "50Gi"
|
||||
}
|
||||
}
|
||||
spec {
|
||||
access_modes = ["ReadWriteOnce"]
|
||||
storage_class_name = "proxmox-lvm"
|
||||
resources {
|
||||
requests = {
|
||||
storage = var.storage_size
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 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" {
|
||||
manifest = {
|
||||
apiVersion = "external-secrets.io/v1beta1"
|
||||
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"
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
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"
|
||||
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"
|
||||
# Set true after first admin user is created via UI
|
||||
DISABLE_REGISTRATION = "false"
|
||||
IS_GENERAL = "true"
|
||||
NX_ADD_PLUGINS = "false"
|
||||
}
|
||||
|
||||
# Empty placeholder for chart-rendered Secret. ESO patches JWT_SECRET via
|
||||
# creationPolicy=Merge above. DATABASE_URL/REDIS_URL are auto-wired by the
|
||||
# chart's bundled subcharts and don't need to be set here.
|
||||
secrets = {
|
||||
DATABASE_URL = ""
|
||||
REDIS_URL = ""
|
||||
JWT_SECRET = ""
|
||||
X_API_KEY = ""
|
||||
X_API_SECRET = ""
|
||||
LINKEDIN_CLIENT_ID = ""
|
||||
LINKEDIN_CLIENT_SECRET = ""
|
||||
REDDIT_CLIENT_ID = ""
|
||||
REDDIT_CLIENT_SECRET = ""
|
||||
GITHUB_CLIENT_ID = ""
|
||||
GITHUB_CLIENT_SECRET = ""
|
||||
RESEND_API_KEY = ""
|
||||
CLOUDFLARE_ACCOUNT_ID = ""
|
||||
CLOUDFLARE_ACCESS_KEY = ""
|
||||
CLOUDFLARE_SECRET_ACCESS_KEY = ""
|
||||
CLOUDFLARE_BUCKETNAME = ""
|
||||
CLOUDFLARE_BUCKET_URL = ""
|
||||
}
|
||||
|
||||
# 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"
|
||||
}]
|
||||
|
||||
resources = {
|
||||
requests = {
|
||||
cpu = "100m"
|
||||
memory = "256Mi"
|
||||
}
|
||||
limits = {
|
||||
memory = "2Gi"
|
||||
}
|
||||
}
|
||||
|
||||
# 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.
|
||||
postgresql = {
|
||||
enabled = true
|
||||
auth = {
|
||||
username = "postiz"
|
||||
database = "postiz"
|
||||
}
|
||||
}
|
||||
|
||||
redis = {
|
||||
enabled = true
|
||||
}
|
||||
})]
|
||||
|
||||
depends_on = [
|
||||
kubernetes_persistent_volume_claim.uploads,
|
||||
kubernetes_manifest.external_secret_jwt,
|
||||
]
|
||||
}
|
||||
|
||||
module "ingress" {
|
||||
source = "../../../../modules/kubernetes/ingress_factory"
|
||||
dns_type = "proxied"
|
||||
namespace = kubernetes_namespace.postiz.metadata[0].name
|
||||
name = "postiz"
|
||||
host = var.host
|
||||
service_name = "postiz" # chart Service name resolves to fullnameOverride
|
||||
port = 80
|
||||
tls_secret_name = var.tls_secret_name
|
||||
extra_annotations = {
|
||||
"gethomepage.dev/enabled" = "true"
|
||||
"gethomepage.dev/name" = "Postiz"
|
||||
"gethomepage.dev/description" = "Social media post scheduler"
|
||||
"gethomepage.dev/icon" = "postiz.png"
|
||||
"gethomepage.dev/group" = "Automation"
|
||||
"gethomepage.dev/pod-selector" = ""
|
||||
}
|
||||
}
|
||||
40
stacks/postiz/modules/postiz/variables.tf
Normal file
40
stacks/postiz/modules/postiz/variables.tf
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
variable "tls_secret_name" {
|
||||
type = string
|
||||
sensitive = true
|
||||
description = "Name of the wildcard TLS Secret to copy into the postiz namespace."
|
||||
}
|
||||
|
||||
variable "tier" {
|
||||
type = string
|
||||
description = "Workload tier label applied to the namespace (e.g. 4-aux)."
|
||||
}
|
||||
|
||||
variable "namespace" {
|
||||
type = string
|
||||
default = "postiz"
|
||||
description = "Kubernetes namespace for Postiz."
|
||||
}
|
||||
|
||||
variable "host" {
|
||||
type = string
|
||||
default = "postiz"
|
||||
description = "Ingress hostname label (joined with root_domain by ingress_factory)."
|
||||
}
|
||||
|
||||
variable "image_tag" {
|
||||
type = string
|
||||
default = "v2.21.7"
|
||||
description = "Postiz container image tag."
|
||||
}
|
||||
|
||||
variable "chart_version" {
|
||||
type = string
|
||||
default = "1.0.5"
|
||||
description = "Postiz Helm chart version (OCI ghcr.io/gitroomhq/postiz-helmchart)."
|
||||
}
|
||||
|
||||
variable "storage_size" {
|
||||
type = string
|
||||
default = "20Gi"
|
||||
description = "Persistent volume size for /uploads."
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue