Initial commit: UmbraDatastore configuration and deployment scripts

- Database deployment script (scripts/deploy-database.sh)
- Kubernetes manifests for CNPG PostgreSQL cluster (k8s/base/)
- Documentation (docs/DEPLOYMENT_GUIDE.md, DATASTORE_SPEC.md, INTEGRATION_SPEC.md)
- README with architecture overview and operations guide
This commit is contained in:
umbra audio
2026-10-31 19:47:27 -07:00
commit cce2ce55dc
14 changed files with 3666 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
# Copyright 2026 Umbra Tools (umbratools.com)
# SPDX-License-Identifier: Apache-2.0
apiVersion: v1
kind: Namespace
metadata:
name: umbratrans
labels:
name: umbratrans
environment: production

View File

@@ -0,0 +1,97 @@
# Copyright 2026 Umbra Tools (umbratools.com)
# SPDX-License-Identifier: Apache-2.0
# CloudNativePG Cluster for UmbraTrans
# Requires CNPG operator installed: kubectl apply -f https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.24/releases/cnpg-1.24.0.yaml
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: umbratrans-db
namespace: umbratrans
labels:
app: umbratrans-db
environment: production
spec:
instances: 3
imageName: ghcr.io/cloudnative-pg/postgresql:16.4
primaryUpdateStrategy: unsupervised
postgresql:
parameters:
max_connections: "200"
shared_buffers: "256MB"
effective_cache_size: "1GB"
work_mem: "16MB"
maintenance_work_mem: "128MB"
max_worker_processes: "8"
max_parallel_workers_per_gather: "4"
max_parallel_workers: "8"
wal_buffers: "16MB"
min_wal_size: "1GB"
max_wal_size: "4GB"
checkpoint_completion_target: "0.9"
random_page_cost: "1.1"
effective_io_concurrency: "200"
log_destination: "stderr"
logging_collector: "off"
log_min_duration_statement: "1000"
log_line_prefix: "%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h "
log_checkpoints: "on"
log_connections: "on"
log_disconnections: "on"
log_lock_waits: "on"
log_temp_files: "10MB"
log_autovacuum_min_duration: "1000"
statement_timeout: "300000"
idle_in_transaction_session_timeout: "300000"
bootstrap:
initdb:
database: umbratrans
owner: umbratrans
secret:
name: umbratrans-db-credentials
postInitApplicationSQL:
- CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
- CREATE EXTENSION IF NOT EXISTS "citext";
- CREATE EXTENSION IF NOT EXISTS "pgcrypto";
storage:
storageClass: nfs-client
size: 50Gi
backup:
barmanObjectStore:
destinationPath: s3://umbra-backups/postgresql/umbratrans-db
endpointURL: https://s3.example.com
s3Credentials:
accessKeyId:
name: umbratrans-backup-credentials
key: ACCESS_KEY_ID
secretAccessKey:
name: umbratrans-backup-credentials
key: SECRET_ACCESS_KEY
retentionPolicy: "30d"
schedule: "0 2 * * *"
proxy:
pgBouncer:
imageName: ghcr.io/cloudnative-pg/pgbouncer:1.23
replicas: 2
poolMode: transaction
maxClientConn: 2000
defaultPoolSize: 20
maxDbConnections: 100
admin:
secret:
name: umbratrans-pgbouncer-admin
monitoring:
enablePodMonitor: true
customQueriesConfigMap:
- name: umbratrans-db-queries
resources:
requests:
memory: "1Gi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2000m"
affinity:
podAntiAffinityType: preferred
topologyKey: topology.kubernetes.io/zone

View File

