apiVersion: v1
kind: Service
metadata:
name: db
namespace: data
spec:
clusterIP: None
selector:
app: db
ports:
- name: postgres
port: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
namespace: data
spec:
serviceName: db
replicas: 3
selector:
matchLabels:
app: db
persistentVolumeClaimRetentionPolicy:
whenDeleted: Retain
whenScaled: Retain
template:
metadata:
labels:
app: db
spec:
terminationGracePeriodSeconds: 60
containers:
- name: postgres
image: postgres:18
ports:
- containerPort: 5432
envFrom:
- secretRef:
name: db-credentials
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: gp3
resources:
requests:
storage: 100Gi
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: db
namespace: data
spec:
maxUnavailable: 1
selector:
matchLabels:
app: db
---
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: ebs-snapshots
driver: ebs.csi.aws.com
deletionPolicy: Retain
---
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: db-0-2026-11-13
namespace: data
spec:
volumeSnapshotClassName: ebs-snapshots
source:
persistentVolumeClaimName: data-db-0
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-db-restore-0
namespace: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: gp3
resources:
requests:
storage: 100Gi
dataSource:
apiGroup: snapshot.storage.k8s.io
kind: VolumeSnapshot
name: db-0-2026-11-13All graphs
Kubernetes graph
A StatefulSet database: headless Service, per-Pod volumes, snapshots and restore
The objects around a replicated database on Kubernetes: a StatefulSet with a headless Service and one PersistentVolumeClaim per Pod, a PodDisruptionBudget, and a VolumeSnapshot restored into a new claim.
A StatefulSet gives each replica a fixed name and its own volume. db-0 is always db-0, it always gets the claim data-db-0, and it has a DNS name of its own. That is what a database needs and what a Deployment can't give.
The second half of the graph is the backup path: a CSI snapshot of one replica's volume, and a new claim created from it.
Interactive graph
Click a resource to see what it does, or follow the setup step by step.
You write itCreated by a controllerComes with an add-onreferencesselects by labelcreates
How it works
All 10 resources and what they do
- Service dbv1
- A headless Service (
clusterIP: None). It has no virtual IP: DNS returns the Pod addresses, and each Pod gets a name such asdb-0.db.data.svc.cluster.local. Replicas use those names to find each other. - StatefulSet dbapps/v1
- Creates
db-0,db-1anddb-2in order, each from the same template, and one claim per Pod fromvolumeClaimTemplates.serviceNameties the Pods to the headless Service for their DNS names. - Pod db-0, db-1, db-2v1
- Stable names with an ordinal.
db-1starts only whendb-0is Ready, and a rolling update goes from the highest ordinal down. A Pod that is replaced comes back with the same name and the same claim. - PersistentVolumeClaim data-db-0, data-db-1, data-db-2v1
- One per Pod, named
<template>-<pod>. The StatefulSet creates them and, with the retention policy onRetain, never deletes them, not on scale-down and not when the StatefulSet is deleted. - StorageClass gp3storage.k8s.io/v1
- Provisions each volume when its Pod is scheduled (
WaitForFirstConsumer), in that Pod's zone. A volume lives in one zone, so its Pod is tied to that zone for good. - PodDisruptionBudget dbpolicy/v1
- Lets a node drain or an autoscaler evict one database Pod at a time. A second eviction is refused until the first Pod is Ready again, which protects the quorum.
- VolumeSnapshotClass ebs-snapshotssnapshot.storage.k8s.io/v1
- Which CSI driver takes snapshots and what happens to the cloud snapshot when the object is deleted.
Retainkeeps it. - VolumeSnapshot db-0-2026-11-13snapshot.storage.k8s.io/v1
- A request for a point-in-time copy of one claim.
status.readyToUseturns true when the driver has finished. It is crash-consistent: what Postgres would find after a power cut. - VolumeSnapshotContent snapcontent-8c1f…snapshot.storage.k8s.io/v1
- The cluster-scoped record of the real snapshot, with its ID at the cloud provider. It is to a VolumeSnapshot what a PersistentVolume is to a claim.
- PersistentVolumeClaim data-db-restore-0v1
- A new claim whose
dataSourceis the snapshot. The driver creates a volume from it, at least as large as the original. Nameddata-<statefulset>-0, it is adopted by a StatefulSet calleddb-restore.
Common mistakes
kubectl delete statefulset dbleaves the claims and the data. Deleting a claim with aDeletereclaim policy destroys the disk. Know which one you are typing.volumeClaimTemplatescan't be edited. To grow the volumes, expand each claim (the class needsallowVolumeExpansion), then recreate the StatefulSet with--cascade=orphanand the new size.- A volume is zonal. If its zone has no node with room, the Pod stays Pending with
volume node affinity conflict, however empty the other zones are. - A snapshot of a running database is crash-consistent only. For a clean one, use the database's own backup tool or take the snapshot from a replica you paused.
- VolumeSnapshot needs the snapshot CRDs and the snapshot controller, which many clusters don't install with the CSI driver. Without them the object is accepted by nothing and
kubectl get volumesnapshotfails.
The manifests you write
Everything above that is marked "You write it", in one file. The rest comes from add-ons and controllers.
Keep going
Kubernetes quizTest yourself on Pods, Deployments, Services, scheduling, storage, networking, security and kubectl.Try it in the playgroundRecover a StatefulSet Pod from a dead nodeTry it in the playgroundRecover data from a Released PersistentVolumeTry it in the playgroundDrain a node without breaking a PodDisruptionBudgetArticleHow to debug OOMKilled containers in KubernetesArticleCVE-2026-123158 (*sshole): the OpenSSH password bypass on AlpineArticleHow to drain Kubernetes nodes safely with PodDisruptionBudgets
More graphs
- Deployment, Service and Ingress: how a request reaches your Pods
- Gateway API: GatewayClass, Gateway and HTTPRoute with a canary split
- PVC, StorageClass and CSI: how a cloud disk ends up in your Pod
- RBAC: how a ServiceAccount gets its permissions
- cert-manager with Let's Encrypt: Certificate, CertificateRequest, Order and Challenge
- A private CA with cert-manager and trust-manager
- Istio traffic management: Gateway, VirtualService, DestinationRule and policies
- Flux dependencies: Git and OCI sources, Kustomizations and HelmReleases
- Argo CD: ApplicationSet, AppProject and the Applications it generates
- Inside an OCI image: tag, index, manifests, config and layers
- OCI artifacts: Helm charts, Flux artifacts and image volumes
- A Helm release: chart, values, revisions and the objects Helm owns
- Kustomize: a base, two overlays, a component and generated ConfigMaps
- Autoscaling end to end: KEDA, the HPA it creates and Karpenter adding nodes
- Prometheus Operator: ServiceMonitor, PrometheusRule and the path to an alert
- External Secrets Operator: from a cloud secret manager to a Pod, with rotation
- Kyverno CEL policies: ValidatingPolicy, MutatingPolicy, GeneratingPolicy and exceptions
- Cilium egress to a domain: CiliumNetworkPolicy, the DNS proxy and toFQDNs
- Istio ambient mode: ztunnel, waypoint proxies and authorization without sidecars
- Argo Rollouts canary: traffic weights on an HTTPRoute and analysis from Prometheus