Kubernetes · Advanced

Kubernetes Cluster Operations

Pod Security rolled out with warn before enforce, the removed API that takes workloads with it on upgrade, the difference between an etcd snapshot and a real backup, and the three cluster failures that are silent until they are outages.

28 min read Level: advanced Kubernetes 05 / 05
The model

THE LIFECYCLE OF A CLUSTER YOU OPERATE

Harden, upgrade, drain, back up, spread — and prove the restore works before you need it to.

HARDENPod Security AdmissionNetworkPolicyRBAC least privilegeEncryption at restVERSIONcontrol plane firstkubelet within skewdeprecated API checkDRAINcordon → evict → rebootPDB gates every evictionBACK UPetcd snapshot (cluster state)Velero (namespaces + PV data)SPREADmulti-cluster: fleet, not one big clusterPROVErestore rehearsal = your real RTO

PodDisruptionBudget appears in the drain row for a reason: the same object that protects availability is what stops an upgrade and what pins a node against scale-down.

Diagrams

THREE VIEWS OF THE SAME SYSTEM

The diagram above is the high level: what the pieces are. These two are the ones you want when something is wrong — what is inside one of those boxes, and the path a request really takes through them.

Low levelWhat is inside etcd, and what makes it slow?
ETCD MEMBERapiserver writespeer replicationcommitted revisionwatch streamCONSENSUSRaft logappend-only, replicatedleader electionelection timeout 1000msquorum = N/2 + 12 of 3; lose 2 and it is read-onlyDURABILITYWAL fsyncdisk latency IS etcd latencysnapshotevery 100k revisionsbackend .db (bbolt)HOUSEKEEPINGcompactiondrops old revisionsdefragreturns free pages to diskquota 2 GB defaultexceeded → NOSPACE alarm
Every write is an fsync on a quorum of members, so etcd's p99 is your disk's p99 — this is the one component where slow storage becomes a cluster-wide outage rather than a slow application.
ConnectionWhat breaks during an upgrade, and in what order?
read the releasenotesetcd snapshotthe only real rollbackcontrol plane firstn+1 minor maxremoved APIs biteHEREworkloads 404cordon + drain a nodePDB gates evictionkubelet upgradedrejoinsuncordon, next noderepeat
The control plane goes first and must never be more than one minor ahead of the kubelets. The hop that actually hurts is the removed API: your manifests stop applying at the moment the new apiserver comes up.
Core

SECURITY, UPGRADES, BACKUP AND FLEET

The four operational concerns that are nobody's feature work until the day they are the incident.

Cluster hardening is a long subject with a short high-value core. Four controls, each one label or object, each closing a category of problem:

  • Pod Security Admission. Replaced PodSecurityPolicy in v1.25. Three levels — privileged, baseline, restricted — applied as a namespace label. restricted blocks running as root, privilege escalation, host namespaces and most capabilities.
  • NetworkPolicy. Without one, every pod reaches every pod in every namespace. Default-deny per namespace, then allow what is needed.
  • RBAC least privilege. Covered in Config & Access — the short version is that create pods and get secrets are both effectively privileged.
  • Encryption at rest. Secrets sit in etcd in the clear by default.

Use warn before enforce

PSA can warn and audit without blocking. Labelling a namespace warn=restricted first tells you exactly which workloads would break, from real traffic, before anything is refused. Going straight to enforce on a live namespace is how you find out during an incident.

rolling out Pod Security without breaking production
1. warn only — nothing is blocked, violations are reported
$ kubectl label ns prod pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/audit=restricted
2. redeploy something and read the warnings it prints
$ kubectl rollout restart deploy -n prod
Warning: would violate "restricted": allowPrivilegeEscalation != false,
unrestricted capabilities, runAsNonRoot != true, seccompProfile
3. fix the workloads, THEN enforce
$ kubectl label ns prod pod-security.kubernetes.io/enforce=restricted --overwrite
what is enforced across the fleet right now:
$ kubectl get ns -o custom-columns=\
NAME:.metadata.name,ENFORCE:.metadata.labels.pod-security\\.kubernetes\\.io/enforce

