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:
1 parent
c84eb1e740
commit
91791b791f
20 files changed
+332
-494
No files matched your search
@@ -0,0 +1,6 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: artista
|
||||
labels:
|
||||
pod-security.kubernetes.io/enforce: restricted
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"] }
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>"
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user