etcd — the cluster store
The single source of truth, and why it is guarded.
Every object in your cluster lives in exactly one place, and it isn't on the worker machines that run your apps. It sits in a single database on the control plane (the cluster's management brain) called etcd (say it 'et-cee-dee'). etcd is a key-value store, which is the same shape as a giant dictionary that maps a name to a blob of data. Think of a warehouse's master inventory ledger. The shelves don't decide what's in stock. The ledger does, and staff keep restocking until the shelves match what's written down. Kubernetes runs the same loop. You write what you want into etcd, and controllers keep nudging the real cluster until it matches.
One rule shapes everything else. Only the API server (the front door that every kubectl command and every controller talks to) is allowed to read or write etcd. Your Pods (the smallest deployable unit, one or more containers that run together) never touch it. The kubelet (the agent on each node that actually starts your containers) never touches it either. They all go through the API server, which is the single writer standing in front of the store. That's on purpose. One doorway gives you a single place to check who's calling and confirm an object is valid before it's ever stored. It's also the one place to log what happened. And etcd only ever has to trust one client instead of thousands.
How a write actually lands
Say you run three copies of etcd for safety. When you create a Deployment (a request to run some copies of your app), that change can't just be scribbled into one copy and called done, because that copy could die a second later and take the only record with it. Think of a small committee that refuses to act until more than half its members have signed off. etcd works the same way, through a voting protocol called Raft. One member is elected leader, the write goes to the leader, the leader ships it to the followers, and the change counts as committed only once a majority have written it to their own disk. Majority is the word to hold onto. With three members, two is a majority, so the cluster keeps serving even if one dies.
This is why you run an odd number of members. Three tolerates one failure. Five tolerates two. Four sounds safer than three but isn't: a majority of four is still three, so you survive only one failure, and now you have an extra machine that can break. There's a nastier case too. If a network split drops two members on each side of a four-node cluster, neither side holds a majority, and the whole store goes read-only until the split heals. So the answer is almost always three, occasionally five for bigger or more critical clusters. Going past five mostly just slows writes down, since every commit now waits on more disks.
Reading the store, and why Secrets scare people
Everything etcd holds is filed under a key that starts with /registry. A Pod lands at /registry/pods/<namespace>/<name>, a Secret at /registry/secrets/<namespace>/<name>, and the value is the object serialized into a compact binary format (protobuf). If you're on a control-plane node with the etcd certificates, you can read a key straight from the store. Here's the part that catches people out. A Secret's value sits in etcd as good as plaintext. The base64 you see in kubectl get secret -o yaml is just an encoding for moving bytes around, not protection. Anyone who can read etcd's data files, or an old snapshot, can read every password in the cluster.
sudo ETCDCTL_API=3 etcdctl get /registry/secrets/default/db-cred \--endpoints=https://127.0.0.1:2379 \--cacert=/etc/kubernetes/pki/etcd/ca.crt \--cert=/etc/kubernetes/pki/etcd/server.crt \--key=/etc/kubernetes/pki/etcd/server.key | strings
/registry/secrets/default/db-credk8sv1Secretdb-creddefault"*$9d1c7f0e-...zOpaquepasswordS3cr3t-P@ssw0rd
The fix is encryption at rest. You give the API server an EncryptionConfiguration that tells it to encrypt certain resource types (Secrets first) before they ever reach etcd, ideally with keys held in a KMS (Key Management Service, an external vault the API server calls so the real key never lives on the node). After that, the same etcdctl get returns ciphertext instead of your password. Lock the network path down as well: mutual TLS between the API server and etcd (both ends prove who they are with certificates), and a firewall so nothing but the API server can reach port 2379.
A snapshot is the whole cluster
Because every bit of state lives here, one etcd snapshot is a complete point-in-time backup of the control plane. Not just your Deployments. All of it, down to the last RBAC (Role-Based Access Control) rule and Secret. Before you back anything up, check the store is healthy and that you're pointed at the right endpoint.
sudo ETCDCTL_API=3 etcdctl endpoint status --write-out=table \--endpoints=https://127.0.0.1:2379 \--cacert=/etc/kubernetes/pki/etcd/ca.crt \--cert=/etc/kubernetes/pki/etcd/server.crt \--key=/etc/kubernetes/pki/etcd/server.key
+------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+| ENDPOINT | ID | VERSION | DB SIZE | IS LEADER | IS LEARNER | RAFT TERM | RAFT INDEX | RAFT APPLIED INDEX | ERRORS |+------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+| https://127.0.0.1:2379 | 8e9e05c52164694d | 3.5.15 | 31 MB | true | false | 4 | 158204 | 158204 | |+------------------------+------------------+---------+---------+-----------+------------+-----------+------------+--------------------+--------+
One leader, a healthy database size, an empty errors column. Now take the snapshot itself. The command streams the live database out to a single file you can copy anywhere.
sudo ETCDCTL_API=3 etcdctl snapshot save /var/backups/etcd-2026-07-16.db \--endpoints=https://127.0.0.1:2379 \--cacert=/etc/kubernetes/pki/etcd/ca.crt \--cert=/etc/kubernetes/pki/etcd/server.crt \--key=/etc/kubernetes/pki/etcd/server.key
{"level":"info","ts":"2026-07-16T09:12:03.441Z","caller":"snapshot/v3_snapshot.go:65","msg":"created temporary db file","path":"/var/backups/etcd-2026-07-16.db.part"}{"level":"info","ts":"2026-07-16T09:12:03.658Z","caller":"snapshot/v3_snapshot.go:73","msg":"fetching snapshot","endpoint":"https://127.0.0.1:2379"}{"level":"info","ts":"2026-07-16T09:12:03.871Z","caller":"snapshot/v3_snapshot.go:88","msg":"fetched snapshot","total-bytes":32505856,"took":"0.213s"}Snapshot saved at /var/backups/etcd-2026-07-16.db
Restoring is a disaster-recovery move, and it happens offline. You stop the API server and etcd, restore the snapshot into a fresh data directory with etcdutl snapshot restore (current etcd moved restore out of etcdctl into the etcdutl helper), point etcd's static Pod manifest at that new directory, and start everything back up. Rehearse it on a throwaway cluster before you ever need it for real, because a backup you have never restored is a wish, not a plan. And store snapshots off the cluster and encrypted, since each one carries every Secret you own.
When etcd runs out of room
etcd keeps a history. Every change bumps a global revision number, and old versions stick around, which is what makes watches and rollbacks work. That history is already being pruned for you: the API server compacts it every five minutes out of the box. What compaction never does is shrink the file. The pages it frees stay inside the database to be reused, so heavy churn (a flood of Events is the usual culprit), a few very large objects, or plain fragmentation can still walk the file up to its size limit (the default backend quota is about 2 GiB). At that point etcd raises a NOSPACE alarm and flips the whole store to read-only. Your cluster stops accepting changes, and kubectl apply comes back with 'etcdserver: mvcc: database space exceeded', even though every node still looks perfectly fine. Clearing it takes two steps. First, compact by hand so no old revision is still holding pages. Then defragment, because that is what hands the freed room back to the filesystem.
export ETCDCTL_API=3E="--endpoints=https://127.0.0.1:2379 --cacert=/etc/kubernetes/pki/etcd/ca.crt --cert=/etc/kubernetes/pki/etcd/server.crt --key=/etc/kubernetes/pki/etcd/server.key"etcdctl alarm list $Erev=$(etcdctl endpoint status --write-out=json $E | grep -o '"revision":[0-9]*' | head -1 | cut -d: -f2)etcdctl compact "$rev" $Eetcdctl defrag $Eetcdctl alarm disarm $E
memberID:10276657743932975437 alarm:NOSPACEcompacted revision 158204Finished defragmenting etcd member[https://127.0.0.1:2379]
Quorum matters more than raw disk speed. A majority of members must agree or the cluster refuses writes. Two of three dead is a read-only disaster waiting for the next change.
Encryption at rest for Secrets is configured at the API server, but the ciphertext still lives in etcd. Snapshot files are as sensitive as the live database.
Restore is offline work: stop the static pods, restore to a fresh data dir, repoint the manifest, bring the API back. Practice it before you need it.
Try this
If you have a kubeadm control-plane box, list a few keys under /registry with etcdctl using the local certs, then ask the store how it is doing. See that Secrets and Deployments are just keys in one place. Everything below is a read, and on a box you care about it should stay that way: compaction, defrag and snapshot save are maintenance work, and defrag blocks the member while it runs, so on a single-member control plane the API server stalls until it finishes.
$ sudo ETCDCTL_API=3 etcdctl get /registry/secrets/default/db-cred \--endpoints=https://127.0.0.1:2379 \--cacert=/etc/kubernetes/pki/etcd/ca.crt \--cert=/etc/kubernetes/pki/etcd/server.crt \--key=/etc/kubernetes/pki/etcd/server.key | strings$ sudo ETCDCTL_API=3 etcdctl get /registry/deployments/ --prefix --keys-only \--endpoints=https://127.0.0.1:2379 \--cacert=/etc/kubernetes/pki/etcd/ca.crt \--cert=/etc/kubernetes/pki/etcd/server.crt \--key=/etc/kubernetes/pki/etcd/server.key$ sudo ETCDCTL_API=3 etcdctl endpoint status --write-out=table \--endpoints=https://127.0.0.1:2379 \--cacert=/etc/kubernetes/pki/etcd/ca.crt \--cert=/etc/kubernetes/pki/etcd/server.crt \--key=/etc/kubernetes/pki/etcd/server.key$ sudo ETCDCTL_API=3 etcdctl alarm list \--endpoints=https://127.0.0.1:2379 \--cacert=/etc/kubernetes/pki/etcd/ca.crt \--cert=/etc/kubernetes/pki/etcd/server.crt \--key=/etc/kubernetes/pki/etcd/server.key
Takeaway
etcd is the source of truth. Back it up, protect its certs, and never treat a casual etcdctl write as a substitute for the API.
kubectl get secret -o yaml shows the value as base64. Why is that wrong, and what actually protects a Secret at rest?kubectl apply fails with etcdserver: mvcc: database space exceeded and no new objects can be created, yet every node reports Ready. etcdctl alarm list returns a NOSPACE alarm. What is the correct sequence to recover?