@@ -0,0 +1,150 @@
# Copyright 2026 Umbra Tools (umbratools.com)
# SPDX-License-Identifier: Apache-2.0
# NFS CSI Driver installation (apply once per cluster)
# kubectl apply -f https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/v4.7.0/deploy/example/nfs-driver.yaml
---
# StorageClass for NFS
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-client
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: nfs.csi.k8s.io
parameters:
server: 192.168.123.14 # NFS server (this machine)
share: /mnt/umbra-data
reclaimPolicy: Retain
volumeBindingMode: Immediate
mountOptions:
- hard
- nfsvers=4.1
- rsize=1048576
- wsize=1048576
- timeo=600
- retrans=2
---
# PersistentVolume for existing productions data
apiVersion: v1
kind: PersistentVolume
metadata:
name: umbratrans-productions
labels:
app: umbratrans
data: productions
spec:
capacity:
storage: 100Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: nfs-client
csi:
driver: nfs.csi.k8s.io
volumeHandle: umbratrans-productions
volumeAttributes:
server: 192.168.123.14 # NFS server (this machine)
share: /mnt/umbra-data/productions
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: umbratrans-productions
namespace: umbratrans
labels:
app: umbratrans
data: productions
spec:
accessModes:
- ReadWriteMany
storageClassName: nfs-client
resources:
requests:
storage: 100Gi
volumeName: umbratrans-productions
---
# PersistentVolume for artifacts (audio files, covers, etc.)
apiVersion: v1
kind: PersistentVolume
metadata:
name: umbratrans-artifacts
labels:
app: umbratrans
data: artifacts
spec:
capacity:
storage: 500Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: nfs-client
csi:
driver: nfs.csi.k8s.io
volumeHandle: umbratrans-artifacts
volumeAttributes:
server: 192.168.123.14
share: /mnt/umbra-data/artifacts
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: umbratrans-artifacts
namespace: umbratrans
labels:
app: umbratrans
data: artifacts
spec:
accessModes:
- ReadWriteMany
storageClassName: nfs-client
resources:
requests:
storage: 500Gi
volumeName: umbratrans-artifacts
---
# PersistentVolume for database backups (optional, separate from PG backup)
apiVersion: v1
kind: PersistentVolume
metadata:
name: umbratrans-backups
labels:
app: umbratrans
data: backups
spec:
capacity:
storage: 200Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: nfs-client
csi:
driver: nfs.csi.k8s.io
volumeHandle: umbratrans-backups
volumeAttributes:
server: 192.168.123.14
share: /mnt/umbra-data/backups
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: umbratrans-backups
namespace: umbratrans
labels:
app: umbratrans
data: backups
spec:
accessModes:
- ReadWriteMany
storageClassName: nfs-client
resources:
requests:
storage: 200Gi
volumeName: umbratrans-backups

View File

