• English icon
  • German icon
Erklärvideo

Kubernetes-Pod-Daten nach dem Neustart weg? AKS-Disks vs. Azure Files

· Reihe: Container auf Azure

azureakskubernetesstoragepersistent-volumeskubectl

Das Video ist auf Englisch.

Ich habe in meinem Pod auf Azure Kubernetes Service (AKS) eine Notiz geschrieben, neben die Logdatei, die die App führt. Dann habe ich den Pod gelöscht. Kubernetes hat in 22 Sekunden einen neuen gestartet. Eine der beiden Dateien hat überlebt. Die andere war weg.

Dieser Beitrag zeigt, wo die Daten eines Pods wirklich liegen, warum ReadWriteOnce einen Node bedeutet und nicht einen Pod, und wann die Azure-Disk hinter einem Claim endgültig gelöscht wird. Alles stammt aus der Übung des Microsoft-Learn-Moduls und ein paar zusätzlichen Schritten, ausgeführt in meinem eigenen Cluster.

Das Wichtigste

  • Die eigenen Dateien eines Containers starten leer. Jeder Container startet aus seinem Image plus einer frischen, leeren beschreibbaren Schicht. Ein ersetzter Pod bekommt einen neuen Container, und selbst nach einem Absturz startet das Kubelet den Container in einem sauberen Zustand neu. Was Sie später noch brauchen, gehört auf ein Volume.
  • Ein Claim ist eine Anfrage, ein Volume der Speicher, die Disk das, was Azure abrechnet. Ein PersistentVolumeClaim fragt nach Größe, Zugriffsmodus und Storage-Klasse; die Klasse erzeugt ein PersistentVolume, und auf AKS legt die Standardklasse managed-csi eine Azure Managed Disk in der Node-Ressourcengruppe an.
  • ReadWriteOnce bedeutet ein Node, nicht ein Pod. Zwei Pods auf demselben Node haben sich eine Disk geteilt. Ein Pod auf einem zweiten Node wartete in ContainerCreating, weil die Disk am ersten Node hing. Für mehrere Nodes nehmen Sie Azure Files (azurefile-csi, ReadWriteMany).
  • Den Claim zu löschen, löscht die Disk. Die eingebauten Storage-Klassen haben die Reclaim-Policy Delete. Eine Storage-Klasse mit reclaimPolicy: Retain behält die Disk, wenn der Claim weg ist, und Sie löschen sie selbst.

Weg mit dem Container

In der Übung betreibt ein Deployment eine kleine API. Ein Deployment hält eine feste Anzahl Pods aus einer Vorlage am Laufen, und ein Pod ist die Einheit, die Ihren Container umschließt. Die App schreibt für jede Anfrage eine Zeile in eine Logdatei:

> python cfg.py get /logs/summary
GET /logs/summary -> 200
{… "total_requests":264, … "first_request":"2026-10-28T12:31:16.705746", …}

Dann habe ich eine Notiz in den Ordner /tmp des Containers geschrieben, den Pod gelöscht und gewartet, bis das Deployment ihn ersetzt hatte:

> kubectl exec deploy/aks-config-api -- sh -c "echo note > /tmp/note.txt; ls /tmp"
note.txt
> kubectl delete pod -l app=aks-config-api
pod "aks-config-api-876f7899f-jqpxj" deleted from default namespace
> kubectl rollout status deployment aks-config-api --timeout 300s
deployment "aks-config-api" successfully rolled out

Im neuen Pod war /tmp leer, während das Log ab derselben ersten Anfrage weiterzählte:

> kubectl exec deploy/aks-config-api -- ls /tmp
> python cfg.py get /logs/summary
{… "total_requests":266, … "first_request":"2026-10-28T12:31:16.705746", …}

Warum: Jeder Container startet aus seinem Image plus einer frischen, leeren Schicht für alles, was er schreibt. Ein neuer Pod bekommt einen neuen Container, also startet er leer. Sogar ein Absturz wirkt so: Das Kubelet, der Kubernetes-Agent auf dem Node (der virtuellen Maschine, auf der Ihre Pods laufen), startet einen abgestürzten Container in einem sauberen Zustand neu. Wer Docker kennt, wird hier überrascht: docker restart behält die Dateien eines Containers, Kubernetes startet dagegen einen frischen Container.

