refactor(k8s): minimale, flache Manifeste zum Kopieren

Kustomize-Struktur (base/overlays) entfernt. Stattdessen nummerierte
Einzeldateien, ausrollbar mit "kubectl apply -f k8s/":
00-namespace, 01-config, 02-storage (SMB-PVs/PVCs Postgres + Medien),
03-postgres (StatefulSet + Service), 04-app (Deployment mit
Migrations-initContainer + Service), 05-ingress, 06-worker,
07-networkpolicy. Secrets als secrets.yaml.example (echte secrets.yaml
ist gitignored). Images mit Tag latest aus der Gitea-Pipeline.
Kurzanleitung in k8s/README.md, Betriebsdoku angepasst.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-09-28 01:12:26 +02:00
1 parent c84eb1e740
commit 91791b791f
20 files changed
+332 -494

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: artista
labels:
pod-security.kubernetes.io/enforce: restricted
+15
View File
@@ -0,0 +1,15 @@
# Nicht geheime Einstellungen. ANPASSEN: APP_URL, MAIL_FROM, ADMIN_NOTIFY_EMAIL.
apiVersion: v1
kind: ConfigMap
metadata:
name: artista-modelcamp-config
namespace: artista
data:
APP_ENV: "production"
APP_URL: "https://modelcamp.example.org"
MAIL_TRANSPORT: "smtp"
MAIL_FROM: "ARTISTA MODELCAMP <[email protected]>"
ADMIN_NOTIFY_EMAIL: "[email protected]"
SMTP_PORT: "587"
SMTP_SECURE: "false"
MEDIA_DIR: "/app/media"
+74
View File
@@ -0,0 +1,74 @@
# Speicher auf der SMB-Freigabe (benoetigt csi-driver-smb, siehe README).
# ANPASSEN: "source" (//<nas>/<freigabe>/<ordner>). Ordner vorher anlegen.
#
# PostgreSQL auf SMB ist eine bewusste Entscheidung (Risiko siehe
# docs/BETRIEB.md). NAS-Backups sind fuer die laufende Datenbank nur mit
# atomaren Snapshots (Btrfs/ZFS) konsistent.
# --- PostgreSQL-Daten ---
apiVersion: v1
kind: PersistentVolume
metadata:
name: artista-modelcamp-postgres
spec:
capacity:
storage: 10Gi
accessModes: ["ReadWriteOnce"]
persistentVolumeReclaimPolicy: Retain
storageClassName: ""
mountOptions: [vers=3.1.1, seal, hard, cache=strict, uid=70, gid=70, dir_mode=0700, file_mode=0600, mfsymlinks, nobrl]
csi:
driver: smb.csi.k8s.io
volumeHandle: artista-modelcamp-postgres
volumeAttributes:
source: //nas.example.lan/artista/modelcamp-postgres
nodeStageSecretRef:
name: artista-modelcamp-smb
namespace: artista
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: artista-modelcamp-postgres
namespace: artista
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: ""
volumeName: artista-modelcamp-postgres
resources:
requests:
storage: 10Gi
---
# --- Medien (Bilder/Videos; App liest nur, Dateien direkt aufs NAS legen) ---
apiVersion: v1
kind: PersistentVolume
metadata:
name: artista-modelcamp-media
spec:
capacity:
storage: 5Gi
accessModes: ["ReadWriteMany"]
persistentVolumeReclaimPolicy: Retain
storageClassName: ""
mountOptions: [vers=3.1.1, seal, uid=1001, gid=1001, dir_mode=0550, file_mode=0440, mfsymlinks, nobrl]
csi:
driver: smb.csi.k8s.io
volumeHandle: artista-modelcamp-media
volumeAttributes:
source: //nas.example.lan/artista/modelcamp-media
nodeStageSecretRef:
name: artista-modelcamp-smb
namespace: artista
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: artista-modelcamp-media
namespace: artista
spec:
accessModes: ["ReadWriteMany"]
storageClassName: ""
volumeName: artista-modelcamp-media
resources:
requests:
storage: 5Gi
@@ -1,30 +1,20 @@
# PostgreSQL fuer Buchungen, Zahlungen, Gutscheine und Protokoll.
#
# Datenverzeichnis: PVC "artista-modelcamp-postgres-data" (storage.yaml), im
# Produktions-Overlay auf SMB. Das ist eine BEWUSSTE, vom Betreiber
# akzeptierte Ausnahme von der Empfehlung "Blockspeicher": SMB garantiert
# Sperren und fsync nicht in jedem Fall wie ein lokales Dateisystem. Die
# Mount-Optionen in smb-volumes.yaml sind darauf ausgelegt; Risiken und
# Hinweise zur Sicherung stehen in docs/BETRIEB.md.
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: artista-modelcamp-postgres
namespace: artista
spec:
serviceName: artista-modelcamp-postgres
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: artista-modelcamp-postgres
app: artista-modelcamp-postgres
template:
metadata:
labels:
app.kubernetes.io/name: artista-modelcamp-postgres
app: artista-modelcamp-postgres
spec:
automountServiceAccountToken: false
# Beim Beenden sauber herunterfahren (Checkpoint), bevor das Volume
# ausgehaengt wird — auf Netzwerkspeicher besonders wichtig.
terminationGracePeriodSeconds: 120
terminationGracePeriodSeconds: 120 # sauber herunterfahren vor dem Aushaengen
securityContext:
runAsNonRoot: true
runAsUser: 70
@@ -44,17 +34,11 @@ spec:
valueFrom:
secretKeyRef: { name: artista-modelcamp-secrets, key: DB_PASSWORD }
volumeMounts:
# Ab PostgreSQL 18 liegt PGDATA unter /var/lib/postgresql/18/docker.
- name: data
mountPath: /var/lib/postgresql
readinessProbe:
exec: { command: ["pg_isready", "-U", "artista"] }
periodSeconds: 5
livenessProbe:
exec: { command: ["pg_isready", "-U", "artista"] }
initialDelaySeconds: 60
periodSeconds: 20
failureThreshold: 6
resources:
requests: { cpu: 100m, memory: 256Mi }
limits: { memory: 1Gi }
@@ -63,15 +47,16 @@ spec:
capabilities: { drop: ["ALL"] }
volumes:
- name: data
persistentVolumeClaim: { claimName: artista-modelcamp-postgres-data }
persistentVolumeClaim: { claimName: artista-modelcamp-postgres }
---
apiVersion: v1
kind: Service
metadata:
name: artista-modelcamp-postgres
namespace: artista
spec:
clusterIP: None
selector:
app.kubernetes.io/name: artista-modelcamp-postgres
app: artista-modelcamp-postgres
ports:
- port: 5432
+85
View File
@@ -0,0 +1,85 @@
# Website. Der initContainer migriert vor jedem Start die Datenbank.
# Update nach neuem Image: kubectl -n artista rollout restart deploy/artista-modelcamp-app
apiVersion: apps/v1
kind: Deployment
metadata:
name: artista-modelcamp-app
namespace: artista
spec:
replicas: 2
selector:
matchLabels:
app: artista-modelcamp-app
template:
metadata:
labels:
app: artista-modelcamp-app
db-client: "true"
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 1001
seccompProfile: { type: RuntimeDefault }
initContainers:
- name: migrate
image: git.encry.de/nick.rosenloecher/artista-modelcamp-tools:latest
imagePullPolicy: Always
command: ["prisma", "migrate", "deploy"]
envFrom:
- secretRef: { name: artista-modelcamp-secrets }
securityContext:
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
containers:
- name: app
image: git.encry.de/nick.rosenloecher/artista-modelcamp:latest
imagePullPolicy: Always
ports:
- name: http
containerPort: 3000
envFrom:
- configMapRef: { name: artista-modelcamp-config }
- secretRef: { name: artista-modelcamp-secrets }
readinessProbe:
httpGet: { path: /api/health/ready, port: http }
periodSeconds: 10
livenessProbe:
httpGet: { path: /api/health, port: http }
initialDelaySeconds: 20
periodSeconds: 20
resources:
requests: { cpu: 200m, memory: 256Mi }
limits: { memory: 768Mi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
volumeMounts:
- name: media
mountPath: /app/media
readOnly: true
- name: cache
mountPath: /app/.next/cache
- name: tmp
mountPath: /tmp
volumes:
- name: media
persistentVolumeClaim: { claimName: artista-modelcamp-media }
- name: cache
emptyDir: {}
- name: tmp
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: artista-modelcamp-app
namespace: artista
spec:
selector:
app: artista-modelcamp-app
ports:
- name: http
port: 80
targetPort: http
+24
View File
@@ -0,0 +1,24 @@
# ANPASSEN: Domain (2x, auch APP_URL in 01-config.yaml), ClusterIssuer.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: artista-modelcamp
namespace: artista
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: traefik
tls:
- hosts: [modelcamp.example.org]
secretName: artista-modelcamp-tls
rules:
- host: modelcamp.example.org
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: artista-modelcamp-app
port:
name: http
+6 -15
View File
@@ -1,43 +1,34 @@
# Worker: gibt abgelaufene Reservierungen frei (nach Rueckfrage bei Stripe),
# zieht faellige Raten ein, trennt Zahlungsmethoden nach der letzten Rate und
# versendet den E-Mail-Ausgang. Ohne ihn laufen Ratenzahlung und
# Reservierungsfreigabe nicht. Idempotent; Forbid verhindert Ueberlappung.
# Alle 10 Minuten: faellige Raten einziehen, abgelaufene Reservierungen
# freigeben, E-Mails nachsenden. Ohne ihn funktioniert Ratenzahlung nicht.
apiVersion: batch/v1
kind: CronJob
metadata:
name: artista-modelcamp-worker
namespace: artista
spec:
schedule: "*/10 * * * *"
timeZone: "Europe/Berlin"
concurrencyPolicy: Forbid
startingDeadlineSeconds: 300
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
backoffLimit: 1
activeDeadlineSeconds: 900
template:
metadata:
labels:
artista/db-client: "true"
db-client: "true"
spec:
restartPolicy: Never
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1001
seccompProfile: { type: RuntimeDefault }
containers:
- name: worker
image: artista-modelcamp-tools
image: git.encry.de/nick.rosenloecher/artista-modelcamp-tools:latest
imagePullPolicy: Always
command: ["tsx", "scripts/worker.ts"]
envFrom:
- configMapRef: { name: artista-modelcamp-config }
- secretRef: { name: artista-modelcamp-secrets }
resources:
requests: { cpu: 50m, memory: 192Mi }
limits: { memory: 512Mi }
securityContext:
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
+35
View File
@@ -0,0 +1,35 @@
# Datenbank nur fuer App und Worker erreichbar; App nur ueber Traefik.
# ANPASSEN: Namespace deines Traefik ("kubectl get pods -A | grep traefik").
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: artista-modelcamp-postgres
namespace: artista
spec:
podSelector:
matchLabels:
app: artista-modelcamp-postgres
ingress:
- from:
- podSelector:
matchLabels:
db-client: "true"
ports:
- port: 5432
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: artista-modelcamp-app
namespace: artista
spec:
podSelector:
matchLabels:
app: artista-modelcamp-app
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: traefik
ports:
- port: 3000
+37
View File
@@ -0,0 +1,37 @@
# Kubernetes (RKE2, Traefik, cert-manager)
## Einmalig
1. **csi-driver-smb installieren:**
```bash
helm repo add csi-driver-smb https://raw.githubusercontent.com/kubernetes-csi/csi-driver-smb/master/charts
helm install csi-driver-smb csi-driver-smb/csi-driver-smb -n kube-system
```
2. **Auf dem NAS** die Ordner `artista/modelcamp-postgres` (leer) und `artista/modelcamp-media` anlegen und SMB-Verschlüsselung erlauben.
3. **Anpassen:**
- `nas.example.lan` in `02-storage.yaml`
- `modelcamp.example.org` in `01-config.yaml` und `05-ingress.yaml`
- den Traefik-Namespace in `07-networkpolicy.yaml`
4. **Secrets:** `cp k8s/secrets.yaml.example k8s/secrets.yaml`, die Werte eintragen.
5. **Ausrollen:**
```bash
kubectl apply -f k8s/00-namespace.yaml
kubectl apply -f k8s/secrets.yaml
kubectl apply -f k8s/
```
6. **Admin anlegen** (E-Mail und Name ersetzen; das einmalige Passwort wird ausgegeben, die 2FA richtest du bei der ersten Anmeldung unter `/admin` ein):
```bash
kubectl -n artista run admin-create --rm -i --restart=Never \
--image=git.encry.de/nick.rosenloecher/artista-modelcamp-tools:latest --labels=db-client=true \
--overrides='{"spec":{"securityContext":{"runAsNonRoot":true,"runAsUser":1001,"seccompProfile":{"type":"RuntimeDefault"}},"containers":[{"name":"admin-create","image":"git.encry.de/nick.rosenloecher/artista-modelcamp-tools:latest","command":["tsx","scripts/create-admin.ts","--email","[email protected]","--name","Dein Name"],"envFrom":[{"configMapRef":{"name":"artista-modelcamp-config"}},{"secretRef":{"name":"artista-modelcamp-secrets"}}],"securityContext":{"allowPrivilegeEscalation":false,"capabilities":{"drop":["ALL"]}}}]}}'
```
## Update
Bei jedem Push auf `main` baut die Gitea-Pipeline die Images mit dem Tag `latest`. Danach:
```bash
kubectl -n artista rollout restart deploy/artista-modelcamp-app
```
Der initContainer migriert die Datenbank automatisch.
-133
View File
@@ -1,133 +0,0 @@
# Next.js-Anwendung. Zustandslos: Buchungen, Kapazitaet, Gutscheine,
# Reservierungen, Rate Limits, E-Mail-Ausgang und Zahlungsjobs liegen in
# PostgreSQL — beliebig viele Replikate moeglich.
#
# Migrationen laufen als eigener Schritt VOR dem App-Container: initContainer
# "migrate" (Werkzeug-Image, `prisma migrate deploy`). Scheitert er, startet
# die App nicht. Mehrere Replikate gleichzeitig sind unkritisch — Prisma
# serialisiert Migrationen ueber eine Datenbank-Sperre.
apiVersion: apps/v1
kind: Deployment
metadata:
name: artista-modelcamp-app
spec:
replicas: 2
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate: { maxSurge: 1, maxUnavailable: 0 }
selector:
matchLabels:
app.kubernetes.io/name: artista-modelcamp-app
template:
metadata:
labels:
app.kubernetes.io/name: artista-modelcamp-app
artista/db-client: "true"
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 1001
runAsGroup: 1001
fsGroup: 1001
seccompProfile: { type: RuntimeDefault }
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: artista-modelcamp-app
initContainers:
- name: migrate
image: artista-modelcamp-tools
command: ["prisma", "migrate", "deploy"]
envFrom:
- secretRef: { name: artista-modelcamp-secrets }
resources:
requests: { cpu: 50m, memory: 128Mi }
limits: { memory: 512Mi }
securityContext:
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
containers:
- name: app
image: artista-modelcamp
ports:
- name: http
containerPort: 3000
envFrom:
- configMapRef: { name: artista-modelcamp-config }
- secretRef: { name: artista-modelcamp-secrets }
readinessProbe:
httpGet: { path: /api/health/ready, port: http }
periodSeconds: 10
timeoutSeconds: 5
livenessProbe:
# Bewusst ohne Datenbank: eine DB-Stoerung soll nicht zu
# Neustartschleifen fuehren.
httpGet: { path: /api/health, port: http }
initialDelaySeconds: 20
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 3
resources:
requests: { cpu: 200m, memory: 256Mi }
limits: { memory: 768Mi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
volumeMounts:
- name: media
mountPath: /app/media
readOnly: true
# Reiner Zwischenspeicher fuer optimierte Bilder: fluechtig ok.
- name: next-cache
mountPath: /app/.next/cache
- name: tmp
mountPath: /tmp
volumes:
- name: media
persistentVolumeClaim: { claimName: artista-modelcamp-media }
- name: next-cache
emptyDir: { sizeLimit: 512Mi }
- name: tmp
emptyDir: { sizeLimit: 64Mi }
---
apiVersion: v1
kind: Service
metadata:
name: artista-modelcamp-app
spec:
selector:
app.kubernetes.io/name: artista-modelcamp-app
ports:
- name: http
port: 80
targetPort: http
---
# Die App nimmt Verbindungen nur vom Ingress-Controller an. Ohne diese Regel
# koennte jeder Pod im Cluster den Service direkt ansprechen und per
# gefaelschtem X-Real-Ip-Header die Rate Limits (inkl. Admin-Login/2FA)
# umgehen. Namespace des Traefik-Controllers anpassen
# ("kubectl get pods -A | grep traefik").
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: artista-modelcamp-app-ingress
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: artista-modelcamp-app
policyTypes: ["Ingress"]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: traefik
ports:
- port: 3000
# Kubelet-Probes kommen vom Knoten und sind von NetworkPolicies je nach
# CNI (Canal/Calico in RKE2) ausgenommen.
-39
View File
@@ -1,39 +0,0 @@
# Der Namespace "artista" wird einmalig von Hand angelegt:
# kubectl create namespace artista
# kubectl label namespace artista pod-security.kubernetes.io/enforce=restricted
#
# Nicht geheime Einstellungen. Geheimnisse: k8s/secret.example.yaml.
apiVersion: v1
kind: ConfigMap
metadata:
name: artista-modelcamp-config
data:
APP_ENV: "production"
APP_URL: "https://modelcamp.example.org"
DATABASE_POOL_SIZE: "10"
MAIL_TRANSPORT: "smtp"
MAIL_FROM: "ARTISTA MODELCAMP <[email protected]>"
ADMIN_NOTIFY_EMAIL: ""
SMTP_PORT: "587"
SMTP_SECURE: "false"
MEDIA_DIR: "/app/media"
EXTERNAL_VIDEO_PROVIDER: "none"
---
# Die Datenbank ist nur fuer Pods mit dem Label artista/db-client erreichbar
# (App inkl. Migrations-initContainer, Worker).
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: artista-modelcamp-postgres-access
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: artista-modelcamp-postgres
policyTypes: ["Ingress"]
ingress:
- from:
- podSelector:
matchLabels:
artista/db-client: "true"
ports:
- port: 5432
-15
View File
@@ -1,15 +0,0 @@
# Basis fuer RKE2. Nicht direkt anwenden — ueber ein Overlay
# (k8s/overlays/production), das Domain, Images und TLS setzt.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: artista
resources:
- config.yaml
- storage.yaml
- postgres.yaml
- app.yaml
- worker.yaml
labels:
- pairs:
app.kubernetes.io/part-of: artista-modelcamp
includeSelectors: false
-27
View File
@@ -1,27 +0,0 @@
# Die beiden dauerhaften Speicher. Im Produktions-Overlay werden sie fest an
# die SMB-PersistentVolumes gebunden (k8s/overlays/production/smb-volumes.yaml).
# Eine separate Sicherung gibt es nicht: die SMB-Freigabe wird auf dem NAS
# selbst mehrfach gesichert.
# PostgreSQL-Datenverzeichnis.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: artista-modelcamp-postgres-data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10Gi
---
# Freigegebene Bilder und Videos. ReadWriteMany, weil mehrere App-Replikate
# lesen.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: artista-modelcamp-media
spec:
accessModes: ["ReadWriteMany"]
resources:
requests:
storage: 5Gi
-34
View File
@@ -1,34 +0,0 @@
# Ingress fuer Traefik mit cert-manager.
# Anpassen: Host (auch APP_URL in kustomization.yaml), ingressClassName
# ("kubectl get ingressclass"), ClusterIssuer, TLS-Secret-Name.
# HTTP->HTTPS-Umleitung erwartet dieses Manifest EntryPoint-weit in Traefik
# (Helm: ports.web.redirections.entryPoint.to=websecure).
#
# X-Forwarded-For/X-Real-Ip: Rate Limits verlassen sich darauf, dass Traefik
# vom Client mitgeschickte Werte verwirft. Dafuer im Traefik-EntryPoint
# forwardedHeaders.trustedIPs nur auf echte vorgelagerte Proxys setzen.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: artista-modelcamp
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
spec:
ingressClassName: traefik
tls:
- hosts:
- modelcamp.example.org
secretName: artista-modelcamp-tls
rules:
- host: modelcamp.example.org
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: artista-modelcamp-app
port:
name: http
@@ -1,47 +0,0 @@
# Produktions-Overlay. Anpassen: Images/Tags, Domain, IngressClass,
# ClusterIssuer und TLS-Secret-Name. Anwenden:
# kubectl apply -k k8s/overlays/production
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: artista
resources:
- ../../base
- ingress.yaml
# PostgreSQL-Daten und Medien auf SMB.
# Ohne SMB: diese Zeile und die beiden PVC-Patches unten entfernen.
- smb-volumes.yaml
images:
# Gebaut von .gitea/workflows/docker-build.yml: Git-Tag "v0.1.0" pushen
# ergibt Image-Tag "0.1.0" (Alternativen: "sha-<kurz>", "latest").
# Beide Images immer mit identischem Tag ausrollen.
- name: artista-modelcamp
newName: git.encry.de/nick.rosenloecher/artista-modelcamp
newTag: "0.1.0"
- name: artista-modelcamp-tools
newName: git.encry.de/nick.rosenloecher/artista-modelcamp-tools
newTag: "0.1.0"
patches:
# PVCs fest an die SMB-PersistentVolumes binden (statisch, ohne StorageClass).
- target: { kind: PersistentVolumeClaim, name: artista-modelcamp-media }
patch: |-
- op: add
path: /spec/storageClassName
value: ""
- op: add
path: /spec/volumeName
value: artista-modelcamp-media-smb
- target: { kind: PersistentVolumeClaim, name: artista-modelcamp-postgres-data }
patch: |-
- op: add
path: /spec/storageClassName
value: ""
- op: add
path: /spec/volumeName
value: artista-modelcamp-postgres-smb
- target: { kind: ConfigMap, name: artista-modelcamp-config }
patch: |-
- op: replace
path: /data/APP_URL
value: https://modelcamp.example.org
-89
View File
@@ -1,89 +0,0 @@
# PersistentVolumes auf einer SMB-Freigabe (csi-driver-smb, smb.csi.k8s.io).
#
# VORAUSSETZUNG: csi-driver-smb im Cluster installiert, z. B.:
# helm repo add csi-driver-smb https://raw.githubusercontent.com/kubernetes-csi/csi-driver-smb/master/charts
# helm install csi-driver-smb csi-driver-smb/csi-driver-smb -n kube-system
# und das Zugangs-Secret "artista-modelcamp-smb" (k8s/secret.example.yaml).
#
# ANZUPASSEN: "source" (//<nas>/<freigabe>/<ordner>) je Volume. Die Ordner
# muessen auf der Freigabe bereits existieren; der Postgres-Ordner leer.
#
# Beide Verbindungen verschluesselt (SMB 3.1.1 + seal); das NAS muss
# SMB-Verschluesselung erlauben. fsGroup wirkt auf SMB nicht: Besitz und
# Rechte setzen uid/gid/dir_mode/file_mode. reclaimPolicy Retain: Loeschen
# von PVC/PV laesst die Dateien auf dem NAS.
# Keine eigene Sicherung im Cluster: die Freigabe wird auf dem NAS gesichert.
# --- PostgreSQL-Datenverzeichnis ------------------------------------------------
# BEWUSSTE AUSNAHME (Entscheidung des Betreibers): Datenbank auf SMB.
# Risiko: SMB garantiert Sperren/fsync nicht in jedem Fall wie ein lokales
# Dateisystem; bei Netzwerk- oder NAS-Ausfall waehrend Schreibvorgaengen kann
# die Datenbank beschaedigt werden. Die Optionen unten minimieren das:
# hard E/A wartet bei Netzstoerung statt Fehler an PostgreSQL zu
# melden (kein "soft"-Abbruch mitten im Schreiben)
# cache=strict strikte Cache-Kohaerenz, fsync geht bis zum Server
# nobrl keine Byte-Range-Locks (PostgreSQL braucht sie nicht)
# uid/gid 70, 0700/0600 PostgreSQL verlangt ein privates Datenverzeichnis
# Sicherung: nur NAS-Snapshots, die atomar sind (Btrfs/ZFS-Snapshot der ganzen
# Freigabe), sind fuer ein laufendes Datenverzeichnis konsistent. Einfaches
# Dateikopieren waehrend des Betriebs ergibt KEINE brauchbare Sicherung.
apiVersion: v1
kind: PersistentVolume
metadata:
name: artista-modelcamp-postgres-smb
spec:
capacity:
storage: 10Gi
accessModes: ["ReadWriteOnce"]
persistentVolumeReclaimPolicy: Retain
storageClassName: ""
mountOptions:
- vers=3.1.1
- seal
- hard
- cache=strict
- uid=70
- gid=70
- dir_mode=0700
- file_mode=0600
- mfsymlinks
- nobrl
csi:
driver: smb.csi.k8s.io
volumeHandle: artista-modelcamp-postgres-smb
volumeAttributes:
source: //nas.example.lan/artista/modelcamp-postgres
nodeStageSecretRef:
name: artista-modelcamp-smb
namespace: artista
---
# --- Medien (Bilder/Videos) ------------------------------------------------------
# Die App liest nur. Neue Bilder legst du direkt ueber die SMB-Freigabe ab,
# z. B. vom Windows-Explorer aus.
apiVersion: v1
kind: PersistentVolume
metadata:
name: artista-modelcamp-media-smb
spec:
capacity:
storage: 5Gi
accessModes: ["ReadWriteMany"]
persistentVolumeReclaimPolicy: Retain
storageClassName: ""
mountOptions:
- vers=3.1.1
- seal
- uid=1001 # Containerbenutzer "nextjs" (Dockerfile)
- gid=1001
- dir_mode=0550 # App liest nur; Schreiben ausschliesslich ueber das NAS
- file_mode=0440
- mfsymlinks
- nobrl
csi:
driver: smb.csi.k8s.io
volumeHandle: artista-modelcamp-media-smb
volumeAttributes:
source: //nas.example.lan/artista/modelcamp-media
nodeStageSecretRef:
name: artista-modelcamp-smb
namespace: artista
-45
View File
@@ -1,45 +0,0 @@
# VORLAGE — nicht mit echten Werten ins Repository einchecken.
# Anlegen z. B. per:
# kubectl -n artista create secret generic artista-modelcamp-secrets \
# --from-literal=DB_PASSWORD='…' \
# --from-literal=DATABASE_URL='postgresql://artista:…@artista-modelcamp-postgres:5432/artista' \
# --from-literal=APP_SECRET="$(openssl rand -base64 48)" \
# --from-literal=BETTER_AUTH_SECRET="$(openssl rand -base64 48)" \
# --from-literal=STRIPE_SECRET_KEY='sk_test_…' \
# --from-literal=STRIPE_WEBHOOK_SECRET='whsec_…' \
# --from-literal=SMTP_HOST='…' --from-literal=SMTP_USER='…' --from-literal=SMTP_PASSWORD='…'
# Besser noch: Sealed Secrets oder External Secrets Operator.
apiVersion: v1
kind: Secret
metadata:
name: artista-modelcamp-secrets
namespace: artista
type: Opaque
stringData:
DB_PASSWORD: "<starkes-passwort>"
# Passwort URL-kodiert einsetzen, falls Sonderzeichen enthalten sind.
DATABASE_URL: "postgresql://artista:<starkes-passwort>@artista-modelcamp-postgres:5432/artista"
APP_SECRET: "<mind-32-zeichen-zufall>"
BETTER_AUTH_SECRET: "<anderer-mind-32-zeichen-zufall>"
STRIPE_SECRET_KEY: "<sk_test_… oder sk_live_…>"
STRIPE_WEBHOOK_SECRET: "<whsec_…>"
SMTP_HOST: "<smtp.example.org>"
SMTP_USER: "<benutzer>"
SMTP_PASSWORD: "<passwort>"
---
# Zugang zur SMB-Freigabe fuer die PersistentVolumes
# (k8s/overlays/production/smb-volumes.yaml). Eigener, auf die Artista-
# Ordner beschraenkter NAS-Benutzer empfohlen.
# kubectl -n artista create secret generic artista-modelcamp-smb \
# --from-literal=username='…' --from-literal=password='…'
apiVersion: v1
kind: Secret
metadata:
name: artista-modelcamp-smb
namespace: artista
type: Opaque
stringData:
username: "<nas-benutzer>"
password: "<nas-passwort>"
# Optional bei Domaenenkonten:
# domain: "<domaene>"
+28
View File
@@ -0,0 +1,28 @@
# Kopieren nach k8s/secrets.yaml (ist in .gitignore), Werte eintragen,
# dann: kubectl apply -f k8s/secrets.yaml
# Zufallswerte: openssl rand -base64 48
apiVersion: v1
kind: Secret
metadata:
name: artista-modelcamp-secrets
namespace: artista
stringData:
DB_PASSWORD: "PASSWORT"
DATABASE_URL: "postgresql://artista:PASSWORT@artista-modelcamp-postgres:5432/artista"
APP_SECRET: "ZUFALL-MIND-32-ZEICHEN"
BETTER_AUTH_SECRET: "ANDERER-ZUFALL-MIND-32-ZEICHEN"
STRIPE_SECRET_KEY: "sk_test_..."
STRIPE_WEBHOOK_SECRET: "whsec_..."
SMTP_HOST: "smtp.example.org"
SMTP_USER: "benutzer"
SMTP_PASSWORD: "passwort"
---
# Zugang zur SMB-Freigabe (fuer 02-storage.yaml)
apiVersion: v1
kind: Secret
metadata:
name: artista-modelcamp-smb
namespace: artista
stringData:
username: "NAS-BENUTZER"
password: "NAS-PASSWORT"