Skip to main content
RunBook Academy

Git, CI/CD & GitOpsCV · GitOps Anti-PatternsUncontrolledDrift

Uncontrolled drift — when the cluster forgets Git

Advanced⏱ ~24 mingitargocdflux

What you'll learn

  • Identify the three controls that turn a controller from a passive reader into an active reconciler
  • Configure self-heal with the right drift-detection window for the workload
  • Apply IgnoreDifferences to the fields that legitimately change in the cluster without reconciling to Git
  • Design a reconciliation budget that alerts on persistent drift rather than transient reconcile lag

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

A GitOps controller without self-heal is a watchdog that barks but does not bite. The controller polls the cluster, compares the live state to the desired state in Git, and reports OutOfSync when the two diverge. With self-heal disabled, the report is the entire response.

The three controls drift needs

Drift is manageable when the controller combines three controls. Remove any one and the pattern becomes unsafe.

    flowchart LR
        A["Cluster diverges from Git"] --> B["Self-heal reconciles"]
        B --> C["Cluster matches Git"]
        D["IgnoreDifferences"] --> B
        E["Reconciliation budget"] --> F["Alert on persistent drift"]
        F --> G["Operator investigates"]
  • Self-heal. The controller’s reconcile loop applies the desired state when the live state diverges. Argo CD’s automated.selfHeal: true; Flux’s Kustomization controllers’ implicit reconcile-on-drift.
  • IgnoreDifferences. A per-field declaration naming the fields that legitimately differ from Git.
  • Reconciliation budget. A monitoring signal that alerts on persistent reconcile failure.

The team that has none has a cluster that drifts without bound and an audit trail that records the divergence only after an incident.

Why self-heal is not optional

A controller without self-heal is a controller that requires a human to be the reconciler. A kubectl scale deployment from three to ten replicas, never reverted, is a divergence that persists for months because no one reads the controller’s report.

apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
  syncPolicy:
    automated:
      selfHeal: true

The flag above is the difference. With the flag, the next reconcile applies the desired state and reverts the cluster-side change. Without the flag, the reconcile records the change and waits for a human.

IgnoreDifferences for legitimate divergence

A Kubernetes Service gets a clusterIP assigned at create time by the API server. The value is not in the manifest; the controller’s diff would mark the Service OutOfSync on every reconcile. The fix is an ignoreDifferences declaration:

apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
  ignoreDifferences:
    - group: ""
      kind: Service
      jsonPointers:
        - /spec/clusterIP
    - group: autoscaling
      kind: HorizontalPodAutoscaler
      jsonPointers:
        - /spec/replicas

The declaration names the fields that legitimately diverge. The un-disciplined use of ignoreDifferences is the same anti-pattern as disabling self-heal globally.

The reconciliation budget as an alert

Self-heal applies the desired state; the cluster matches Git. When the reconcile fails - a manifest the controller cannot apply, a webhook that rejects the resource - the controller retries. Argo CD records the failure in argocd_app_sync_total{result="error"}; Flux records it in flux_reconcile_total{result="error"}. A reconciliation budget alerts when the rate of failures exceeds a threshold.

- alert: GitOpsReconcileFailing
  expr: |
    sum(rate(argocd_app_sync_total{result="error"}[15m])) > 0.1
  for: 15m
  labels:
    severity: page

The alert fires when the controller cannot recover on its own. The team’s response is to read the controller’s logs, identify the failing Application, and decide whether to fix the manifest.

Production discipline

  1. Self-heal is enabled by default. The opt-out is per-resource, with an ignoreDifferences declaration.
  2. ignoreDifferences entries are reviewed.
  3. A reconciliation budget alerts on persistent failure.
  4. Drift incidents trigger a follow-up.

Cross-course references

  • This course, Part LXXVII (ArgoCD) - the reconcile loop in detail.
  • This course, Part LXXXI (SyncedAndHealthy) - the synced/healthy matrix.

Quiz

Knowledge check · 4 questions

  1. Q1. A controller reports `OutOfSync` on an Application whose Deployment has been scaled from 3 to 10 replicas via `kubectl scale`. Self-heal is disabled. What happens on the next reconcile?

  2. Q2. A team disables self-heal globally to avoid surprise reconciliations. The cluster will not drift because no one applies out-of-band changes.

  3. Q3. Name the three controls that turn drift from a silent failure into a managed one.

  4. Q4. Diagnose an uncontrolled-drift incident and recommend the architecture that prevents it.

    An Argo CD Application runs without self-heal. An operator scales a Deployment from 3 to 10 replicas during a traffic spike. The controller reports OutOfSync on every reconcile. The team disables alerts to reduce noise. Three months later, the spike is over, but the Deployment is still at 10 replicas because no one reverted the scale and the controller did not either.

Passing score: 75%. Answers are checked in this browser.