Zum Inhalt springen

Postgres mit CloudNativePG betreiben

Die meisten echten Anwendungen brauchen Postgres, und es auf Kubernetes gut zu betreiben war lange das Argument dagegen, es überhaupt auf Kubernetes zu betreiben. CloudNativePG (CNPG) hat das geändert: ein CNCF-Operator, der PostgreSQL-Cluster deklarativ verwaltet, mit Provisionierung, Replikation, Failover und Backups in einem kurzen Manifest. Dieser Guide führt dich von null zu einer gesicherten Datenbank, die deine App nutzen kann. Die Schritte funktionieren auf jedem konformen Cluster, auch auf einem PaaSbox-Cluster.

  • Einen beliebigen Kubernetes-Cluster und seine kubeconfig.
  • kubectl lokal installiert.
  • Für den Backup-Abschnitt: einen S3-kompatiblen Bucket samt Access-Keys, zum Beispiel bei Hetzner Object Storage, das über dein eigenes Hetzner-Konto abgerechnet wird.

Auf einem PaaSbox-Cluster prüfst du vorher eines. Läuft auf dem Cluster das Add-on PaaSbox Platform (im Labor gebaut, noch nicht veröffentlicht) mit dem Profil saas-http01 oder saas, ist CloudNativePG schon installiert: Überspring Schritt 1 und installiere den Operator kein zweites Mal. Eine App auf der Plattform fordert ihre Datenbank in ihrer SaaSApplication an (data.postgres), mit Backups nach S3; siehe Schritt 2 des Lernpfads, Deine erste App deployen.

Terminal-Fenster
kubectl apply --server-side -f \
https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.30/releases/cnpg-1.30.0.yaml

Das installiert den Operator in cnpg-system (Release 1.30, Stand: 2026-07-09; die aktuelle Version findest du bei den CNPG-Releases). Ab jetzt sind Postgres-Cluster Kubernetes-Ressourcen.

Speichere als pg.yaml. Mit dieser Konfiguration startet eine kleine App:

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: pg
namespace: myapp
spec:
instances: 1
# A zero-eviction PDB would block node drains and protects nothing with a
# single replica. Re-enable (the default) when instances > 1.
enablePDB: false
bootstrap:
initdb:
database: myapp
owner: myapp
storage:
size: 2Gi
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 512Mi
Terminal-Fenster
kubectl create namespace myapp
kubectl apply -f pg.yaml
kubectl -n myapp wait --for=condition=Ready cluster/pg --timeout=300s

Wo das 2-Gi-Volume liegt, hängt von der Standard-Storage-Class des Clusters ab.

  • Auf einem PaaSbox-Cluster ist die Standardklasse local-lvm-thin: Das Volume liegt auf der eigenen Platte des Servers, mit der Geschwindigkeit der lokalen Platte und ohne Zusatzkosten. Es übersteht einen Neustart, einen Reboot und ein Image-Update an Ort und Stelle, nicht einen ersetzten Server.
  • Für ein Volume, das die Pods und den Server überdauert, schaltest du das Add-on Hetzner volumes an (hcloud-csi, siehe Add-ons auswählen) und ergänzt unter storage die Zeile storageClass: hcloud-volumes. Das Volume ist dann ein Hetzner-Volume in deinem Projekt, mindestens 10 GB groß, auf deiner Hetzner-Rechnung. Löschst du den PaaSbox-Cluster, wird es mitgelöscht. Auf einem Cluster, dessen Standardklasse der Volume-Treiber von Hetzner ist, bekommt ein Claim ein solches Volume, ohne die Klasse zu nennen.

CNPG erzeugt alles, was deine App braucht. Für einen Cluster namens pg bekommst du die Services pg-rw (Primary, Read-Write) und pg-ro (Replicas, Read-Only) sowie ein Secret pg-app mit fertigen Zugangsdaten, inklusive eines uri-Keys der Form postgresql://myapp:…@pg-rw.myapp:5432/myapp. Die meisten Frameworks können das direkt verwenden:

env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: pg-app
key: uri

Kein Passwort-Handling, kein manuell angelegtes Secret. Der Operator besitzt und rotiert die Zugangsdaten.

Für die Produktion änderst du eine einzige Zahl:

spec:
instances: 3

CNPG betreibt einen Primary und zwei Streaming-Replicas, verteilt sie über die Nodes und führt automatisch ein Failover durch, wenn der Node des Primary ausfällt. Der Service pg-rw zeigt immer auf den aktuellen Primary, an deiner App ändert sich nichts. Entferne die Zeile enablePDB: false (das Standard-PodDisruptionBudget schützt dich jetzt bei Node-Drains) und stelle sicher, dass der Cluster mindestens drei Nodes hat, die sie ausführen können.

Ein PaaSbox-Cluster hat einen Node. Drei Instanzen würden sich dort einen Server teilen: Das Failover fängt dann einen abgestürzten Postgres-Prozess ab, nicht einen verlorenen Server, und der Speicher verdreifacht sich. Auf einem einzelnen Node bleibst du bei einer Instanz und verlässt dich auf die Backups aus Schritt 5.

Volumes schützen vor Pod-Neustarts, nicht vor DROP TABLE. CNPG macht kontinuierliches WAL-Archiving plus geplante Basis-Backups in den Object Storage. Lege ein Secret mit den Zugangsdaten deines Buckets an und ergänze dann den Backup-Abschnitt:

Terminal-Fenster
kubectl -n myapp create secret generic pg-backup-creds \
--from-literal=ACCESS_KEY_ID=<your-access-key> \
--from-literal=ACCESS_SECRET_KEY=<your-secret-key>
spec:
backup:
barmanObjectStore:
destinationPath: s3://my-backup-bucket/pg
endpointURL: https://nbg1.your-objectstorage.com # your S3-compatible endpoint
s3Credentials:
accessKeyId:
name: pg-backup-creds
key: ACCESS_KEY_ID
secretAccessKey:
name: pg-backup-creds
key: ACCESS_SECRET_KEY
retentionPolicy: 30d

Plane anschließend ein nächtliches Basis-Backup:

apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: pg-nightly
namespace: myapp
spec:
cluster:
name: pg
schedule: "0 0 3 * * *" # 03:00 daily (six-field cron, seconds first)

Weil das WAL-Archiving zwischen den Basis-Backups durchgehend läuft, bekommst du Point-in-Time-Recovery: Ein neues Cluster-Manifest mit einem bootstrap.recovery-Abschnitt stellt jeden beliebigen Zeitpunkt innerhalb deines Aufbewahrungsfensters wieder her. Probe das einmal, bevor du es brauchst.

Ein deklaratives Postgres mit automatischem Failover, Zugangsdaten, die deine App aus einem Secret liest, und Backups außerhalb des Clusters mit Point-in-Time-Recovery: die Liste, die früher die Rechnung für eine Managed Database gerechtfertigt hat. Was dir bleibt, ist der Inhalt: Schema-Migrationen, Datenqualität und das Testen des Restore-Pfads. (Ein Snapshot eines PaaSbox-Clusters enthält den Zustand des Clusters, seine Kubernetes-Objekte, nicht die Inhalte deiner Datenbank. Die sind Anwendungsdaten, und sie zu schützen ist deine Aufgabe. Dieses Setup ist der Weg dazu.)

Wie das in einer vollständigen Anwendung aussieht, zeigt der Guide Eine Django-SaaS deployen. Er nutzt genau dieses CNPG-Muster als Datenbank-Schicht.

Terminal-Fenster
kubectl delete namespace myapp # removes the cluster and its volumes (backups in the bucket remain)