Das Log hat überlebt, weil es nie in den eigenen Dateien des Containers lag. Es lag auf einem Volume: Speicher von außerhalb des Pods, unter einem Ordnerpfad eingebunden.

Der Bestellzettel: Claim, Volume, Disk

Die Übung fordert Speicher mit einem PersistentVolumeClaim (kurz Claim) an: einer Anfrage, die sagt, wie viel Speicher, wie er geteilt werden darf und welche Storage-Klasse ihn anlegt. Eine Storage-Klasse ist das Rezept, das den Speicher erzeugt. Gekürzt:

kind: PersistentVolumeClaim
metadata:
name: api-logs-pvc
spec:
resources: {requests: {storage: 1Gi}}
accessModes: [ReadWriteOnce]
storageClassName: managed-csi

Das Deployment bindet den Claim über seinen Namen in den Ordner ein, in den die App ihr Log schreibt:

# the pod template
volumes:
- name: log-storage
persistentVolumeClaim: {claimName: api-logs-pvc}
# the container
volumeMounts:
- {name: log-storage, mountPath: /var/log/api}

Der Claim war Bound, gebunden an ein Volume mit generiertem Namen. Diesen Speicher gab es vorher nicht: managed-csi (die Standardklasse auf AKS) wartet auf den ersten Pod, der den Claim nutzt, und legt ihn dann an. Spalten mit „…“ gekürzt, der lange Volume-Name auf sein Ende verkürzt:

> kubectl get pvc api-logs-pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS …
api-logs-pvc Bound pvc-…0001 1Gi RWO managed-csi …
> kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS …
pvc-…0001 1Gi RWO Delete Bound …

Hinter dem Volume steht eine Azure Managed Disk mit dem Namen des Volumes, in der Node-Ressourcengruppe des Clusters, der Ressourcengruppe, die AKS für die eigenen Ressourcen des Clusters führt:

> az disk list -g MC_rg-demo_aks-demo_westeurope --query "[].{name:name, gb:diskSizeGB}" -o table
Name Gb
---------------------------------------- ----
pvc-00000000-0000-0000-0000-000000000001 1

Der Claim ist also die Anfrage, das Volume der Speicher, den Kubernetes kennt, und die Disk das, was Azure speichert und abrechnet. Die Daten liegen auf dieser Disk, außerhalb jedes Pods.

ReadWriteOnce: einmal pro was?

Die meisten lesen ReadWriteOnce als einen Pod. Ich habe das Deployment auf zwei Replikate skaliert, während der Cluster nur einen Node hatte. Beide starteten, beide auf demselben Node, beide schrieben auf dieselbe Disk:

> kubectl get pods -l app=aks-config-api -o wide
NAME READY STATUS … NODE …
aks-config-api-876f7899f-csl9m 1/1 Running … node …vmss000000 …
aks-config-api-876f7899f-tmq9k 1/1 Running … node …vmss000000 …

ReadWriteOnce bedeutet ein Node, nicht ein Pod. Eine Azure-Disk hängt an genau einer virtuellen Maschine, dem Node, und jeder Pod auf diesem Node kann sie nutzen. (Für genau einen Pod im ganzen Cluster hat Kubernetes einen eigenen Zugriffsmodus, ReadWriteOncePod.)

Danach habe ich einen zweiten Node und ein zweites Deployment hinzugefügt: zwei Pods, eine neue ReadWriteOnce-Disk und eine Pod-Anti-Affinity-Regel, die die beiden Pods auf verschiedene Nodes legt. Der erste Pod lief nach 15 Sekunden. Der zweite blieb die ganzen zweieinhalb Minuten, die ich zugesehen habe, in ContainerCreating. Das Warning-Event sagt, warum (Auszug):

> kubectl events --types=Warning
FailedAttachVolume … Volume is already used by pod(s) disk-writer-…xqsgp

