feat: PostgreSQL als StatefulSet im Kubernetes-Cluster deployen
Architektur-Entscheidung geaendert: PostgreSQL laeuft jetzt selbst im Cluster statt auf einer bestehenden externen Instanz. Neu: deploy/k8s/postgres/ mit StatefulSet (postgres:18-alpine, volumeClaimTemplates, pg_isready-Probes), headless Service (apicore-postgres) und Secret-Vorlage. Backend-DATABASE_URL zeigt auf den neuen In-Cluster-Service. CLAUDE.md/ARCHITECTURE.md aktualisiert: Backup-Verantwortung liegt jetzt beim Projekt, nicht mehr beim externen Cluster-/DBA-Betrieb (Backup-CronJob als offener Punkt in TASKS.md ergaenzt).
This commit is contained in:
1 parent
41c82d7dc7
commit
80890a5fc3
7 files changed
+90
-5
No files matched your search
+5
-3
@@ -101,14 +101,16 @@ Fachliche Annahmen, getroffen weil das Lastenheft an der Stelle keine eindeutige
|
|||||||
|
|
||||||
`deploy/k8s/` enthält je Projekt: `deployment.yaml`, `service.yaml`, `ingress.yaml`, sowie `configmap.example.yaml` und `secret.example.yaml` (Platzhalter, keine echten Werte). Backend exponiert `GET /health` (liveness) und `GET /health/ready` (readiness, prüft DB-Verbindung). Beide Projekte je ein schlankes Multi-Stage-Dockerfile (Build-Stage mit vollem `node_modules`, Runtime-Stage mit `npm ci --omit=dev` bzw. Next.js `output: 'standalone'`).
|
`deploy/k8s/` enthält je Projekt: `deployment.yaml`, `service.yaml`, `ingress.yaml`, sowie `configmap.example.yaml` und `secret.example.yaml` (Platzhalter, keine echten Werte). Backend exponiert `GET /health` (liveness) und `GET /health/ready` (readiness, prüft DB-Verbindung). Beide Projekte je ein schlankes Multi-Stage-Dockerfile (Build-Stage mit vollem `node_modules`, Runtime-Stage mit `npm ci --omit=dev` bzw. Next.js `output: 'standalone'`).
|
||||||
|
|
||||||
|
`deploy/k8s/postgres/` enthält das PostgreSQL-`StatefulSet` (ein Replica, `volumeClaimTemplates` für persistente Daten, `postgres:18-alpine`), den zugehörigen headless `Service` (`apicore-postgres`, DNS-Name fürs Backend) sowie `secret.example.yaml` für `POSTGRES_USER`/`POSTGRES_PASSWORD`/`POSTGRES_DB`. `deploy/k8s/backend/secret.example.yaml`s `DATABASE_URL` zeigt auf diesen Service.
|
||||||
|
|
||||||
## 8. Backup & Restore
|
## 8. Backup & Restore
|
||||||
|
|
||||||
**Schema-Migrationen (Reproduzierbarkeit):** Ausschließlich `prisma/migrations/` als Quelle der Wahrheit. Lokal/Entwicklung: `npx prisma migrate dev`. Gegen jede Produktions- oder Staging-Datenbank ausschließlich `npx prisma migrate deploy` (idempotent, wendet nur fehlende Migrationen an, kein Schema-Drift). `prisma db push` wird an keiner Stelle im Code, in Skripten oder im Dockerfile verwendet und ist für Produktions-/Staging-Umgebungen untersagt. `migrate deploy` läuft als eigener Schritt vor dem Rollout (z. B. Kubernetes-`Job` oder manueller Schritt in der Deploy-Pipeline), nicht im `CMD` des Runtime-Images, damit mehrere Pod-Replicas nicht gleichzeitig Migrationen ausführen.
|
**Schema-Migrationen (Reproduzierbarkeit):** Ausschließlich `prisma/migrations/` als Quelle der Wahrheit. Lokal/Entwicklung: `npx prisma migrate dev`. Gegen jede Produktions- oder Staging-Datenbank ausschließlich `npx prisma migrate deploy` (idempotent, wendet nur fehlende Migrationen an, kein Schema-Drift). `prisma db push` wird an keiner Stelle im Code, in Skripten oder im Dockerfile verwendet und ist für Produktions-/Staging-Umgebungen untersagt. `migrate deploy` läuft als eigener Schritt vor dem Rollout (z. B. Kubernetes-`Job` oder manueller Schritt in der Deploy-Pipeline), nicht im `CMD` des Runtime-Images, damit mehrere Pod-Replicas nicht gleichzeitig Migrationen ausführen.
|
||||||
|
|
||||||
**Datensicherung:** Die PostgreSQL-Instanz selbst ist bestehende Cluster-Infrastruktur (siehe CLAUDE.md, "keine eigene DB-Infrastruktur"); Backup-Ausführung liegt beim Cluster-/DBA-Betrieb. Empfohlene Strategie dort:
|
**Datensicherung:** Seit der Entscheidung, PostgreSQL selbst im Cluster zu betreiben (siehe CLAUDE.md), liegt die Backup-Verantwortung bei diesem Projekt, nicht mehr bei einem externen Cluster-/DBA-Betrieb. Aktuell **noch nicht umgesetzt** (offener Punkt, siehe TASKS.md) – empfohlene Strategie:
|
||||||
|
|
||||||
- Tägliches logisches Backup via `pg_dump -Fc` (custom format, komprimiert, für `pg_restore` geeignet), Aufbewahrung nach Retention-Policy des Clusters.
|
- Tägliches logisches Backup via `pg_dump -Fc` (custom format, komprimiert, für `pg_restore` geeignet), z. B. als Kubernetes-`CronJob` gegen den `apicore-postgres`-Service, Ablage auf einem separaten PVC oder Objektspeicher außerhalb des Postgres-eigenen Volumes.
|
||||||
- Kontinuierliches WAL-Archiving/PITR (Point-in-Time Recovery), falls vom Cluster unterstützt, für Wiederherstellung auf einen beliebigen Zeitpunkt zwischen den täglichen Dumps.
|
- Kontinuierliches WAL-Archiving/PITR (Point-in-Time Recovery), falls die Storage-Class/das Backup-Tooling das unterstützt, für Wiederherstellung auf einen beliebigen Zeitpunkt zwischen den täglichen Dumps.
|
||||||
- Restore-Test: `pg_restore` eines Dumps in eine isolierte Datenbank, danach `npx prisma migrate deploy` zur Kontrolle, dass der Migrationsstand konsistent ist (`prisma migrate status`).
|
- Restore-Test: `pg_restore` eines Dumps in eine isolierte Datenbank, danach `npx prisma migrate deploy` zur Kontrolle, dass der Migrationsstand konsistent ist (`prisma migrate status`).
|
||||||
|
|
||||||
**Restore-Ablauf (Kurzfassung):** 1) Zielinstanz/-datenbank bereitstellen. 2) `pg_restore` (oder WAL-Replay bis zum Zielzeitpunkt bei PITR). 3) `npx prisma migrate status` prüfen. 4) `npx prisma migrate deploy` falls seit dem Dump neue Migrationen hinzugekommen sind. 5) `GET /health/ready` gegen die wiederhergestellte Instanz aufrufen, um DB-Erreichbarkeit zu bestätigen, bevor Verkehr umgeschaltet wird.
|
**Restore-Ablauf (Kurzfassung):** 1) Zielinstanz/-datenbank bereitstellen. 2) `pg_restore` (oder WAL-Replay bis zum Zielzeitpunkt bei PITR). 3) `npx prisma migrate status` prüfen. 4) `npx prisma migrate deploy` falls seit dem Dump neue Migrationen hinzugekommen sind. 5) `GET /health/ready` gegen die wiederhergestellte Instanz aufrufen, um DB-Erreichbarkeit zu bestätigen, bevor Verkehr umgeschaltet wird.
|
||||||
@@ -23,7 +23,7 @@ Ein Git-Repo für beide Projekte gemeinsam, Root ist `ApiCore/`. Feature-Branche
|
|||||||
|
|
||||||
**Frontend** (`ApiCore-Frontend/`): Next.js (App Router), TypeScript, Tailwind CSS, shadcn/ui, TanStack Query, Vitest + React Testing Library für Tests. Mobile-first, PWA-Manifest von Anfang an.
|
**Frontend** (`ApiCore-Frontend/`): Next.js (App Router), TypeScript, Tailwind CSS, shadcn/ui, TanStack Query, Vitest + React Testing Library für Tests. Mobile-first, PWA-Manifest von Anfang an.
|
||||||
|
|
||||||
**Betrieb**: Beide Projekte containerisiert, bestehendes Kubernetes-Cluster, bestehende PostgreSQL-Instanz (keine eigene DB-Infrastruktur, kein Managed-Service).
|
**Betrieb**: Beide Projekte containerisiert, bestehendes Kubernetes-Cluster. PostgreSQL läuft selbst im Cluster als `StatefulSet` (`deploy/k8s/postgres/`, kein externer Managed-Service) – Backup-Verantwortung liegt damit bei diesem Projekt, siehe ARCHITECTURE.md Abschnitt 8.
|
||||||
|
|
||||||
## Lokal starten
|
## Lokal starten
|
||||||
|
|
||||||
|
|||||||
@@ -147,6 +147,7 @@ Abgeleitet aus [`docs/lastenheft.md`](docs/lastenheft.md) Abschnitt 26. Reihenfo
|
|||||||
- [x] Prisma-Migrationen reproduzierbar (kein `db push` in Produktion, nur `migrate deploy`)
|
- [x] Prisma-Migrationen reproduzierbar (kein `db push` in Produktion, nur `migrate deploy`)
|
||||||
- [x] Backup-/Restore-Strategie in ARCHITECTURE.md dokumentiert
|
- [x] Backup-/Restore-Strategie in ARCHITECTURE.md dokumentiert
|
||||||
- [x] `/health/ready` prüft DB-Erreichbarkeit
|
- [x] `/health/ready` prüft DB-Erreichbarkeit
|
||||||
|
- [ ] Backup-CronJob (`pg_dump -Fc` gegen `apicore-postgres`) im Cluster einrichten, seit PostgreSQL selbst gehostet wird (`deploy/k8s/postgres/`), siehe ARCHITECTURE.md Abschnitt 8
|
||||||
|
|
||||||
### 21. Datenexport
|
### 21. Datenexport
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ metadata:
|
|||||||
name: apicore-backend-secret
|
name: apicore-backend-secret
|
||||||
type: Opaque
|
type: Opaque
|
||||||
stringData:
|
stringData:
|
||||||
DATABASE_URL: "postgresql://user:password@host:5432/apicore?schema=public"
|
# Host ist der In-Cluster-Service aus deploy/k8s/postgres/service.yaml.
|
||||||
|
# Anderer Namespace als das Backend? Dann FQDN nutzen:
|
||||||
|
# apicore-postgres.<namespace>.svc.cluster.local
|
||||||
|
DATABASE_URL: "postgresql://apicore:change-me-postgres-password@apicore-postgres:5432/apicore?schema=public"
|
||||||
JWT_ACCESS_SECRET: "change-me-access-secret"
|
JWT_ACCESS_SECRET: "change-me-access-secret"
|
||||||
JWT_REFRESH_SECRET: "change-me-refresh-secret"
|
JWT_REFRESH_SECRET: "change-me-refresh-secret"
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# Vorlage – keine echten Werte. Kopieren, echte Werte setzen, per `kubectl apply` außerhalb
|
||||||
|
# der Versionskontrolle anwenden (oder ueber Secret-Manager/Sealed-Secrets im Cluster).
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: apicore-postgres-secret
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
POSTGRES_USER: "apicore"
|
||||||
|
POSTGRES_PASSWORD: "change-me-postgres-password"
|
||||||
|
POSTGRES_DB: "apicore"
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: apicore-postgres
|
||||||
|
spec:
|
||||||
|
clusterIP: None
|
||||||
|
selector:
|
||||||
|
app: apicore-postgres
|
||||||
|
ports:
|
||||||
|
- port: 5432
|
||||||
|
targetPort: 5432
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: StatefulSet
|
||||||
|
metadata:
|
||||||
|
name: apicore-postgres
|
||||||
|
spec:
|
||||||
|
serviceName: apicore-postgres
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: apicore-postgres
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: apicore-postgres
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: postgres
|
||||||
|
image: postgres:18-alpine
|
||||||
|
ports:
|
||||||
|
- containerPort: 5432
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: apicore-postgres-secret
|
||||||
|
env:
|
||||||
|
# Unterordner statt Mount-Root: Postgres' initdb verlangt ein
|
||||||
|
# leeres Datenverzeichnis, der Mount-Root von PersistentVolumes
|
||||||
|
# enthaelt aber oft ein verstecktes "lost+found"-Verzeichnis.
|
||||||
|
- name: PGDATA
|
||||||
|
value: /var/lib/postgresql/data/pgdata
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /var/lib/postgresql/data
|
||||||
|
livenessProbe:
|
||||||
|
exec:
|
||||||
|
command: ["pg_isready", "-U", "$(POSTGRES_USER)", "-d", "$(POSTGRES_DB)"]
|
||||||
|
initialDelaySeconds: 30
|
||||||
|
periodSeconds: 15
|
||||||
|
readinessProbe:
|
||||||
|
exec:
|
||||||
|
command: ["pg_isready", "-U", "$(POSTGRES_USER)", "-d", "$(POSTGRES_DB)"]
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 10
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 256Mi
|
||||||
|
limits:
|
||||||
|
cpu: "1"
|
||||||
|
memory: 1Gi
|
||||||
|
volumeClaimTemplates:
|
||||||
|
- metadata:
|
||||||
|
name: data
|
||||||
|
spec:
|
||||||
|
accessModes: ["ReadWriteOnce"]
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
Reference in new issue
Block a user