Two rules govern every Kubernetes upgrade:

  • Control plane first, then nodes. kubelet may be up to three minor versions behind the API server, never ahead.
  • One minor version at a time. 1.28 → 1.30 is two upgrades, not one.

The thing that actually breaks workloads is API removal. A deprecated API is removed on a schedule, and a manifest or controller still calling it starts failing the moment the control plane moves. The cluster knows who is calling what, which turns this from an audit into a query.

The pre-upgrade sequence

Check for removed APIs, take an etcd snapshot, confirm PDBs will not deadlock the drain, upgrade the control plane, then the nodes one pool at a time.

finding what will break before it breaks
1. what is still calling a deprecated API, and who
$ kubectl get --raw /metrics | grep apiserver_requested_deprecated_apis
$ kubectl get apirequestcount -o json | jq -r '.items[]
| select(.status.removedInRelease != null)
| "\(.metadata.name) removed in \(.status.removedInRelease)"'
2. snapshot etcd BEFORE touching anything
$ ETCDCTL_API=3 etcdctl snapshot save /backup/etcd-$(date +%F).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
$ ETCDCTL_API=3 etcdctl snapshot status /backup/etcd-$(date +%F).db -w table
3. will the drain deadlock? any PDB with 0 allowed is a stop sign
$ kubectl get pdb -A -o json | jq -r '.items[]
| select(.status.disruptionsAllowed==0)
| "\(.metadata.namespace)/\(.metadata.name)"'
4. then, per node
$ kubectl drain worker-04 --ignore-daemonsets --delete-emptydir-data --timeout=10m
etcd snapshotVelero
ContainsEvery API objectSelected namespaces + PV contents
RestoresThe whole cluster to that instantNamespaces, into this or another cluster
GranularityAll or nothingPer namespace, per label
PV dataNoYes — snapshots or file-level copy
Right forLost quorum, corrupted control planeDeleted namespace, migration, real DR

Conflating the two is how a DR test fails. "We back up etcd nightly" does not protect against someone deleting the prod namespace with Delete-policy PVCs — the objects come back from a cluster-wide restore, the volume contents do not, because they were never in etcd.

Your RTO is a measured number or it is fiction

An etcd restore stops the control plane, restores on one member and rebuilds the others. It is documented, disruptive, and takes as long as it takes. The only way to know that number is to do it once on a cluster you can afford to break — and the first rehearsal always takes longer than anyone predicted.

both, and the check that they actually ran
$ velero backup create prod-$(date +%F) --include-namespaces prod \
--snapshot-volumes --ttl 720h
$ velero backup describe prod-$(date +%F) --details
$ velero backup get
NAME STATUS ERRORS WARNINGS CREATED
prod-2026-09-15 Completed 0 0 2h ago
restore ONE namespace, without touching the rest of the cluster:
$ velero restore create --from-backup prod-2026-09-15 --include-namespaces prod
a backup that has never been restored is a hypothesis. Prove it somewhere safe:
$ velero restore create drill --from-backup prod-2026-09-15 \
--namespace-mappings prod:prod-restore-drill
The reclaim policy decides whether the data survives at all

On a Delete-policy StorageClass, removing a PVC destroys the backing volume immediately — including when the PVC goes as a side effect of deleting a namespace or a Helm release. Retain turns irreversible loss into orphaned volumes you clean up deliberately, which is a far better problem.

Past a certain size the question stops being "how big can this cluster get" and becomes "how many clusters, split how". The honest reasons to run more than one:

  • Blast radius. A cluster is a failure domain. A bad CRD, a broken webhook or a control-plane upgrade takes out everything in it.
  • Region. A cluster does not span regions well — etcd needs low latency between members.
  • Hard tenancy. Namespaces are not a security boundary; separate clusters are.
  • Upgrade staging. Somewhere real to land a new version first.