Die Disk hing am ersten Node, und der zweite Node kann sie nicht anhängen, solange der erste Pod sie nutzt. Kein Absturz, kein Fehler in der App, nur ein Pod, der nie startet. Die AKS-Troubleshooting-Dokumentation führt diese Meldung als Multi-Attach-Fehler. Auch ohne Anti-Affinity-Regel kann der Scheduler Replikate auf mehrere Nodes verteilen, ein Deployment mit Disk und mehreren Replikaten kann also ebenfalls darauf stoßen.

Viele Nodes, eine Freigabe

Für Pods auf mehreren Nodes nehmen Sie eine andere Storage-Klasse: azurefile-csi. Sie legt eine Azure-Files-Freigabe an, eine Dateifreigabe in einem Azure-Speicherkonto (AKS legt das Konto in der Node-Ressourcengruppe an), und jeder Node bindet sie über das Netzwerk ein. Ihr Zugriffsmodus ist ReadWriteMany. Im Claim ändern sich nur zwei Zeilen:

kind: PersistentVolumeClaim
metadata: {name: shared-files}
spec:
resources: {requests: {storage: 1Gi}}
accessModes: [ReadWriteMany]
storageClassName: azurefile-csi

Mit demselben Deployment über zwei Nodes warteten beide Pods etwa 25 Sekunden, während die Freigabe angelegt wurde, dann liefen sie, einer pro Node. Ein Pod schrieb eine Zeile in die Freigabe, und beide lasen sie:

> python cfg.py share files-writer
0.8s files-writer-6bc8ddf6b-85gqk writes /data/note.txt
1.3s files-writer-6bc8ddf6b-85gqk reads: written by the first pod
1.8s files-writer-6bc8ddf6b-zcmcc reads: written by the first pod

Warum dann nicht immer eine Freigabe? Eine Datenbank erwartet ihre Dateien für sich allein, deshalb legt das Modul Datenbanken auf Disks. Azure Files ist für Dateien, die viele Pods teilen.

Azure Disk (managed-csi)Azure Files (azurefile-csi)
ZugriffsmodusReadWriteOnceReadWriteMany
Wer sie nutzen kanndie Pods auf jeweils einem NodePods auf vielen Nodes gleichzeitig
Typischer Einsatzdie Daten einer DatenbankDateien, die viele Pods teilen
Beim Löschen des Claimsdie Disk wird gelöschtdie Freigabe wird gelöscht

Die Spalte, die die Disk löscht

Die Spalte in der Volume-Liste hat es von Anfang an gesagt: Reclaim-Policy Delete. Sie legt fest, was mit dem Speicher passiert, wenn sein Claim gelöscht wird. Ich habe den Claim des Tests mit zwei Nodes zusammen mit seinem Deployment gelöscht (nur das Deployment zu löschen, hätte den Claim und seine Disk behalten) und die Disks eineinhalb Minuten später noch einmal aufgelistet:

> kubectl delete -f k8s/storage/disk-writer.yaml
persistentvolumeclaim "shared-disk" deleted from default namespace
deployment.apps "disk-writer" deleted from default namespace
> az disk list -g MC_rg-demo_aks-demo_westeurope --query "[].{name:name, gb:diskSizeGB}" -o table
Name Gb
---------------------------------------- ----
pvc-00000000-0000-0000-0000-000000000001 1

Eine Disk übrig, die der Übung. Die Disk des Tests war weg, samt ihren Daten. Das tun alle eingebauten Storage-Klassen. Um eine Disk zu behalten, legen Sie eine eigene Storage-Klasse mit reclaimPolicy: Retain an und nutzen sie im Claim (angepasst aus dem Beispiel der AKS-Dokumentation):

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata: {name: managed-csi-retain}
provisioner: disk.csi.azure.com
parameters: {skuName: StandardSSD_LRS}
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer

Dann bleibt die Disk, wenn der Claim weg ist. Sie löschen sie selbst, und Azure rechnet sie ab, bis Sie das tun. AKS überschreibt Änderungen an seinen eingebauten Klassen, legen Sie also eine eigene Klasse an, statt managed-csi zu ändern.

