Backup & DRXIII · Container and Kubernetes RecoveryKubernetes
The Kubernetes backup model: four separate things
What you'll learn
- Separate a Kubernetes estate into desired state, cluster state, persistent data and external dependencies
- State what an etcd snapshot protects and what it demonstrably does not
- Explain why re-applying a complete GitOps repository can restore every object and no data
- Assign every item in a cluster estate to the mechanism that actually covers it
Prerequisites
Practice
Verified against restic 0.19.1 · BorgBackup 1.4.5 · rclone 1.75.0 · MinIO (S3-compatible object storage) RELEASE.2025-09-07T16-13-09Z · OpenZFS 2.4.1 · LVM2 2.03.31(2) · btrfs-progs 6.17.1 · PostgreSQL 18.6 · pgBackRest 2.59.1 · Kubernetes (k3s) and etcd k3s v1.36.3+k3s1, etcd 3.7.1 · Velero 1.18.2 · Docker Engine 29.7.2 · Proxmox Backup Server (documentation only) 4.0.10-1 · Ubuntu (host baseline) 26.04 LTS · 2026-08-28
Backing up container data consistently settled what to copy and when to hold the writer still, and it settled it one volume at a time. A Kubernetes estate does not fail one volume at a time. Ask a team where their Kubernetes backup is and you will usually be shown one artefact — a Git repository, an etcd snapshot, a nightly volume job, a backup operator — and told that this is it. Each of those protects something real. None of them protects what the other three protect, and the distance between them stays invisible until a namespace disappears.
flowchart TB
Service["Recovered Kubernetes service"]
Git["Desired state: manifests, charts, values"] -->|"Git mirror or export"| Service
Etcd["Cluster state: API objects and bindings"] -->|"etcd snapshot"| Service
PV["Persistent application data"] -->|"application backup or independent volume copy"| Service
External["External dependencies: registry, DNS, PKI, secrets"] -->|"their own recovery procedures"| Service
The service returns only where all four arrows meet. Reapplying Git can recreate an empty claim, an etcd restore can recreate a binding to missing storage, and a volume restore cannot recreate the identity or DNS path that makes it usable.
Four artefacts, four mechanisms, four ways to be uncovered
Nothing in a Kubernetes cluster deserves to be called the backup, because there are four separate things to protect, each held somewhere else, each protected by a different mechanism, usually on a different schedule and often by a different team.
The desired state is the material a human wrote: manifests, Helm values, Kustomize overlays, the chart versions pinned alongside them. It lives in a Git repository and it is protected by protecting that repository — an independent mirror, a clone that survives the loss of the hosting account, an export of the issues and pipeline definitions that surround it. What matters for recovery is less how it is protected than what it contains. The repository is a complete description of what should exist. It contains nothing whatsoever of what the running system produced.
The cluster state is the set of API objects as etcd holds them at a moment in time, and it is a strict superset of the desired state. The difference is the interesting part. A cluster accumulates objects nobody committed: a Secret created by hand at three in the morning and never written down, a ServiceAccount and its token, a PersistentVolume object minted by a provisioner, a binding between a claim and a volume, objects a controller or an admission webhook wrote on the way past, and the Lease and Endpoint churn that never belonged in a repository at all. Cluster state is protected by an etcd snapshot, and by nothing else on this list.
The persistent application data is the bytes inside the PersistentVolumes: the rows, the uploads, the queue, the ledger. This is the only one of the four that the business would describe as its data, and it is the only one that neither Git nor etcd holds even a copy of. It is protected by a volume-level backup — a storage or CSI snapshot copied somewhere else — or by an application-level backup taken from inside the workload, which is what the previous lesson was about.
The external dependencies are everything the cluster refers to but does not contain. Credentials whose source of truth is a secret manager outside the cluster. TLS certificates and the issuer that mints them. DNS records that point at a load balancer. Cloud resources created by a controller and owned by the cloud account rather than the cluster. The container images the manifests name, sitting in a registry that is a separate system with its own retention policy. A cluster can be rebuilt perfectly and still not start, because the image tag its manifests reference is no longer in the registry.
The design rule that follows is unglamorous: for every item in the estate, name which of the four covers it. Do it item by item rather than service by service, because a single service usually spans three of the four categories and is covered in two of them. And expect the exercise to find at least one item that nothing covers, because the categories are protected by different teams and nobody owns the seam between them.
The manifests came back; orders.csv did not
The claim that the desired state is not the data is easy to assert and easy to believe without ever having felt it, so it is worth executing. A single-node k3s cluster ran a namespace containing a Namespace, a PersistentVolumeClaim, a ConfigMap and a Pod that wrote two order lines to a file on the volume. The YAML was the whole of what a GitOps repository would have held. The order lines were not in it and could not have been.
$ kubectl delete namespace rbdr-shopnamespace "rbdr-shop" deleted
namespace fully removed after 0s
--- is the PersistentVolume data still on disk? ---
GONE - the local-path provisioner reclaimed the volume with the PVCDeleting the namespace deleted the claim, and deleting the claim released the volume to the provisioner, which reclaimed it. The bytes were gone from the node before anyone had decided that a recovery was needed. Recovery attempt one is the move every team reaches for first, and the capture labels it in exactly the terms teams use: this is what people mean when they say everything is in Git.
$ kubectl apply -f rbdr-shop.yamlnamespace/rbdr-shop created
persistentvolumeclaim/rbdr-orders created
configmap/rbdr-config created
pod/rbdr-orders-writer created
>>> exit code: 0
pod Ready after 6s
--- every object is back ---
persistentvolumeclaim/rbdr-orders Bound pvc-a29538af-09d4-4bb8-82bb-b2c222b9db93 64Mi RWO local-path <unset> 6s
configmap/kube-root-ca.crt 1 6s
configmap/rbdr-config 1 6s
pod/rbdr-orders-writer 1/1 Running 0 6s
--- and the business data? ---
total 8
drwxrwxrwx 2 root root 4096 Aug 28 14:34 .
drwxr-xr-x 1 root root 4096 Aug 28 14:34 ..
cat: can't open '/data/orders.csv': No such file or directory
command terminated with exit code 1Read the two halves of that output against each other, because the whole lesson
is in the contrast. Every object came back. The apply exited 0, the Pod reached
Ready after 6s, the claim reached Bound, the ConfigMap was present, the
container was 1/1 Running. A dashboard watching object health would have gone
green, and a reconciliation controller would have reported the namespace as
synced. Then ls on the mounted directory returned two entries — . and ..
— and reading the file returned cat: can't open '/data/orders.csv': No such file or directory with exit code 1.
There is a second detail worth pausing on. Before the deletion, the claim was
bound to pvc-6edd5db0-25f4-4b33-b26b-e2ad78aac9cf; after it, to
pvc-a29538af-09d4-4bb8-82bb-b2c222b9db93. The object name rbdr-orders
survived the round trip unchanged because it was written in the manifest. The
volume identity did not, because it was never in the manifest to survive. Any
restore procedure that hard-codes the volume path it saw yesterday is writing
into a directory that the recreated claim is not using.
What the 1,208,352-byte snapshot is a backup of
Before the deletion the capture took a cluster-state snapshot, which is the second of the four mechanisms and the one most often confused with the third.
$ k3s etcd-snapshot savetime="2026-08-28T14:33:54Z" level=info msg="Snapshot rbdr-before-125e3e56d5eb-1787927634 saved."
>>> exit code: 0
snapshot file: rbdr-before-125e3e56d5eb-1787927634
drwx------ 4 root root 4096 Aug 28 14:32 ..
-rw------- 1 root root 1208352 Aug 28 14:33 rbdr-before-125e3e56d5eb-1787927634One file, 1,208,352 bytes, holding the Namespace, the claim and its binding, the ConfigMap and the Pod spec — and holding them as the API server had them, which includes whatever had been created without passing through the repository. The capture states the boundary in its own words: the data actually protected by that file is Kubernetes objects only, and the data not protected by that file is every byte in every PersistentVolume. An etcd snapshot is a backup of the cluster’s mind and not of its contents.
The file also needs an owner, and the mechanism carries a seam that is easy to
inherit without noticing. The k3s CLI documentation separates two kinds of
snapshot on this datastore: the scheduled ones the server takes for itself,
which are pruned down to a retention count, and the on-demand ones produced by
k3s etcd-snapshot save, which that documentation states have no retention
applied and stay in place until somebody removes them. The asymmetry cuts both
ways. A snapshot taken deliberately before a risky change is exactly the
artefact a recovery wants to find, and it is also the artefact that accumulates
unwatched on a control-plane node. More important for this course is where the
file landed: on the node whose datastore it describes. A copy that lives only
there shares the failure domain it was taken to survive, so a cluster-state
snapshot becomes a recovery mechanism only once something moves it somewhere
the cluster cannot reach.
The objects-only boundary has a second direction, and it is the one that gets argued about. In an estate reconciled continuously from Git, the desired state is already covered, so an etcd snapshot can look redundant. It is not, because it is the only artefact that holds the objects nobody committed. If the answer to “what is in this cluster that is not in the repository” is “nothing”, that is a claim worth checking rather than assuming, and the checking is usually where the ad-hoc Secret and the hand-edited object turn up.
The dependencies that are not in the cluster at all
The fourth category is the one that gets discovered during the rebuild, because it is the only one with no artefact in the cluster to remind anybody it exists.
Start with credentials. A workload that fetches its database password from an external secret manager has, in the cluster, only a reference: a custom resource, a mounted projection, an annotation. Recreate the objects and the reference comes back pointing at a system that may not have been part of the recovery plan, holding a version of the credential that may no longer match what the restored database expects. The same holds for certificates: the Certificate object is desired state, the issued key material is cluster state, and the issuing authority is neither — it is a separate service with its own recovery story, and rebuilding a cluster whose issuer is unavailable produces a namespace full of workloads that will not accept a connection.
Then the resources the cluster asked the world to create on its behalf. A Service of type LoadBalancer produced an address in a cloud account. A DNS record points at that address, and the record lives in a zone the cluster does not own. An ingress controller obtained a certificate tied to that name. Every one of those is reconstructable, and none of them is reconstructed by re-applying a manifest — the manifest asks for a load balancer, gets a new one, and the DNS record still points at the old address until somebody changes it.
Images are the quietest failure of the four. Manifests reference images by tag or digest, and the registry that holds them is a separate system with its own retention, its own storage and its own credentials. A cluster rebuilt from a perfect repository into a working control plane will produce Pods that cannot start if the referenced image is no longer pullable, and the recovery then waits on rebuilding an image from a source tree that has moved on since the tag was cut. The registry belongs on the same inventory as the volumes.
Both, or neither
The capture finishes by doing the thing that actually works, which is using two
of the four mechanisms together. The first disaster had already destroyed the
original file, so the capture had to recreate the two order lines before it
could demonstrate backing them up; the archive it then wrote from the node
directory backing the second claim was 2560 bytes. The namespace and its volume
were destroyed a second time, the manifests recreated the objects into a third
directory — pvc-ef5cf541-eebf-4eb3-9c91-cb4e59d6d787_rbdr-shop_rbdr-orders,
sharing no identifier with either of the first two — and the archive was
unpacked into that new directory.
$ tar xf /tmp/rbdr-pv-backup.tar -C $NEWDIRORDER-1001,4500.00
ORDER-1002,1250.00
recovered md5 : 9eb4e2ad8e08e1dcaaf87ababab964b0
original md5 : 9eb4e2ad8e08e1dcaaf87ababab964b0
RECOVERED - the application data is back, byte-identicalBoth md5 sums read 9eb4e2ad8e08e1dcaaf87ababab964b0, which is the value the
capture recorded when the application first wrote the file and which the
recreated copy reproduced exactly, so the comparison is a real one. The capture
draws the conclusion itself, and it is worth quoting rather than paraphrasing:
full recovery needed both the desired state to rebuild the objects and a
separate data backup to refill the volume, and neither alone was enough.
Notice what the successful recovery required that the failed one did not. It needed a copy of the bytes taken before the loss, and it needed the restore to target the directory backing the new claim rather than the old one. The second requirement is a procedural one and it is where hand-written runbooks tend to break, because the path that was correct when the runbook was written is a path that no longer exists.
Production discipline
- Classify before you buy. Decide which of the four categories a proposed tool covers before evaluating it. A tool that captures API objects and a tool that captures volume contents are answering different questions, and a product that does both still does them as two operations with two failure modes.
- Keep an etcd snapshot even under GitOps. The 1,208,352-byte file in the capture held the objects as the API server had them, including anything created outside the repository. Reconciliation cannot restore an object that was never committed.
- Never let an object-level restore stand in for a data restore. In the
capture the apply exited 0, the Pod reached Ready after 6s and the claim
reached
Bound, and the file the business cared about returnedcat: can't open '/data/orders.csv': No such file or directory. - Resolve volume paths at restore time. The claim came back as
rbdr-ordersand bound topvc-a29538af-09d4-4bb8-82bb-b2c222b9db93, a different volume from thepvc-6edd5db0-25f4-4b33-b26b-e2ad78aac9cfit had before, and the capture went through a third identifier before it finished. A procedure that hard-codes yesterday’s path restores into a directory nothing is reading. - Inventory the fourth category by name. List the registry, the DNS zone, the certificate issuer, the external secret store and the cloud resources as individual items with owners. They are the part of the estate that no cluster-side backup mechanism can see, and therefore the part that no cluster-side gap analysis will report.
Cross-course references
- Kubernetes for Production Sysadmins — Part LXVIII (etcd Backup) and Part XCVI (Workload Backup) build the two mechanisms this lesson only classifies, covering how a snapshot is taken and verified and what a workload-level backup captures; use this lesson to decide which of the four categories an item needs, and those parts to operate the mechanism you chose.
- Git, CI/CD & GitOps for Infrastructure Engineers — Part LXXII (GitOps Foundations) and Part LXXV (Drift) explain the reconciliation loop that made the re-apply above succeed so completely, which is exactly why it is dangerous as a recovery story: reconciliation converges objects to the committed state and has no opinion at all about the bytes in a volume.
- Secrets, PKI & Certificate Management for Infrastructure Engineers — Part XII (Secret Management Platforms) covers the systems that hold the credentials a cluster only references, which is the fourth category in this lesson; a cluster rebuilt without its secret platform and issuer restores every object and still cannot authenticate anything.
Quiz
Knowledge check · 5 questions
Q1. A namespace was deleted and the same manifests were re-applied. The apply exited 0, the Pod reached Ready after 6s and the PersistentVolumeClaim reached Bound. What has the recovery established?
Q2. An estate is reconciled continuously from Git, so every manifest in the cluster is committed. What does an etcd snapshot still protect that the repository does not?
Q3. The namespace was deleted and the provisioner reclaimed the volume with the claim. Which of these were needed to bring the two order lines back? Select all that apply.
Q4. Re-applying the manifests recreated a PersistentVolumeClaim under the same name, and that claim bound to a volume with a different identifier than the one it had before.
Q5. A team has an etcd snapshot every thirty minutes, a Git repository reconciled continuously, and nightly volume snapshots. Name the category this still leaves uncovered and give two concrete examples from a real estate.
Passing score: 75%. Answers are checked in this browser.