PersistentVolumes und StorageClasses
Stateful Storage in Kubernetes.
Lernziele
Nach dieser Lektion kannst du das Storage-Modell von Kubernetes erklären: warum es die Trennung in PersistentVolume und PersistentVolumeClaim gibt, wie StorageClasses dynamisches Provisioning ermöglichen und was Access Modes und Reclaim Policies konkret bedeuten. Du weißt außerdem, wie StatefulSets über volumeClaimTemplates jedem Pod sein eigenes Volume geben — und kannst die klassische Frage „Wie betreibt man eine Datenbank auf Kubernetes?” fundiert beantworten.
Das Problem: Container vergessen alles
Das Dateisystem eines Containers ist flüchtig — bei jedem Neustart beginnt es beim Image-Stand. Ein emptyDir-Volume überlebt zwar Container-Restarts innerhalb des Pods, stirbt aber mit dem Pod. Für eine Datenbank ist beides inakzeptabel: Die Daten müssen den Pod, den Node und am besten auch ein Cluster-Upgrade überleben.
Gleichzeitig will Kubernetes Pods frei über Nodes verschieben können. Lokaler Storage auf einem Node widerspricht dem — landet der Pod woanders, sind die Daten weg. Die Lösung ist, Storage als eigenständige, vom Pod-Lebenszyklus entkoppelte Ressource zu modellieren, die meist auf netzwerkbasiertem Storage liegt (Cloud-Block-Devices wie EBS, NFS, Ceph). Genau dafür gibt es das PV/PVC-Modell.
PV und PVC: warum zwei Objekte statt einem?
Ein PersistentVolume (PV) repräsentiert ein konkretes Stück Storage im Cluster: 50 GiB auf einem EBS-Volume, ein NFS-Export, eine Ceph-RBD. Es ist ein clusterweites Objekt — traditionell die Domäne des Admins, der weiß, welche Storage-Systeme existieren.
Ein PersistentVolumeClaim (PVC) ist die Anfrage aus Anwendungssicht: „Ich brauche 20 GiB, beschreibbar von einem Node.” Der PVC lebt im Namespace des Teams und sagt nichts darüber, woher der Storage kommt. Kubernetes bindet dann einen passenden PV an den Claim (1:1), und der Pod mountet das Volume über den PVC-Namen.
Warum die Indirektion? Sie trennt Zuständigkeiten und macht Manifeste portabel: Das Anwendungsteam beschreibt seinen Bedarf, die Plattform liefert die Implementierung. Dasselbe Manifest läuft auf EKS (EBS dahinter) und on-prem (Ceph dahinter) — der PVC ist die Abstraktionsgrenze. Im Interview bleibt das Bild hängen: „PVC verhält sich zu PV wie Pod zu Node — Anforderung trifft Kapazität.”
PVs von Hand anzulegen (statisches Provisioning) skaliert allerdings nicht — niemand will für jeden Claim ein Ticket. Deshalb:
StorageClasses: Storage auf Zuruf
Eine StorageClass beschreibt eine Sorte Storage und vor allem, wie man sie automatisch erzeugt: welcher Provisioner (ein CSI-Driver, Lektion 16) zuständig ist und mit welchen Parametern (z. B. Volume-Typ gp3 bei AWS, Replikationsfaktor bei Ceph). Verweist ein PVC über storageClassName auf eine Klasse, passiert dynamisches Provisioning: Der Provisioner legt das Volume im Storage-System an, erzeugt das PV-Objekt und bindet es — ohne Admin-Beteiligung. Eine StorageClass kann als Default markiert sein und greift dann für alle PVCs ohne explizite Angabe.
Zwei Detail-Einstellungen mit großer Wirkung:
volumeBindingMode: WaitForFirstConsumer verzögert das Provisioning, bis ein Pod den Claim tatsächlich nutzt. Klingt nach Kleinkram, verhindert aber ein reales Problem: Cloud-Block-Volumes sind an eine Availability Zone gebunden. Wird das Volume sofort erzeugt (Immediate), kann es in Zone A landen, während der Scheduler den Pod nur in Zone B unterbringen kann — der Pod hängt dann für immer in Pending. Mit WaitForFirstConsumer entscheidet erst der Scheduler über den Node, dann entsteht das Volume in der passenden Zone.
reclaimPolicy bestimmt das Schicksal des Volumes nach dem Löschen des PVC: Delete räumt PV und echtes Storage-Volume weg — bequem, aber bei Produktionsdaten riskant. Retain behält beides; das PV wird Released und muss manuell aufgeräumt werden, dafür sind die Daten nach einem versehentlichen kubectl delete pvc noch da. (Recycle ist deprecated — erwähnen, aber nicht mehr verwenden.) Für dynamisch provisionierte Volumes ist Delete der übliche Default; für Datenbanken setzen viele Teams bewusst Retain.
Dazu die Access Modes, präzise verstanden: RWO (ReadWriteOnce) — beschreibbar von einem Node (nicht Pod!), der Normalfall für Block-Storage wie EBS. ROX (ReadOnlyMany) — lesbar von vielen Nodes. RWX (ReadWriteMany) — beschreibbar von vielen Nodes, das können nur geteilte Dateisysteme wie NFS, CephFS oder EFS. Die Fangfrage „Können zwei Pods dasselbe RWO-Volume nutzen?” beantwortest du mit: Ja, wenn sie auf demselben Node laufen — deshalb steht da Once pro Node, nicht pro Pod.
StatefulSets: jedem Pod sein eigenes Volume
Ein Deployment mit einem PVC gibt allen Replicas denselben Claim — bei RWO-Storage kollidiert das sofort. Eine Datenbank mit drei Instanzen braucht aber drei eigene Volumes, fest verheiratet mit je einer Instanz.
Das leistet das StatefulSet mit volumeClaimTemplates: Für jeden Pod (postgres-0, postgres-1, postgres-2) wird aus dem Template ein eigener PVC erzeugt (data-postgres-0, …). Die Bindung ist stabil: Wird postgres-1 neu erstellt — auch auf einem anderen Node — bekommt er wieder exakt seinen PVC und damit seine Daten. Dazu liefert das StatefulSet stabile DNS-Namen über einen Headless Service und eine geordnete Start-Reihenfolge. Wichtig zu wissen: Beim Löschen oder Herunterskalieren eines StatefulSets bleiben die PVCs standardmäßig bestehen — Datenschutz vor Bequemlichkeit.
Praxis
Dynamisches Provisioning mit kind beobachten (kind bringt eine Default-StorageClass mit lokalem Provisioner mit):
kind create cluster
kubectl get storageclass # "standard" mit WaitForFirstConsumer
kubectl apply -f - <<EOF
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
EOF
kubectl get pvc data # Pending — WaitForFirstConsumer wartet auf einen Pod
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: writer
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "echo ueberlebt > /data/test.txt && sleep 3600"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: data
EOF
kubectl get pvc,pv # jetzt Bound — PV wurde dynamisch erzeugt
# Persistenz beweisen: Pod löschen, neu anlegen, Datei ist noch da
kubectl delete pod writer
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: reader
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c", "cat /data/test.txt && sleep 3600"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: data
EOF
kubectl logs reader # "ueberlebt"
Der Moment, in dem der PVC von Pending auf Bound springt, sobald der Pod erscheint, ist WaitForFirstConsumer live — und kubectl logs reader ist der Beweis, dass Daten den Pod-Tod überleben.
Typische Stolperfallen
PVC hängt in Pending: Entweder wartet WaitForFirstConsumer schlicht auf einen Pod (kein Fehler!), oder es gibt keine passende StorageClass/keinen passenden PV. kubectl describe pvc zeigt die Events — immer zuerst dort nachsehen.
RWO-Volume blockiert Rolling Update: Deployment mit RWO-PVC und Strategie RollingUpdate: Der neue Pod kann das Volume nicht mounten, solange der alte es hält — Deadlock. Abhilfe: Strategie Recreate oder gleich ein StatefulSet.
Multi-Attach-Error nach Node-Ausfall: Stirbt ein Node unsauber, gilt das Block-Volume noch als attached; der Ersatz-Pod auf einem anderen Node scheitert mit „Multi-Attach error”, bis das Detach durchläuft. Das zu kennen spart im Incident Panik.
Volume voll, weil Vergrößern nicht geht: PVCs lassen sich nur vergrößern, wenn die StorageClass allowVolumeExpansion: true setzt — und verkleinern geht nie. Kapazität von Anfang an realistisch planen.
Interview-Vorbereitung
Die Standardfrage „Wie funktioniert persistenter Storage in Kubernetes?” beantwortest du als Kette: Problem (Container-FS ist flüchtig, Pods wandern über Nodes) → Modell (PV = Storage-Angebot, PVC = namespaced Anforderung, 1:1-Bindung; Trennung entkoppelt App-Teams von Storage-Details) → Automatisierung (StorageClass + CSI-Provisioner = dynamisches Provisioning, WaitForFirstConsumer gegen das Zonen-Problem) → Betrieb (Access Modes pro Node gedacht, Reclaim Policy Retain vs. Delete bewusst wählen, StatefulSet mit volumeClaimTemplates für Datenbanken).
Typische Follow-ups:
- „Was bedeutet ReadWriteOnce wirklich?” — Ein Node darf schreibend mounten; mehrere Pods auf demselben Node gehen. Für echtes Multi-Node-Schreiben braucht es RWX und ein geteiltes Dateisystem.
- „Was passiert beim Löschen eines PVC?” — Hängt an der reclaimPolicy des PV: Delete entfernt Volume und Daten, Retain lässt das PV als Released zurück.
- „Warum StatefulSet statt Deployment für eine Datenbank?” — Pro Pod ein eigener, stabil zugeordneter PVC via volumeClaimTemplates, stabile Identität und DNS-Namen, geordnetes Starten.
- „PVC bleibt Pending — Diagnose?” —
describe pvcfür Events; dann: StorageClass vorhanden? Provisioner gesund? Oder wartet nur WaitForFirstConsumer auf den ersten Pod?
Zusammenfassung
Kubernetes entkoppelt Storage vom Pod-Lebenszyklus: PVs bieten Kapazität an, PVCs fordern sie namespaced an, StorageClasses und CSI-Provisioner erzeugen Volumes on demand — mit WaitForFirstConsumer als Schutz vor dem Zonen-Mismatch. Access Modes gelten pro Node, die Reclaim Policy entscheidet über Datenleben nach dem Claim, und StatefulSets geben jeder Replika ihr eigenes, wiederfindbares Volume.
Offen blieb, wer die Volumes eigentlich erzeugt, attached und mountet: die CSI-Driver. Deren Architektur — Controller- und Node-Plugin, Snapshots, Cloud- vs. On-Prem-Optionen — ist das Thema der nächsten Lektion.