Was passiert ist, als ich es ausgeführt habe

Getestet am 28. Oktober 2026 in meinem eigenen Azure-Abonnement (AKS 1.36 in westeurope, Azure CLI 2.88.0, kubectl 1.36); danach wurde alles gelöscht. Der Cluster startete mit einem Node Standard_D2s_v5; der zweite Node brauchte 161,9 Sekunden. Der ersetzte Pod war 22,0 Sekunden nach dem Löschen bereit; die erste Anfrage an die App direkt nach dem Rollout lief einmal in ein Timeout und wurde beim zweiten Versuch beantwortet. Der zweite disk-writer-Pod stand von Sekunde 4,0 bis zum Ende der 150 Sekunden Beobachtung in ContainerCreating. Den Disk-Claim samt Deployment zu löschen, dauerte 31,2 Sekunden (Kubernetes behält einen Claim, solange ein Pod ihn nutzt); solange die Disk noch am ersten Node hing, zeichnete Kubernetes Warnungen VolumeFailedDelete … is still attached to node auf, und 90 Sekunden später war die Disk weg. Für Azure Files gibt die Übung der Kubelet-Identität die Rolle Storage Account Contributor auf der Node-Ressourcengruppe; das habe ich genauso gemacht.

Was das Video weglässt

  • emptyDir: ein Scratch-Volume, das einen Container-Absturz übersteht, aber gelöscht wird, wenn der Pod den Node verlässt.
  • StatefulSets mit volumeClaimTemplates, ein Claim pro Replikat, die übliche Antwort für Datenbanken mit mehreren Replikaten.
  • Azure Container Storage, Azure Blob Storage über den CSI-Treiber, Premium SSD v2 und zonenredundante Disks (ZRS), die AKS in Clustern über Verfügbarkeitszonen nutzt.
  • Volume-Erweiterung, Snapshots und AKS-Backup.

Weiterführende Links

Video-Transkript (Englisch)

I wrote a note inside my pod on Azure Kubernetes Service, next to the log file the app keeps. Then I deleted the pod. Kubernetes started a new one in twenty-two seconds. One of the two files survived. The other was gone.

In this video, you'll see where a pod's data really lives, and when it's gone for good. At the end: the one column in the volume list that said how my data would be deleted.

Which file survives a new pod? In the Microsoft Learn module's exercise, a deployment runs a small API. A deployment keeps a set number of pods running from one template, and a pod is the unit that wraps your container. The app writes one line to a log file for every request it gets. With a small script of mine that calls the app, I asked it what it had logged so far. Two hundred sixty-four requests, the first one at twelve thirty-one. Then I wrote a short note into the container's temp folder, with kubectl exec, which runs a command inside the container. There it is: note.txt. Then I deleted the pod, and asked kubectl to wait until the deployment had replaced it. Back after twenty-two seconds, with a new pod from the same template. One file was gone. Which one was gone: the note, or the log? The note. The temp folder was empty. I listed it again, in the new pod, with the same kubectl exec. Nothing. Not one file. And the log? I asked the app again, with the same script. Two hundred sixty-six requests, and the same first request at twelve thirty-one. Nothing lost. Here's why. Every container starts from its image, plus a fresh, empty layer for whatever it writes. A new pod gets a new container, so it starts clean. Even a crash does that. A node is the virtual machine your pods run on, and its agent, the kubelet, restarts a crashed container with a clean state. If you know Docker, that's the surprise: docker restart keeps a container's files. Kubernetes starts a fresh container instead. The log survived because it was never in the container's own files. It was on a volume: storage from outside the pod, mounted at a folder path. The container's own files live as long as the container. Anything you need later goes on a volume.