@@ -0,0 +1,284 @@
# Copyright 2026 Umbra Tools (umbratools.com)
# SPDX-License-Identifier: Apache-2.0
---
# ServiceAccount for web
apiVersion: v1
kind: ServiceAccount
metadata:
name: umbratrans-web
namespace: umbratrans
labels:
app: umbratrans-web
---
# Role for web (minimal permissions)
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: umbratrans-web
namespace: umbratrans
rules:
- apiGroups: [""]
resources: ["pods", "services", "configmaps"]
verbs: ["get", "list", "watch"]
- apiGroups: ["postgresql.cnpg.io"]
resources: ["clusters", "backups"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: umbratrans-web
namespace: umbratrans
subjects:
- kind: ServiceAccount
name: umbratrans-web
namespace: umbratrans
roleRef:
kind: Role
name: umbratrans-web
apiGroup: rbac.authorization.k8s.io
---
# ConfigMap for static files (served by web)
apiVersion: v1
kind: ConfigMap
metadata:
name: umbratrans-static
namespace: umbratrans
labels:
app: umbratrans-web
data:
# Static files are built into the image, this is for runtime config
nginx.conf: |
# Not used - FastAPI serves static files directly
---
# Secret for web configuration (non-sensitive)
apiVersion: v1
kind: Secret
metadata:
name: umbratrans-web-config
namespace: umbratrans
type: Opaque
stringData:
UMBRA_ENV: "production"
UMBRATRANS_REPOSITORY: "postgres"
UMBRATRANS_PRODUCTIONS_ROOT: "/mnt/productions"
UMBRATRANS_STATIC_ROOT: "/app/web"
UMBRATRANS_ALLOWED_HOSTS: "umbratrans.bdmurphy.me,localhost"
UMBRATRANS_VERIFICATION_BASE_URL: "https://umbratrans.bdmurphy.me"
UMBRATRANS_COOKIE_SECURE: "true"
UMBRATRANS_REQUIRE_CSRF: "true"
UMBRATRANS_MAILER: "smtp"
UMBRATRANS_SMTP_HOST: "smtp.example.com"
UMBRATRANS_SMTP_PORT: "587"
UMBRATRANS_SMTP_USERNAME: "umbra@example.com"
UMBRATRANS_SMTP_SENDER: "umbratrans@umbra.example.com"
UMBRATRANS_SMTP_STARTTLS: "true"
# Sensitive values in separate secret
# UMBRATRANS_DATABASE_DSN: from umbratrans-db-credentials
# UMBRATRANS_SMTP_PASSWORD: from umbratrans-smtp
# UMBRATRANS_TOKEN_PEPPER: from umbratrans-secrets
---
# Deployment for web API + UI
apiVersion: apps/v1
kind: Deployment
metadata:
name: umbratrans-web
namespace: umbratrans
labels:
app: umbratrans-web
component: web
spec:
replicas: 2
selector:
matchLabels:
app: umbratrans-web
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: umbratrans-web
component: web
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8442"
prometheus.io/path: "/api/metrics"
spec:
serviceAccountName: umbratrans-web
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
initContainers:
- name: migrate
image: umbratrans-web:latest
imagePullPolicy: IfNotPresent
command: ["python", "-m", "umbratrans_web.cli", "migrate"]
envFrom:
- secretRef:
name: umbratrans-web-config
- secretRef:
name: umbratrans-db-credentials
- secretRef:
name: umbratrans-smtp
- secretRef:
name: umbratrans-secrets
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "500m"
containers:
- name: web
image: umbratrans-web:latest
imagePullPolicy: IfNotPresent
command: ["python", "-m", "umbratrans_web", "serve"]
args:
- "--host"
- "0.0.0.0"
- "--port"
- "8442"
- "--repository"
- "postgres"
- "--allow-production"
ports:
- name: http
containerPort: 8442
protocol: TCP
envFrom:
- secretRef:
name: umbratrans-web-config
- secretRef:
name: umbratrans-db-credentials
- secretRef:
name: umbratrans-smtp
- secretRef:
name: umbratrans-secrets
volumeMounts:
- name: productions
mountPath: /mnt/productions
readOnly: true
- name: artifacts
mountPath: /mnt/artifacts
readOnly: false
- name: tmp
mountPath: /tmp
livenessProbe:
httpGet:
path: /api/health
port: 8442
initialDelaySeconds: 15
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /api/health
port: 8442
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
startupProbe:
httpGet:
path: /api/health
port: 8442
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 30
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "1000m"
volumes:
- name: productions
persistentVolumeClaim:
claimName: umbratrans-productions
- name: artifacts
persistentVolumeClaim:
claimName: umbratrans-artifacts
- name: tmp
emptyDir:
sizeLimit: 1Gi
---
# Service for web
apiVersion: v1
kind: Service
metadata:
name: umbratrans-web
namespace: umbratrans
labels:
app: umbratrans-web
spec:
selector:
app: umbratrans-web
ports:
- name: http
port: 80
targetPort: 8442
protocol: TCP
type: ClusterIP
---
# HorizontalPodAutoscaler for web
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: umbratrans-web
namespace: umbratrans
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: umbratrans-web
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 10
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Percent
value: 50
periodSeconds: 60
- type: Pods
value: 2
periodSeconds: 60
selectPolicy: Max

View File

@@ -0,0 +1,338 @@
# Copyright 2026 Umbra Tools (umbratools.com)
# SPDX-License-Identifier: Apache-2.0
---
# ServiceAccount for workers
apiVersion: v1
kind: ServiceAccount
metadata:
name: umbratrans-worker
namespace: umbratrans
labels:
app: umbratrans-worker
---
# Role for workers
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: umbratrans-worker
namespace: umbratrans
rules:
- apiGroups: [""]
resources: ["pods", "services", "configmaps", "secrets"]
verbs: ["get", "list", "watch"]
- apiGroups: ["batch"]
resources: ["jobs", "cronjobs"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: ["postgresql.cnpg.io"]
resources: ["clusters", "backups"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: umbratrans-worker
namespace: umbratrans
subjects:
- kind: ServiceAccount
name: umbratrans-worker
namespace: umbratrans
roleRef:
kind: Role
name: umbratrans-worker
apiGroup: rbac.authorization.k8s.io
---
# Secret for worker configuration
apiVersion: v1
kind: Secret
metadata:
name: umbratrans-worker-config
namespace: umbratrans
type: Opaque
stringData:
UMBRA_ENV: "production"
UMBRATRANS_REPOSITORY: "postgres"
UMBRATRANS_PRODUCTIONS_ROOT: "/mnt/productions"
UMBRATRANS_ALLOWED_HOSTS: "umbratrans.bdmurphy.me"
OLLAMA_BASE_URL: "http://ollama:11434/v1"
# UMBRATRANS_DATABASE_DSN: from umbratrans-db-credentials
---
# Deployment for Translation Workers
apiVersion: apps/v1
kind: Deployment
metadata:
name: umbratrans-worker-translation
namespace: umbratrans
labels:
app: umbratrans-worker-translation
component: worker
workload: translation
spec:
replicas: 3
selector:
matchLabels:
app: umbratrans-worker-translation
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
template:
metadata:
labels:
app: umbratrans-worker-translation
component: worker
workload: translation
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9090"
prometheus.io/path: "/metrics"
spec:
serviceAccountName: umbratrans-worker
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
# Node affinity for GPU nodes
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: nvidia.com/gpu.present
operator: In
values: ["true"]
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: workload
operator: In
values: ["translation"]
tolerations:
- key: "nvidia.com/gpu"
operator: "Exists"
effect: "NoSchedule"
containers:
- name: worker
image: umbratrans-worker:latest
imagePullPolicy: IfNotPresent
command: ["python", "-m", "umbratrans_web.cli", "worker", "translation"]
envFrom:
- secretRef:
name: umbratrans-worker-config
- secretRef:
name: umbratrans-db-credentials
- secretRef:
name: umbratrans-secrets
volumeMounts:
- name: productions
mountPath: /mnt/productions
- name: artifacts
mountPath: /mnt/artifacts
- name: tmp
mountPath: /tmp
resources:
requests:
memory: "2Gi"
cpu: "2000m"
nvidia.com/gpu: "1"
limits:
memory: "4Gi"
cpu: "4000m"
nvidia.com/gpu: "1"
livenessProbe:
exec:
command: ["python", "-c", "import psycopg; psycopg.connect('$UMBRATRANS_DATABASE_DSN', connect_timeout=3).close()"]
initialDelaySeconds: 30
periodSeconds: 60
timeoutSeconds: 10
failureThreshold: 3
readinessProbe:
exec:
command: ["python", "-c", "import psycopg; psycopg.connect('$UMBRATRANS_DATABASE_DSN', connect_timeout=3).close()"]
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
volumes:
- name: productions
persistentVolumeClaim:
claimName: umbratrans-productions
- name: artifacts
persistentVolumeClaim:
claimName: umbratrans-artifacts
- name: tmp
emptyDir:
sizeLimit: 2Gi
---
# HorizontalPodAutoscaler for translation workers
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: umbratrans-worker-translation
namespace: umbratrans
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: umbratrans-worker-translation
minReplicas: 1
maxReplicas: 20
metrics:
- type: External
external:
metric:
name: umbratrans_translation_queue_depth
target:
type: AverageValue
averageValue: "10"
behavior:
scaleDown:
stabilizationWindowSeconds: 600
scaleUp:
stabilizationWindowSeconds: 60
---
# Deployment for Audio Workers
apiVersion: apps/v1
kind: Deployment
metadata:
name: umbratrans-worker-audio
namespace: umbratrans
labels:
app: umbratrans-worker-audio
component: worker
workload: audio
spec:
replicas: 2
selector:
matchLabels:
app: umbratrans-worker-audio
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app: umbratrans-worker-audio
component: worker
workload: audio
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "9090"
prometheus.io/path: "/metrics"
spec:
serviceAccountName: umbratrans-worker
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: nvidia.com/gpu.present
operator: In
values: ["true"]
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
preference:
matchExpressions:
- key: workload
operator: In
values: ["audio"]
tolerations:
- key: "nvidia.com/gpu"
operator: "Exists"
effect: "NoSchedule"
containers:
- name: worker
image: umbratrans-worker:latest
imagePullPolicy: IfNotPresent
command: ["python", "-m", "umbratrans_web.cli", "worker", "audio"]
envFrom:
- secretRef:
name: umbratrans-worker-config
- secretRef:
name: umbratrans-db-credentials
- secretRef:
name: umbratrans-secrets
volumeMounts:
- name: productions
mountPath: /mnt/productions
- name: artifacts
mountPath: /mnt/artifacts
- name: tmp
mountPath: /tmp
resources:
requests:
memory: "4Gi"
cpu: "2000m"
nvidia.com/gpu: "1"
limits:
memory: "8Gi"
cpu: "4000m"
nvidia.com/gpu: "1"
livenessProbe:
exec:
command: ["python", "-c", "import psycopg; psycopg.connect('$UMBRATRANS_DATABASE_DSN', connect_timeout=3).close()"]
initialDelaySeconds: 30
periodSeconds: 60
timeoutSeconds: 10
failureThreshold: 3
readinessProbe:
exec:
command: ["python", "-c", "import psycopg; psycopg.connect('$UMBRATRANS_DATABASE_DSN', connect_timeout=3).close()"]
initialDelaySeconds: 10
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
volumes:
- name: productions
persistentVolumeClaim:
claimName: umbratrans-productions
- name: artifacts
persistentVolumeClaim:
claimName: umbratrans-artifacts
- name: tmp
emptyDir:
sizeLimit: 4Gi
---
# HorizontalPodAutoscaler for audio workers
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: umbratrans-worker-audio
namespace: umbratrans
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: umbratrans-worker-audio
minReplicas: 1
maxReplicas: 10
metrics:
- type: External
external:
metric:
name: umbratrans_audio_queue_depth
target:
type: AverageValue
averageValue: "5"
behavior:
scaleDown:
stabilizationWindowSeconds: 900
scaleUp:
stabilizationWindowSeconds: 120

View File

@@ -0,0 +1,169 @@
# Copyright 2026 Umbra Tools (umbratools.com)
# SPDX-License-Identifier: Apache-2.0
---
# Ingress for web UI and API
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: umbratrans-web
namespace: umbratrans
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
nginx.ingress.kubernetes.io/client-body-timeout: "300"
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/force-ssl-redirect: "true"
nginx.ingress.kubernetes.io/hsts: "true"
nginx.ingress.kubernetes.io/hsts-max-age: "31536000"
nginx.ingress.kubernetes.io/hsts-include-subdomains: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- umbratrans.bdmurphy.me
secretName: umbratrans-tls
rules:
- host: umbratrans.bdmurphy.me
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: umbratrans-web
port:
number: 80
---
# NetworkPolicy for web - allow ingress from nginx, egress to DB
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: umbratrans-web
namespace: umbratrans
spec:
podSelector:
matchLabels:
app: umbratrans-web
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
ports:
- protocol: TCP
port: 8442
- from:
- namespaceSelector:
matchLabels:
name: monitoring
ports:
- protocol: TCP
port: 8442
egress:
- to:
- podSelector:
matchLabels:
app: umbratrans-db
ports:
- protocol: TCP
port: 5432
- protocol: TCP
port: 6432 # PgBouncer
- to:
- namespaceSelector:
matchLabels:
name: kube-system
ports:
- protocol: TCP
port: 53
- protocol: UDP
port: 53
- to: []
ports:
- protocol: TCP
port: 443 # External APIs (Ollama, SMTP, etc.)
---
# NetworkPolicy for workers - egress to DB and NFS
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: umbratrans-worker
namespace: umbratrans
spec:
podSelector:
matchLabels:
component: worker
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: umbratrans-db
ports:
- protocol: TCP
port: 5432
- protocol: TCP
port: 6432
- to:
- namespaceSelector:
matchLabels:
name: kube-system
ports:
- protocol: TCP
port: 53
- protocol: UDP
port: 53
- to: []
ports:
- protocol: TCP
port: 443
- protocol: TCP
port: 11434 # Ollama
- protocol: TCP
port: 2049 # NFS
---
# NetworkPolicy for database - allow from web and workers only
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: umbratrans-db
namespace: umbratrans
spec:
podSelector:
matchLabels:
app: umbratrans-db
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: umbratrans-web
- podSelector:
matchLabels:
component: worker
- namespaceSelector:
matchLabels:
name: monitoring
ports:
- protocol: TCP
port: 5432
- protocol: TCP
port: 6432
- from:
- podSelector:
matchLabels:
app: umbratrans-db
ports:
- protocol: TCP
port: 5432

148
k8s/base/06-import-job.yaml Normal file
View File

@@ -0,0 +1,148 @@
# Copyright 2026 Umbra Tools (umbratools.com)
# SPDX-License-Identifier: Apache-2.0
---
# Job to import existing productions data into PostgreSQL
# Run once after initial deployment: kubectl apply -f 06-import-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: umbratrans-import-productions
namespace: umbratrans
labels:
app: umbratrans-import
component: import
annotations:
# Allow job to run for a long time
batch.kubernetes.io/job-tracking: "true"
spec:
ttlSecondsAfterFinished: 86400 # Keep job for 24h after completion
backoffLimit: 3
activeDeadlineSeconds: 7200 # 2 hours max
template:
metadata:
labels:
app: umbratrans-import
component: import
annotations:
prometheus.io/scrape: "false"
spec:
serviceAccountName: umbratrans-worker
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
initContainers:
- name: wait-for-db
image: postgres:16-alpine
command: ["sh", "-c", "until pg_isready -h umbratrans-db-rw -U umbratrans -d umbratrans; do sleep 5; done"]
env:
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: umbratrans-db-credentials
key: password
containers:
- name: import
image: umbratrans-worker:latest
imagePullPolicy: IfNotPresent
command: ["python", "-m", "umbratrans_web.cli", "import-productions"]
args:
- "--organization"
- "umbra" # Organization slug
- "--confirm"
envFrom:
- secretRef:
name: umbratrans-worker-config
- secretRef:
name: umbratrans-db-credentials
- secretRef:
name: umbratrans-secrets
volumeMounts:
- name: productions
mountPath: /mnt/productions
readOnly: true
- name: artifacts
mountPath: /mnt/artifacts
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2000m"
volumes:
- name: productions
persistentVolumeClaim:
claimName: umbratrans-productions
- name: artifacts
persistentVolumeClaim:
claimName: umbratrans-artifacts
---
# CronJob for periodic re-import (optional - for catching new productions)
apiVersion: batch/v1
kind: CronJob
metadata:
name: umbratrans-import-productions-schedule
namespace: umbratrans
labels:
app: umbratrans-import
component: import
spec:
schedule: "0 3 * * *" # Daily at 3 AM UTC
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
ttlSecondsAfterFinished: 86400
backoffLimit: 1
activeDeadlineSeconds: 7200
template:
spec:
serviceAccountName: umbratrans-worker
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
containers:
- name: import
image: umbratrans-worker:latest
imagePullPolicy: IfNotPresent
command: ["python", "-m", "umbratrans_web.cli", "import-productions"]
args:
- "--organization"
- "umbra"
- "--incremental" # Only import new/changed
envFrom:
- secretRef:
name: umbratrans-worker-config
- secretRef:
name: umbratrans-db-credentials
- secretRef:
name: umbratrans-secrets
volumeMounts:
- name: productions
mountPath: /mnt/productions
readOnly: true
- name: artifacts
mountPath: /mnt/artifacts
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "2Gi"
cpu: "2000m"
volumes:
- name: productions
persistentVolumeClaim:
claimName: umbratrans-productions
- name: artifacts
persistentVolumeClaim:
claimName: umbratrans-artifacts

View File

@@ -0,0 +1,286 @@
# Copyright 2026 Umbra Tools (umbratools.com)
# SPDX-License-Identifier: Apache-2.0
---
# Secret for database credentials (created by CNPG, but we reference it)
# This is a template - CNPG creates the actual secret
apiVersion: v1
kind: Secret
metadata:
name: umbratrans-db-credentials
namespace: umbratrans
type: kubernetes.io/basic-auth
stringData:
username: "umbratrans"
password: "CHANGE_ME_GENERATED_BY_CNPG"
# DSN format: postgresql://user:pass@host:port/db?sslmode=require
dsn: "postgresql://umbratrans:CHANGE_ME@umbratrans-db-rw:5432/umbratrans?sslmode=require"
---
# Secret for SMTP credentials
apiVersion: v1
kind: Secret
metadata:
name: umbratrans-smtp
namespace: umbratrans
type: Opaque
stringData:
host: "smtp.example.com"
port: "587"
username: "umbra@example.com"
password: "CHANGE_ME_SMTP_PASSWORD"
sender: "umbratrans@umbra.example.com"
---
# Secret for application secrets (token pepper, etc.)
apiVersion: v1
kind: Secret
metadata:
name: umbratrans-secrets
namespace: umbratrans
type: Opaque
stringData:
token_pepper: "CHANGE_ME_32_BYTES_MINIMUM_BASE64_ENCODED"
---
# Secret for PgBouncer admin (generated by CNPG)
apiVersion: v1
kind: Secret
metadata:
name: umbratrans-pgbouncer-admin
namespace: umbratrans
type: Opaque
stringData:
username: "pgbouncer_admin"
password: "CHANGE_ME_GENERATED_BY_CNPG"
---
# Secret for backup credentials (S3)
apiVersion: v1
kind: Secret
metadata:
name: umbratrans-backup-credentials
namespace: umbratrans
type: Opaque
stringData:
ACCESS_KEY_ID: "CHANGE_ME"
SECRET_ACCESS_KEY: "CHANGE_ME"
---
# TLS certificate for ingress (managed by cert-manager)
apiVersion: v1
kind: Secret
metadata:
name: umbratrans-tls
namespace: umbratrans
type: kubernetes.io/tls
data:
# cert-manager will populate this
tls.crt: ""
tls.key: ""
---
# ServiceMonitor for Prometheus (requires Prometheus Operator)
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: umbratrans-web
namespace: umbratrans
labels:
release: prometheus
spec:
selector:
matchLabels:
app: umbratrans-web
endpoints:
- port: http
path: /api/metrics
interval: 30s
namespaceSelector:
matchNames:
- umbratrans
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: umbratrans-workers
namespace: umbratrans
labels:
release: prometheus
spec:
selector:
matchLabels:
component: worker
endpoints:
- port: metrics
path: /metrics
interval: 30s
namespaceSelector:
matchNames:
- umbratrans
---
# PodMonitor for PostgreSQL (CNPG)
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
name: umbratrans-db
namespace: umbratrans
labels:
release: prometheus
spec:
selector:
matchLabels:
app: umbratrans-db
podMetricsEndpoints:
- port: metrics
path: /metrics
interval: 30s
---
# ConfigMap for custom PostgreSQL queries
apiVersion: v1
kind: ConfigMap
metadata:
name: umbratrans-db-queries
namespace: umbratrans
data:
umbratrans_queries.yaml: |
- name: umbratrans_projects_total
query: |
SELECT count(*) as value FROM projects;
metrics:
- value
- name: umbratrans_active_jobs
query: |
SELECT count(*) as value FROM jobs WHERE status IN ('queued', 'running', 'blocked');
metrics:
- value
- name: umbratrans_translation_queue_depth
query: |
SELECT count(*) as value FROM jobs WHERE kind = 'translate' AND status = 'queued';
metrics:
- value
- name: umbratrans_audio_queue_depth
query: |
SELECT count(*) as value FROM jobs WHERE kind LIKE 'audio%' AND status = 'queued';
metrics:
- value
- name: umbratrans_artifacts_total
query: |
SELECT count(*) as value FROM artifacts;
metrics:
- value
- name: umbratrans_artifacts_size_bytes
query: |
SELECT sum(byte_size) as value FROM artifacts;
metrics:
- value
- name: umbratrans_import_runs_total
query: |
SELECT count(*) as value FROM translation_imports;
metrics:
- value
---
# PrometheusRule for alerts
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: umbratrans-alerts
namespace: umbratrans
labels:
release: prometheus
spec:
groups:
- name: umbratrans.rules
rules:
- alert: UmbraTransWebDown
expr: up{job="umbratrans-web"} == 0
for: 2m
labels:
severity: critical
annotations:
summary: "UmbraTrans web is down"
description: "UmbraTrans web has been down for more than 2 minutes."
- alert: UmbraTransHighErrorRate
expr: |
sum(rate(http_requests_total{job="umbratrans-web",code=~"5.."}[5m]))
/
sum(rate(http_requests_total{job="umbratrans-web"}[5m])) > 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "High error rate on UmbraTrans web"
description: "Error rate is above 5% for the last 5 minutes."
- alert: UmbraTransDBConnectionsHigh
expr: |
pg_stat_database_numbackends / pg_settings_max_connections > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "PostgreSQL connections near limit"
description: "Database connections are above 80% of max_connections."
- alert: UmbraTransTranslationQueueBacklog
expr: umbratrans_translation_queue_depth > 100
for: 10m
labels:
severity: warning
annotations:
summary: "Translation queue backlog"
description: "Translation queue has more than 100 pending jobs for 10 minutes."
- alert: UmbraTransAudioQueueBacklog
expr: umbratrans_audio_queue_depth > 50
for: 10m
labels:
severity: warning
annotations:
summary: "Audio queue backlog"
description: "Audio queue has more than 50 pending jobs for 10 minutes."
- alert: UmbraTransDiskSpaceLow
expr: |
(kubelet_volume_stats_available_bytes / kubelet_volume_stats_capacity_bytes) < 0.1
for: 5m
labels:
severity: critical
annotations:
summary: "Disk space low on UmbraTrans volumes"
description: "Less than 10% disk space remaining on persistent volumes."
- alert: UmbraTransWorkerTranslationDown
expr: |
kube_deployment_status_replicas_available{deployment="umbratrans-worker-translation"} < 1
for: 5m
labels:
severity: critical
annotations:
summary: "No translation workers available"
description: "All translation worker pods are down."
- alert: UmbraTransWorkerAudioDown
expr: |
kube_deployment_status_replicas_available{deployment="umbratrans-worker-audio"} < 1
for: 5m
labels:
severity: critical
annotations:
summary: "No audio workers available"
description: "All audio worker pods are down."
- alert: UmbraTransImportJobFailed
expr: |
kube_job_status_failed{job=~"umbratrans-import.*"} > 0
for: 1m
labels:
severity: warning
annotations:
summary: "UmbraTrans import job failed"
description: "Production import job has failed."

View File

@@ -0,0 +1,61 @@
# Copyright 2026 Umbra Tools (umbratools.com)
# SPDX-License-Identifier: Apache-2.0
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: umbratrans
resources:
- 00-namespace.yaml
- 01-postgresql.yaml
- 02-nfs-storage.yaml
- 03-web-deployment.yaml
- 04-worker-deployments.yaml
- 05-ingress-network.yaml
- 06-import-job.yaml
- 07-monitoring-secrets.yaml
commonLabels:
app.kubernetes.io/name: umbratrans
app.kubernetes.io/part-of: umbratrans
app.kubernetes.io/managed-by: kustomize
images:
- name: umbratrans-web
newTag: latest
- name: umbratrans-worker
newTag: latest
# Patches for different environments
patches:
- patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: umbratrans-web
spec:
replicas: 1
target:
kind: Deployment
name: umbratrans-web
- patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: umbratrans-worker-translation
spec:
replicas: 1
target:
kind: Deployment
name: umbratrans-worker-translation
- patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: umbratrans-worker-audio
spec:
replicas: 0
target:
kind: Deployment
name: umbratrans-worker-audio