KubernetesVII · Declarative Resource ManagementDeclarative resource management
Drift detection and remediation — keeping live and manifest in sync
What you'll learn
- Define drift and identify its sources
- Detect drift with kubectl diff, GitOps controllers, and audit tools
- Remediate drift through reconcile loops and controlled reverts
- Alert on sustained drift to detect unauthorised changes
Prerequisites
Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16
Drift is the gap between what you declared and what the cluster
has. In a well-managed cluster, the gap is zero: every field
on every object matches a manifest in source control, and
controllers fill in the fields that operators did not declare.
In a poorly-managed cluster, the gap grows: ad-hoc kubectl edits, controllers that add unexpected fields, manual
scaling, and forgotten experiments all accumulate. This lesson
covers how to detect drift, how to remediate it, and how to
alert on sustained divergence.
What drift looks like
Drift is any of:
- A field declared in the manifest but different in the live state.
- A field declared in the manifest but missing in the live state.
- A field present in the live state but declared in the manifest as null/removed.
- A field present in the live state but not declared in the manifest (this is the most insidious kind, because client-side apply preserves it).
flowchart LR
M[Manifest in Git] -->|declared| D[Detected drift]
L[Live state] -->|observed| D
D -->|whichever differs| F[Flag the field]
The four drift categories:
| Category | Manifest | Live | Action |
|---|---|---|---|
| Drift type 1 | A: x | A: y | Update manifest or revert live |
| Drift type 2 | A: x | A: missing | Re-apply manifest |
| Drift type 3 | A: missing | A: y | Decide: re-apply or accept |
| Drift type 4 | A: missing | A: y, owned by another manager | SSA conflict — re-apply carefully |
Type 3 is the most common in practice. A controller added a field the manifest did not declare. Client-side apply preserves it; SSA detects the conflict.
Sources of drift
The production sources of drift, in order of frequency:
- Direct kubectl writes.
kubectl edit,kubectl set,kubectl scale,kubectl patch. These bypass GitOps and the next apply will revert them. - Controller additions. HPA writes
spec.replicas. Service mesh injects sidecars. Cert-manager writes TLS annotations. These are intentional and SSA-friendly. - Helm post-rendering. Helm hooks, post-install Jobs,
and
helm upgradewith new values produce fields not in the original manifest. - Operators (CRD controllers). Custom controllers write status fields, add finalizers, and modify specs. These are legitimate and should be reflected in the manifest.
- Webhooks. Mutating webhooks add fields based on inputs that are not visible to kubectl.
- Forgotten experiments. Someone ran
kubectl execto debug and the side effects stuck (annotations, labels).
The first category is the dangerous one — it represents unauthorised changes to production. The others are legitimate and should be accounted for in the manifest.
Detection: kubectl diff
The basic detection mechanism:
kubectl diff -f manifests/
This compares every manifest in the directory against the live state and prints a unified diff of the changes. Any non-empty output is drift.
For CI:
# .github/workflows/drift-check.yml
name: Drift check
on: [schedule, workflow_dispatch]
jobs:
drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: azure/setup-kubectl@v4
- name: Diff against prod
run: |
kubectl diff -f manifests/
env:
KUBECONFIG: ${{ secrets.PROD_KUBECONFIG }}
A non-empty diff fails the workflow. This catches drift on a schedule (e.g., every 6 hours) and on demand.
Detection: GitOps controllers
GitOps controllers (ArgoCD, Flux) detect drift continuously.
ArgoCD polls the cluster every 3 minutes by default and
compares the live state against the Git state. Drift is shown
in the ArgoCD UI and emitted as a metric (argocd_app_info
with sync_status: OutOfSync).
flowchart LR
Git[Manifest in Git] --> Argo[ArgoCD]
Live[Live state] --> Argo
Argo -->|diff| Compare[Comparison]
Compare -->|matches| Synced[Synced]
Compare -->|differs| OutOfSync[OutOfSync]
OutOfSync -->|alert| Slack[Slack alert]
The GitOps model is continuous reconciliation: the controller detects drift and reverts it automatically (self-heal) or surfaces it for human review.
Detection: kube-state-metrics and alerts
kube-state-metrics exposes the live state as Prometheus
metrics. Some metrics expose drift directly:
kube_deployment_status_replicasvskube_deployment_spec_replicas— divergence in replica counts (Deployment controller may be lagging or the HPA may have moved them).kube_deployment_metadata_generation— increments on spec changes; if it keeps growing without the controller reconciling, the controller is stuck.kube_pod_container_status_restarts_total— restarts; not drift but a related symptom.
Alerts:
- alert: KubernetesDeploymentReplicasMismatch
expr: |
kube_deployment_spec_replicas{namespace="prod"}
!=
kube_deployment_status_replicas{namespace="prod"}
for: 15m
annotations:
summary: "Deployment {{ $labels.deployment }} has spec != status replicas"
This catches sustained divergence (15 minutes) between the operator’s intent (spec) and the controller’s observation (status).
Remediation: the reconcile loop
The right remediation is a reconcile loop: a controller that detects drift and re-applies the desired state. GitOps controllers do this; so does the Deployment controller internally.
flowchart LR
State[Observe state] --> Diff[Compare to desired]
Diff -->|matches| Done[No-op]
Diff -->|differs| Act[Apply desired state]
Act --> State
The pattern:
- Observe the live state (LIST/WATCH on the API).
- Compare against the desired state (the manifest).
- If they differ, apply the desired state.
- Repeat forever.
This is the Kubernetes-native pattern; every controller does it. GitOps controllers extend it from cluster-level controllers (Deployment, StatefulSet) to manifest-level controllers (the GitOps agent itself).
Remediation: controlled reverts
When drift is detected but the GitOps controller cannot or should not auto-revert (e.g., the change was authorised), the human path is:
# 1. Capture the live state
kubectl get deployment web -o yaml > /tmp/web-drift.yaml
# 2. Decide: revert or accept?
# - If the change was unauthorised: revert
# - If the change was a controller adding a legitimate field:
# update the manifest to declare it, then re-apply
# 3. If reverting: apply the manifest
kubectl apply -f manifests/deployment.yaml
# 4. Verify
kubectl diff -f manifests/deployment.yaml
# (empty output = no drift)
The decision matrix:
| Source of drift | Action |
|---|---|
Unauthorised kubectl edit | Revert by re-applying manifest |
| HPA scaling | Update manifest to omit spec.replicas |
| Service mesh sidecar injection | Update manifest to declare the sidecar |
| Manual annotation | Decide: is it needed? Reflect in manifest or remove |
| Webhook addition | Update manifest to declare the field |
The hardest case is the last: webhooks that add fields based on inputs the manifest does not see. These require coordination with the webhook author.
Alerting on sustained drift
Production discipline is to alert when drift persists:
# ArgoCD: applications out of sync for more than 30 minutes
argocd_app_info{sync_status="OutOfSync"} == 1
# Joined with argocd_app_sync_status{sync_status_code="OutOfSync"} and a `for:` clause
- alert: ArgoCDAppOutOfSync
expr: |
argocd_app_info{sync_status="OutOfSync"} == 1
for: 30m
labels:
severity: warning
annotations:
summary: "ArgoCD application {{ $labels.name }} is out of sync"
The for: 30m clause prevents alerts on transient drift (a
controller briefly out of sync during a rollout). Sustained
drift is the signal that something is wrong.
Cross-course references
- The Terraform course part
XVII-Terraform-Driftcovers state-vs-config drift; the same discipline applies at the cluster level. - The Ansible course part
XXXVI-Ansible-Driftcovers configuration drift; cluster drift is the same problem at a different layer. - The GitOps course (CIII-Kubernetes-GitOps) covers ArgoCD and Flux in depth; both implement continuous reconciliation.
Quiz
Knowledge check · 4 questions
Q1. Which of the following is the most common source of unauthorised drift in a production cluster?
Q2. A Deployment whose `spec.replicas` is 5 because an HPA scaled it, while the manifest says `replicas: 3`, is always drift that must be reverted.
Q3. An operator runs `kubectl scale deployment web --replicas=10` during an incident. The incident is resolved and forgotten. Three weeks later, the cluster is running with 10 replicas but the manifest says 3. The next `kubectl apply` is scheduled for next month. Diagnose the failure mode.
Cluster has 30 workloads managed by ArgoCD. selfHeal is disabled on this application (per team policy — they want manual sync). An operator scaled web to 10 during a load spike incident. The ArgoCD UI shows 'OutOfSync' for the application; no one noticed. Three weeks later, the cluster is running 10 replicas; the manifest says 3.
Q4. What is the difference between drift detection and drift remediation? Give an example of each.
Passing score: 75%. Answers are checked in this browser.
Production discipline
- Detect drift continuously. A daily CI job is the minimum; ArgoCD’s 3-minute polling is the standard.
- Distinguish legitimate from unauthorised drift. HPA, service mesh, and webhook additions are not drift; kubectl edits and manual scales are.
- Enable GitOps selfHeal for production applications where the controller can safely revert unauthorised changes. For high-risk applications, disable selfHeal and require manual sync.
- Alert on sustained drift. A
for: 24hclause on ArgoCD OutOfSync catches drift that persists past a normal rollout window. - Make drift a runbook signal, not a notification. A drift alert that no one acts on is worse than no alert. The runbook should specify the action: investigate, revert, or reflect in manifest.