So where was the log, if not in the container? The exercise asks for storage with a PersistentVolumeClaim, or claim for short: a request that says how much storage, how it may be shared, and which storage class makes it. A storage class is the recipe that creates the storage. In its YAML: one gibibyte, ReadWriteOnce, and managed-csi, the default storage class on AKS, which creates an Azure managed disk. The deployment then mounts the claim by its name, at the folder where the app writes its log. I applied both, then asked kubectl for the claim, the way the module checks it. Bound, to a volume with a generated name. Bound means the claim has its storage. Did that storage exist before? No. The storage class created it, when the first pod needed the claim. So I listed the volumes, the PersistentVolumes, the cluster has. One gibibyte, ReadWriteOnce, bound to my claim. Keep an eye on one column here, reclaim policy. We'll come back to it. That's the piece of storage Kubernetes knows, and the volume the pod mounts. And behind it, in Azure? I listed the disks in the cluster's node resource group, the resource group AKS keeps for the cluster's own resources. One disk, one gigabyte, with the volume's name. So the claim is the request, the volume is the storage Kubernetes knows, and the disk is what Azure stores and bills. The data sits on that disk, outside every pod.

ReadWriteOnce: once per what? Most people read it as one pod. So I scaled the deployment to two replicas: two pods, one ReadWriteOnce claim, and the cluster still had a single node. Do both pods start? Both started. I asked kubectl where they run, with the wide output, which adds the node. Both Running, both on the same node, both writing to the same disk. ReadWriteOnce means one node, not one pod. An Azure disk attaches to one virtual machine, the node, and every pod on that node can use it. This series runs one Azure service at a time, for real. Subscribe to follow the series. So what happens with two nodes? I added a second node to the cluster. Then a second deployment: two pods, one new ReadWriteOnce disk, and a pod anti-affinity rule, which tells Kubernetes to put the two pods on different nodes. What happens to the pod on the second node? It waited. I watched both pods with a small script of mine, for two and a half minutes. The first one was Running after fifteen seconds. The second stayed in ContainerCreating the whole time. Kubernetes recorded why, in a warning event. I listed the warnings with kubectl events. Failed attach volume. Volume is already used by pod, and it names the first one. The disk was attached to the first node. The second node can't attach it while the first pod uses it. No crash, no error in the app, just a pod that never starts. When a pod hangs in ContainerCreating, the events say why. The video about pods that won't start shows how to read them. One Azure disk, one node at a time.

So how do pods on two nodes share data? With a different storage class: azurefile-csi. It creates an Azure Files share, a file share in an Azure storage account, and every node mounts it over the network. Its access mode is ReadWriteMany. In the claim, only two lines change: the access mode, and the storage class. I applied the same two-node deployment with that claim, and watched the pods with my script. Both pods waited twenty-five seconds while the share was created. Then both ran, one on each node. Then one pod wrote a line into the share, and both pods read it back. Written by the first pod. The second pod, on the other node, read the same line. So why not always use a share? A database expects its files to itself, so the module puts databases on disks. Azure Files is for files that many pods share. One node at a time: a disk. Many nodes and the same files: a share.

To sum it up: a container's own files start clean, every time a container starts. A claim keeps data on an Azure disk, outside the pod. ReadWriteOnce means one node, not one pod. For many nodes, use Azure Files.

And that column in the volume list? Reclaim policy: Delete. It says what happens to the disk when its claim is deleted. So I deleted the claim of the two-node test, together with its deployment. Deleting only the deployment would have kept the claim and its disk. Deleted, both. A minute and a half later, I listed the disks again, with the same command as before. One disk left, the exercise's. The test's disk was gone, and the data on it. The built-in storage classes all do that: the Azure disk goes with the claim. To keep it, make your own storage class with reclaimPolicy Retain, and use it in the claim. Then the disk stays after the claim is gone. You delete it yourself, and Azure bills it until you do.

Everything here started with one kubectl apply. The video on screen follows one, second by second, to the app's first answer. If this was useful, subscribe for more practical .NET and Azure.

Brauchen Sie das in Ihrer eigenen Azure-Umgebung?

Ich entwickle, migriere und betreibe .NET-Plattformen auf Azure, mit Azure DevOps und Terraform. Wenn Ihr Team das sauber aufsetzen möchte, sehen Sie sich mein Angebot an.

Zu den Leistungen