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
@@ -23,3 +23,6 @@ npm-debug.log*
|
|||||||
|
|
||||||
# Laufzeit-Cache des Design-Hooks (impeccable)
|
# Laufzeit-Cache des Design-Hooks (impeccable)
|
||||||
.impeccable/hook.cache.json
|
.impeccable/hook.cache.json
|
||||||
|
|
||||||
|
# Echte Kubernetes-Secrets (Vorlage: k8s/secrets.yaml.example)
|
||||||
|
k8s/secrets.yaml
|
||||||
+12
-28
@@ -11,13 +11,13 @@ Einmalig einrichten:
|
|||||||
- ein Personal Access Token mit Scope `package` als Repo-Secret `REGISTRY_TOKEN` hinterlegen (Repo-Einstellungen → Actions → Secrets)
|
- ein Personal Access Token mit Scope `package` als Repo-Secret `REGISTRY_TOKEN` hinterlegen (Repo-Einstellungen → Actions → Secrets)
|
||||||
- einen Runner mit dem Label `docker-builder` bereitstellen (wie bei Wabenhain)
|
- einen Runner mit dem Label `docker-builder` bereitstellen (wie bei Wabenhain)
|
||||||
|
|
||||||
Ein Release entsteht dann so: `git tag v0.1.0 && git push origin v0.1.0`, anschließend den Tag im Overlay setzen.
|
Die Manifeste in `k8s/` nutzen den Tag `latest`. Für feste Versionen dort `:latest` durch z. B. `:0.1.0` ersetzen (`git tag v0.1.0 && git push origin v0.1.0`).
|
||||||
|
|
||||||
**Manuell:**
|
**Manuell:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build --target runner -t git.encry.de/nick.rosenloecher/artista-modelcamp:0.1.0 .
|
docker build --target runner -t git.encry.de/nick.rosenloecher/artista-modelcamp:latest .
|
||||||
docker build --target tools -t git.encry.de/nick.rosenloecher/artista-modelcamp-tools:0.1.0 .
|
docker build --target tools -t git.encry.de/nick.rosenloecher/artista-modelcamp-tools:latest .
|
||||||
docker push … # beide Images, immer mit identischem Tag
|
docker push … # beide Images, immer mit identischem Tag
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -32,39 +32,23 @@ Der Build braucht Internet für `next/font` (Schriften werden eingebettet), aber
|
|||||||
|
|
||||||
Eine eigene Sicherung im Cluster gibt es nicht, die SMB-Freigabe wird auf dem NAS gesichert.
|
Eine eigene Sicherung im Cluster gibt es nicht, die SMB-Freigabe wird auf dem NAS gesichert.
|
||||||
|
|
||||||
1. **Namespace:** `kubectl create namespace artista` und `kubectl label namespace artista pod-security.kubernetes.io/enforce=restricted`
|
**Schritt für Schritt zum Kopieren: [k8s/README.md](../k8s/README.md).** Die Manifeste liegen flach in `k8s/` (`00-…` bis `07-…`) und werden mit `kubectl apply -f k8s/` ausgerollt. Die Secrets kommen aus `k8s/secrets.yaml.example` und werden nicht eingecheckt.
|
||||||
2. **Overlay anpassen** (`k8s/overlays/production/`): Image-Tag, Domain in `kustomization.yaml` (APP_URL) und `ingress.yaml`, `ingressClassName` (`kubectl get ingressclass`), ClusterIssuer (Beispiel `letsencrypt-prod`) und TLS-Secret-Name. Die HTTP→HTTPS-Umleitung erwartet das Manifest in Traefik selbst.
|
|
||||||
3. **SMB-Speicher** (`k8s/overlays/production/smb-volumes.yaml`):
|
|
||||||
1. csi-driver-smb installieren, Befehl steht im Kopf der Datei.
|
|
||||||
2. Auf dem NAS die Ordner `modelcamp-postgres` (leer) und `modelcamp-media` anlegen und `source` in der Datei anpassen.
|
|
||||||
3. SMB-Secret anlegen: `kubectl -n artista create secret generic artista-modelcamp-smb --from-literal=username='…' --from-literal=password='…'`. Empfohlen ist ein eigener NAS-Benutzer, der nur diese Ordner sieht.
|
|
||||||
4. Das NAS muss SMB 3.1.1 mit Verschlüsselung erlauben (`seal`), z. B. Synology unter „SMB-Verschlüsselung für Übertragung“, Windows mit `Set-SmbShare -EncryptData $true`.
|
|
||||||
|
|
||||||
Die Rechte setzen die `mountOptions`, auf SMB wirkt `fsGroup` nicht: Postgres `uid=70`, `0700`/`0600`; Medien `uid=1001`, für die App nur lesbar (`0550`/`0440`).
|
Zu PostgreSQL:
|
||||||
|
- **PostgreSQL auf SMB ist eine bewusste Ausnahme.** SMB garantiert Sperren und fsync nicht in jedem Fall wie ein lokales Dateisystem. Bei einem NAS- oder Netzwerkausfall während Schreibvorgängen kann die Datenbank beschädigt werden.
|
||||||
**PostgreSQL auf SMB ist eine bewusste Ausnahme.** SMB garantiert Sperren und fsync nicht in jedem Fall wie ein lokales Dateisystem. Bei einem NAS- oder Netzwerkausfall während Schreibvorgängen kann die Datenbank beschädigt werden. Gemildert wird das durch `hard`, `cache=strict`, Verschlüsselung und ein 120 Sekunden langes, sauberes Herunterfahren. Die NAS-Sicherung ist nur mit **atomaren Snapshots** der Freigabe (Btrfs/ZFS) konsistent. Ein Dateikopie-Backup eines laufenden Datenverzeichnisses lässt sich nicht zuverlässig wiederherstellen. Zusätzlicher Schutz bei Bedarf: gelegentlich einen `pg_dump` ziehen (Abschnitt 7).
|
- **Gemildert** wird das durch `hard`, `cache=strict`, SMB-Verschlüsselung (`seal`) und ein 120 Sekunden langes, sauberes Herunterfahren. Die Rechte setzen die `mountOptions`, weil `fsGroup` auf SMB nicht wirkt: Postgres `uid=70`, `0700`; Medien `uid=1001`, nur lesbar.
|
||||||
4. **Secret anlegen:** Befehl steht in `k8s/secret.example.yaml`, besser über Sealed Secrets oder den External Secrets Operator.
|
- **Sicherung:** Die NAS-Sicherung ist für die laufende Datenbank nur mit **atomaren Snapshots** der Freigabe (Btrfs/ZFS) konsistent. Zusätzlicher Schutz bei Bedarf: gelegentlich einen `pg_dump` ziehen (Abschnitt 7).
|
||||||
5. **Anwenden:** `kubectl apply -k k8s/overlays/production`. Der initContainer migriert die Datenbank vor dem App-Start.
|
|
||||||
6. **Erstes Admin-Konto:**
|
|
||||||
```bash
|
|
||||||
kubectl -n artista run admin-create --rm -it --restart=Never --labels=artista/db-client=true \
|
|
||||||
--image=git.encry.de/nick.rosenloecher/artista-modelcamp-tools:0.1.0 \
|
|
||||||
--overrides='{"spec":{"containers":[{"name":"admin-create","image":"git.encry.de/nick.rosenloecher/artista-modelcamp-tools:0.1.0","command":["tsx","scripts/create-admin.ts","--email","[email protected]","--name","Dein Name"],"envFrom":[{"configMapRef":{"name":"artista-modelcamp-config"}},{"secretRef":{"name":"artista-modelcamp-secrets"}}]}]}}'
|
|
||||||
```
|
|
||||||
Das einmalige Passwort wird ausgegeben. Bei der ersten Anmeldung unter `/admin` wird die 2FA eingerichtet.
|
|
||||||
|
|
||||||
## 3. Update ausrollen
|
## 3. Update ausrollen
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Neuen Tag im Overlay setzen und anwenden. Jeder neue App-Pod migriert per
|
kubectl -n artista rollout restart deploy/artista-modelcamp-app
|
||||||
# initContainer "migrate" zuerst die Datenbank; scheitert das, startet er nicht
|
|
||||||
# und die alten Pods bleiben aktiv (maxUnavailable: 0).
|
|
||||||
kubectl apply -k k8s/overlays/production
|
|
||||||
kubectl -n artista rollout status deploy/artista-modelcamp-app
|
kubectl -n artista rollout status deploy/artista-modelcamp-app
|
||||||
# Migrationsausgabe bei Bedarf:
|
kubectl -n artista logs deploy/artista-modelcamp-app -c migrate # Migrationsausgabe
|
||||||
kubectl -n artista logs deploy/artista-modelcamp-app -c migrate
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Jeder neue App-Pod migriert per initContainer zuerst die Datenbank. Scheitert das, startet der Pod nicht, und die alten Pods laufen weiter.
|
||||||
|
|
||||||
Migrationen sind vorwärtskompatibel zu schreiben (erst Spalten ergänzen, später entfernen), damit alte und neue Pods während des Rollouts parallel laufen können.
|
Migrationen sind vorwärtskompatibel zu schreiben (erst Spalten ergänzen, später entfernen), damit alte und neue Pods während des Rollouts parallel laufen können.
|
||||||
|
|
||||||
## 4. Stripe
|
## 4. Stripe
|
||||||
|
|||||||
@@ -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
|
apiVersion: apps/v1
|
||||||
kind: StatefulSet
|
kind: StatefulSet
|
||||||
metadata:
|
metadata:
|
||||||
name: artista-modelcamp-postgres
|
name: artista-modelcamp-postgres
|
||||||
|
namespace: artista
|
||||||
spec:
|
spec:
|
||||||
serviceName: artista-modelcamp-postgres
|
serviceName: artista-modelcamp-postgres
|
||||||
replicas: 1
|
replicas: 1
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app.kubernetes.io/name: artista-modelcamp-postgres
|
app: artista-modelcamp-postgres
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app.kubernetes.io/name: artista-modelcamp-postgres
|
app: artista-modelcamp-postgres
|
||||||
spec:
|
spec:
|
||||||
automountServiceAccountToken: false
|
terminationGracePeriodSeconds: 120 # sauber herunterfahren vor dem Aushaengen
|
||||||
# Beim Beenden sauber herunterfahren (Checkpoint), bevor das Volume
|
|
||||||
# ausgehaengt wird — auf Netzwerkspeicher besonders wichtig.
|
|
||||||
terminationGracePeriodSeconds: 120
|
|
||||||
securityContext:
|
securityContext:
|
||||||
runAsNonRoot: true
|
runAsNonRoot: true
|
||||||
runAsUser: 70
|
runAsUser: 70
|
||||||
@@ -44,17 +34,11 @@ spec:
|
|||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef: { name: artista-modelcamp-secrets, key: DB_PASSWORD }
|
secretKeyRef: { name: artista-modelcamp-secrets, key: DB_PASSWORD }
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
# Ab PostgreSQL 18 liegt PGDATA unter /var/lib/postgresql/18/docker.
|
|
||||||
- name: data
|
- name: data
|
||||||
mountPath: /var/lib/postgresql
|
mountPath: /var/lib/postgresql
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
exec: { command: ["pg_isready", "-U", "artista"] }
|
exec: { command: ["pg_isready", "-U", "artista"] }
|
||||||
periodSeconds: 5
|
periodSeconds: 5
|
||||||
livenessProbe:
|
|
||||||
exec: { command: ["pg_isready", "-U", "artista"] }
|
|
||||||
initialDelaySeconds: 60
|
|
||||||
periodSeconds: 20
|
|
||||||
failureThreshold: 6
|
|
||||||
resources:
|
resources:
|
||||||
requests: { cpu: 100m, memory: 256Mi }
|
requests: { cpu: 100m, memory: 256Mi }
|
||||||
limits: { memory: 1Gi }
|
limits: { memory: 1Gi }
|
||||||
@@ -63,15 +47,16 @@ spec:
|
|||||||
capabilities: { drop: ["ALL"] }
|
capabilities: { drop: ["ALL"] }
|
||||||
volumes:
|
volumes:
|
||||||
- name: data
|
- name: data
|
||||||
persistentVolumeClaim: { claimName: artista-modelcamp-postgres-data }
|
persistentVolumeClaim: { claimName: artista-modelcamp-postgres }
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
metadata:
|
metadata:
|
||||||
name: artista-modelcamp-postgres
|
name: artista-modelcamp-postgres
|
||||||
|
namespace: artista
|
||||||
spec:
|
spec:
|
||||||
clusterIP: None
|
clusterIP: None
|
||||||
selector:
|
selector:
|
||||||
app.kubernetes.io/name: artista-modelcamp-postgres
|
app: artista-modelcamp-postgres
|
||||||
ports:
|
ports:
|
||||||
- port: 5432
|
- 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),
|
# Alle 10 Minuten: faellige Raten einziehen, abgelaufene Reservierungen
|
||||||
# zieht faellige Raten ein, trennt Zahlungsmethoden nach der letzten Rate und
|
# freigeben, E-Mails nachsenden. Ohne ihn funktioniert Ratenzahlung nicht.
|
||||||
# versendet den E-Mail-Ausgang. Ohne ihn laufen Ratenzahlung und
|
|
||||||
# Reservierungsfreigabe nicht. Idempotent; Forbid verhindert Ueberlappung.
|
|
||||||
apiVersion: batch/v1
|
apiVersion: batch/v1
|
||||||
kind: CronJob
|
kind: CronJob
|
||||||
metadata:
|
metadata:
|
||||||
name: artista-modelcamp-worker
|
name: artista-modelcamp-worker
|
||||||
|
namespace: artista
|
||||||
spec:
|
spec:
|
||||||
schedule: "*/10 * * * *"
|
schedule: "*/10 * * * *"
|
||||||
timeZone: "Europe/Berlin"
|
|
||||||
concurrencyPolicy: Forbid
|
concurrencyPolicy: Forbid
|
||||||
startingDeadlineSeconds: 300
|
|
||||||
successfulJobsHistoryLimit: 3
|
|
||||||
failedJobsHistoryLimit: 5
|
|
||||||
jobTemplate:
|
jobTemplate:
|
||||||
spec:
|
spec:
|
||||||
backoffLimit: 1
|
backoffLimit: 1
|
||||||
activeDeadlineSeconds: 900
|
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
artista/db-client: "true"
|
db-client: "true"
|
||||||
spec:
|
spec:
|
||||||
restartPolicy: Never
|
restartPolicy: Never
|
||||||
automountServiceAccountToken: false
|
|
||||||
securityContext:
|
securityContext:
|
||||||
runAsNonRoot: true
|
runAsNonRoot: true
|
||||||
runAsUser: 1001
|
runAsUser: 1001
|
||||||
seccompProfile: { type: RuntimeDefault }
|
seccompProfile: { type: RuntimeDefault }
|
||||||
containers:
|
containers:
|
||||||
- name: worker
|
- name: worker
|
||||||
image: artista-modelcamp-tools
|
image: git.encry.de/nick.rosenloecher/artista-modelcamp-tools:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
command: ["tsx", "scripts/worker.ts"]
|
command: ["tsx", "scripts/worker.ts"]
|
||||||
envFrom:
|
envFrom:
|
||||||
- configMapRef: { name: artista-modelcamp-config }
|
- configMapRef: { name: artista-modelcamp-config }
|
||||||
- secretRef: { name: artista-modelcamp-secrets }
|
- secretRef: { name: artista-modelcamp-secrets }
|
||||||
resources:
|
|
||||||
requests: { cpu: 50m, memory: 192Mi }
|
|
||||||
limits: { memory: 512Mi }
|
|
||||||
securityContext:
|
securityContext:
|
||||||
allowPrivilegeEscalation: false
|
allowPrivilegeEscalation: false
|
||||||
capabilities: { drop: ["ALL"] }
|
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