And the costs, which are consistently underestimated: every platform component installed N times, N sets of credentials and policy to keep consistent, cross-cluster service discovery to solve, and a fleet-management story (Argo CD ApplicationSets, Cluster API, Fleet) that is now itself production infrastructure.

The DR question that actually matters

Not "do we have a second cluster" but "what is our RPO and RTO, and have we measured them". Active-passive with Velero restore is hours. Active-active with replicated data is minutes, and a much larger standing bill. Both are defensible; only one is usually what people have while believing they have the other.

the fleet-wide checks worth having
version drift across the fleet
$ for c in $(kubectl config get-contexts -o name); do
printf '%-28s %s\n' "$c" "$(kubectl --context=$c version -o json \
| jq -r .serverVersion.gitVersion)"; done
does every cluster have a backup that completed recently?
$ for c in $(kubectl config get-contexts -o name); do
echo "== $c"; kubectl --context=$c get backup -n velero \
--sort-by=.metadata.creationTimestamp | tail -2; done
and the one that catches real drift — what is NOT in git
$ kubectl --context=$c diff -f manifests/ || true
In practice

ADVANCED TROUBLESHOOTING

Cluster-level failures differ from workload failures in one way that matters: they are rarely loud. A certificate that expires in 30 days, a backup that has silently failed for a month, an etcd database approaching its quota — each is invisible until it is an outage.

SymptomCauseCheck
API server suddenly refuses everythingCertificate expiredkubeadm certs check-expiration
Writes fail, reads worketcd lost quorum, or hit its DB quotaetcdctl endpoint status -w table
Everything slow, nothing downetcd fsync latency / leader electionsetcd_disk_wal_fsync_duration_seconds p99 > 10 ms
Upgrade drain hangs on one nodeA PDB that allows zero disruptionskubectl get pdb -A
Workloads vanish after upgradeA removed API their manifests usedkubectl get apirequestcount — before, not after
Restore produces empty volumesetcd snapshot, not an application backupVelero with --snapshot-volumes
Node NotReady, kubelet fineCNI or the container runtimejournalctl -u kubelet -u containerd on the node

The three that should be alerts, not discoveries

certificate expiry, etcd size, backup age
1. certificates — silent until the day everything stops
$ kubeadm certs check-expiration
CERTIFICATE EXPIRES RESIDUAL TIME
apiserver Nov 02, 2026 09:14 UTC 48d
etcd-server Nov 02, 2026 09:14 UTC 48d
2. etcd DB size against its quota (default 8 GB)
$ kubectl -n kube-system exec etcd-master-0 -- etcdctl endpoint status -w table
growing steadily? compact and defrag, do not just raise the quota:
$ etcdctl compact $(etcdctl endpoint status -w json | jq -r '.[0].Status.header.revision')
$ etcdctl defrag --cluster
3. backup age — a job that has failed quietly for a month looks like nothing
$ velero backup get --output json | jq -r '.items[]
| "\(.metadata.name)\t\(.status.phase)\t\(.status.completionTimestamp)"' | tail -5
Alert on the absence, not the error

Each of these three fails by not happening: the backup does not run, the certificate does not renew, the compaction does not occur. None produces an error to alert on. The alert has to be on the age of the last success — which is a different kind of rule, and the one most clusters are missing.

Reference

CHEATSHEET

CommandWhat it answers
kubeadm certs check-expirationThe silent outage 30 days out
etcdctl endpoint status -w tableQuorum, leader and DB size
kubectl get apirequestcountWho still calls an API about to be removed
kubectl get pdb -AWhat will deadlock the next drain
kubectl drain <n> --ignore-daemonsets --delete-emptydir-dataEvacuate a node properly
kubectl get ns -o custom-columns=...enforceWhich namespaces enforce Pod Security
kubectl get networkpolicy -AWhich namespaces are actually isolated
velero backup getWhether the backup ran, and when it last succeeded
velero restore create --from-backup <b>Bring a namespace back
kubectl version -o json | jq .serverVersionWhere this cluster sits in the skew
kubectl get nodes -o widekubelet and runtime versions per node
kubectl diff -f manifests/Drift between git and the cluster