feat: add Listmonk newsletter platform on Legion k3s

- Namespace, ExternalSecrets (Vault), DB init job, deployment, service, PVC, ingress
- Routes newsletter.harmonic-framework.com via existing Cloudflare tunnel
- Uses shared platform postgres with dedicated listmonk DB/user
- Credentials stored at secret/data/listmonk in Vault
This commit is contained in:
Will Anderson
2026-04-04 18:43:44 -05:00
parent eefaeafcfc
commit bf40bd7f15
7 changed files with 301 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: listmonk
namespace: argocd
spec:
project: default
source:
repoURL: https://git.neuralplatform.ai/will/infrastructure.git
targetRevision: main
path: servers/legion/k8s/listmonk
destination:
server: https://kubernetes.default.svc
namespace: listmonk
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
@@ -0,0 +1,56 @@
---
# One-time job to create the listmonk database and user in the shared postgres.
# Safe to re-run (DO $$ guards prevent duplicate creation).
# Argo CD will re-apply if deleted; ttlSecondsAfterFinished cleans it up automatically.
apiVersion: batch/v1
kind: Job
metadata:
name: listmonk-db-init
namespace: listmonk
spec:
ttlSecondsAfterFinished: 300
template:
spec:
restartPolicy: OnFailure
containers:
- name: psql
image: postgres:16-alpine
command:
- sh
- -c
- |
PG_URI="postgresql://postgres:${POSTGRES_PASSWORD}@postgres-postgresql.platform.svc.cluster.local:5432/postgres"
# Create user if missing
psql "$PG_URI" -c "DO \$\$ BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${DB_USER}') THEN
EXECUTE format('CREATE USER %I WITH PASSWORD %L', '${DB_USER}', '${DB_PASSWORD}');
END IF;
END \$\$;"
# Create DB if missing
psql "$PG_URI" -c "SELECT 'CREATE DATABASE ${DB_NAME} OWNER ${DB_USER}'" \
| grep -q 'CREATE DATABASE' && \
psql "$PG_URI" -c "CREATE DATABASE ${DB_NAME} OWNER ${DB_USER};" || true
# Ensure privileges
psql "$PG_URI" -c "GRANT ALL PRIVILEGES ON DATABASE ${DB_NAME} TO ${DB_USER};"
echo "DB init complete."
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: postgres-superuser
key: password
- name: DB_USER
valueFrom:
secretKeyRef:
name: listmonk-secrets
key: db_user
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: listmonk-secrets
key: db_password
- name: DB_NAME
valueFrom:
secretKeyRef:
name: listmonk-secrets
key: db_name
+121
View File
@@ -0,0 +1,121 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: listmonk
namespace: listmonk
spec:
replicas: 1
selector:
matchLabels:
app: listmonk
template:
metadata:
labels:
app: listmonk
spec:
initContainers:
# Run `--install` on first boot to create the schema; no-op on subsequent starts
- name: listmonk-install
image: listmonk/listmonk:v4.1.0
args: ["--install", "--yes", "--idempotent"]
env:
- name: LISTMONK_db__host
value: "postgres-postgresql.platform.svc.cluster.local"
- name: LISTMONK_db__port
value: "5432"
- name: LISTMONK_db__name
valueFrom:
secretKeyRef:
name: listmonk-secrets
key: db_name
- name: LISTMONK_db__user
valueFrom:
secretKeyRef:
name: listmonk-secrets
key: db_user
- name: LISTMONK_db__password
valueFrom:
secretKeyRef:
name: listmonk-secrets
key: db_password
- name: LISTMONK_db__ssl_mode
value: "disable"
- name: LISTMONK_app__admin_username
value: "admin"
- name: LISTMONK_app__admin_password
valueFrom:
secretKeyRef:
name: listmonk-secrets
key: admin_password
containers:
- name: listmonk
image: listmonk/listmonk:v4.1.0
ports:
- containerPort: 9000
env:
- name: LISTMONK_db__host
value: "postgres-postgresql.platform.svc.cluster.local"
- name: LISTMONK_db__port
value: "5432"
- name: LISTMONK_db__name
valueFrom:
secretKeyRef:
name: listmonk-secrets
key: db_name
- name: LISTMONK_db__user
valueFrom:
secretKeyRef:
name: listmonk-secrets
key: db_user
- name: LISTMONK_db__password
valueFrom:
secretKeyRef:
name: listmonk-secrets
key: db_password
- name: LISTMONK_db__ssl_mode
value: "disable"
- name: LISTMONK_app__admin_username
value: "admin"
- name: LISTMONK_app__admin_password
valueFrom:
secretKeyRef:
name: listmonk-secrets
key: admin_password
- name: LISTMONK_app__secret_key
valueFrom:
secretKeyRef:
name: listmonk-secrets
key: app_key
volumeMounts:
- name: uploads
mountPath: /listmonk/uploads
readinessProbe:
httpGet:
path: /health
port: 9000
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
volumes:
- name: uploads
persistentVolumeClaim:
claimName: listmonk-uploads
---
apiVersion: v1
kind: Service
metadata:
name: listmonk
namespace: listmonk
spec:
selector:
app: listmonk
ports:
- port: 9000
targetPort: 9000
@@ -0,0 +1,56 @@
---
# listmonk-secrets — DB credentials and admin password from Vault
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: listmonk-secrets
namespace: listmonk
spec:
refreshInterval: 1h
secretStoreRef:
name: vault
kind: ClusterSecretStore
target:
name: listmonk-secrets
creationPolicy: Owner
data:
- secretKey: db_password
remoteRef:
key: secret/data/listmonk
property: db_password
- secretKey: db_user
remoteRef:
key: secret/data/listmonk
property: db_user
- secretKey: db_name
remoteRef:
key: secret/data/listmonk
property: db_name
- secretKey: admin_password
remoteRef:
key: secret/data/listmonk
property: admin_password
- secretKey: app_key
remoteRef:
key: secret/data/listmonk
property: app_key
---
# postgres superuser password — needed for DB init job
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: postgres-superuser
namespace: listmonk
spec:
refreshInterval: 1h
secretStoreRef:
name: vault
kind: ClusterSecretStore
target:
name: postgres-superuser
creationPolicy: Owner
data:
- secretKey: password
remoteRef:
key: secret/data/legion-db
property: postgres_password
+26
View File
@@ -0,0 +1,26 @@
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: listmonk
namespace: listmonk
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: traefik
tls:
- hosts:
- newsletter.harmonic-framework.com
secretName: listmonk-tls
rules:
- host: newsletter.harmonic-framework.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: listmonk
port:
number: 9000
+12
View File
@@ -0,0 +1,12 @@
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: listmonk-uploads
namespace: listmonk
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
+10
View File
@@ -47,3 +47,13 @@ resource "kubernetes_namespace" "harmonic_wordpress" {
}
}
}
resource "kubernetes_namespace" "listmonk" {
metadata {
name = "listmonk"
labels = {
managed-by = "terraform"
tier = "apps"
}
}
}