De hele belofte van GitOps is dat Git de source of truth is. Die belofte houdt stand tot het moment dat iemand om 2 uur ’s nachts kubectl edit runt op een deployment om een incident te stoppen, een mutating webhook stilletjes een resource herschrijft, of een half afgemaakte sync je cluster ergens tussen wat Git wilde en wat het kreeg achterlaat. Nu zegt Git het ene en doet het cluster het andere, en niemand heeft het je verteld.

Dat gat is configuratie drift, en het is het stuk van GitOps dat mensen vergeten te verdedigen. Het goede nieuws: ArgoCD let er al op. De adder onder het gras is dat de defaults niet doen wat je waarschijnlijk aanneemt, en een paar ervan bijten je. Deze post loopt van de simpelst mogelijke drift check tot de setup die ik echt draai, laag voor laag. Stop waar je genoeg hebt.

Wat drift eigenlijk is

Drift is het moment dat de echte staat van je cluster niet langer matcht met de gewenste staat in Git.

flowchart LR
    subgraph git["Git zegt (Source of truth)"]
        G1["replicas: 3"]
        G2["image: v1.2.3"]
        G3["cpu: 100m"]
    end

    subgraph cluster["Cluster heeft (Actuele staat)"]
        C1["replicas: 5"]
        C2["image: v1.2.3"]
        C3["cpu: 200m"]
    end

    git -.->|"!="| cluster

Git zegt drie replicas, het cluster draait er vijf. Hoe gebeurt dat? Een paar verdachten:

  1. Handmatige wijzigingen: iemand runde kubectl scale of kubectl edit en wilde het later wel in Git fixen
  2. Horizontal Pod Autoscaler: HPA paste replicas zelf aan
  3. Mutating webhooks: een admission controller herschreef de resource onderweg
  4. Controller side effects: een operator wijzigde een veld dat hij beheert
  5. Gedeeltelijke syncs: een sync stierf halverwege

Een deel van die drift is gewenst (HPA die zijn werk doet). Het meeste niet. Het echte probleem is dat je van buitenaf de gewenste soort niet kunt onderscheiden van de gevaarlijke soort, dus moet je alles als een signaal behandelen.

Waarom ik hier om geef

Ongedetecteerde drift annuleert stilletjes de reden dat je ooit voor GitOps koos. Vier dingen breken:

  1. Audit trail: “wat is gedeployed?” is niet langer “lees de Git history” maar “ga het live cluster porren”
  2. Disaster recovery: herbouw vanuit Git en je krijgt een cluster dat niet matcht met het cluster dat je verloor
  3. Security: een ongeautoriseerde wijziging aan een Deployment blijft daar onopgemerkt zitten
  4. Reproduceerbaarheid: twee clusters gebouwd vanuit dezelfde repo lopen subtiel uiteen

Dit hangt direct samen met het sovereignty-verhaal waar ik over blijf doorgaan: ik wil begrijpen wat ik draai, en een cluster dat stilletjes is weggedwaald van zijn gedeclareerde staat is een black box die ik zelf heb gebouwd. Drift detection is hoe ik Git eerlijk houd.

De simpelste versie: sync status

Je hebt geen extra tooling nodig om te beginnen. ArgoCD vergelijkt Git al continu met het live cluster en rapporteert een van drie staten:

  • Synced: cluster matcht Git exact
  • OutOfSync: het vond verschillen
  • Unknown: het kan de staat niet bepalen
ApplicationSync StatusHealth
frontendSyncedHealthy
backendOutOfSyncHealthy
databaseSyncedHealthy
cacheSyncedDegraded

OutOfSync is je drift signaal. Dat is de hele basisversie: kijk naar het dashboard, alles wat niet groen is, is drift. Het werkt, maar naar een dashboard staren is een waardeloze detectiestrategie, en het standaard sync-gedrag is misschien niet wat je verwacht. Dus laten we een laag toevoegen.

Laag 1: self-heal, en waarom het een tweesnijdend zwaard is

ArgoCD kan drift automatisch voor je terugdraaien:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: frontend
spec:
  syncPolicy:
    automated:
      selfHeal: true  # Draai handmatige wijzigingen terug
      prune: true     # Verwijder orphaned resources

Met selfHeal: true merkt ArgoCD op het moment dat iemand kubectl scale deployment frontend --replicas=5 runt het gat op en zet het terug naar wat Git zegt, meestal binnen seconden. Dit is het self-healing infrastructuur-ideaal in actie: het systeem corrigeert zichzelf in plaats van te wachten tot een mens het opmerkt.

Het heeft ook scherpe randen. Diezelfde automatisering draait een bewuste wijziging terug, walst over een HPA-aanpassing heen, en laat een echte spoed-hotfix meteen terugkaatsen. Voor de meeste apps laat ik selfHeal toch aan staan, omdat de kosten van drift laten oplopen hoger zijn dan de kosten van de incidentele teruggedraaide wijziging. Ga er alleen in met kennis van de trade-off, in plaats van die tijdens een incident te ontdekken.

Laag 1b: ArgoCD vertellen wat het moet negeren

Sommige velden horen buiten Git te leven, en self-heal vecht je daarover tenzij je het zegt. Het klassieke geval is replica count wanneer HPA die bezit:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: autoscaled-app
spec:
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers:
        - /spec/replicas
    - group: ""
      kind: Service
      jsonPointers:
        - /spec/clusterIP

Dit vertelt ArgoCD om geen drift meer te rapporteren op die specifieke velden. Degene die ik het meest gebruik:

  • /spec/replicas wanneer HPA de baas is
  • /spec/clusterIP, want Kubernetes wijst die toe
  • /metadata/annotations die controllers zelf toevoegen
  • /status, wat altijd controller-beheerd is

Het doel hier is de ruis stoppen, zodat wanneer iets wel als OutOfSync toont, het iets betekent.

Laag 2: detecteren zonder auto-fix

Self-heal is de juiste default, maar niet overal. Voor de systemen waar ik wil dat een mens kijkt voordat er iets beweegt, zet ik de automatisering uit en houd ik de detectie aan:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: critical-app
spec:
  syncPolicy:
    automated:
      selfHeal: false  # Niet auto-fixen
      prune: false     # Niet auto-deleten

Nu vlagt ArgoCD nog steeds OutOfSync en toont je de diff, maar het wacht tot jij beslist. Dit verdient zijn plek voor kritieke productie waar je ogen wilt op elke wijziging, voor het debuggen van de bron van terugkerende drift, en voor apps die bewust deels buiten GitOps beheerd worden.

Laag 2b: laat je notificeren in plaats van kijken

Detectie helpt alleen als het je bereikt. Tuig ArgoCD notificaties op zodat drift naar je toe komt in plaats van dat jij naar het dashboard gaat:

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-notifications-cm
  namespace: argocd
data:
  trigger.on-sync-status-unknown: |
    - when: app.status.sync.status == 'OutOfSync'
      send: [app-out-of-sync]
  template.app-out-of-sync: |
    message: |
      Application {{.app.metadata.name}} is OutOfSync.
      Sync Status: {{.app.status.sync.status}}
      Health: {{.app.status.health.status}}
      Repository: {{.app.spec.source.repoURL}}

Richt dat op Slack, PagerDuty, of email. Drift zou geluid moeten maken.

De diff lezen

Wanneer iets driftet, vertelt ArgoCD je precies wat er veranderde, en dat is het halve werk:

argocd app diff frontend

Of klik op de OutOfSync app in de UI om hetzelfde te zien:

--- Git (desired)
+++ Cluster (actual)
@@ -1,4 +1,4 @@
 spec:
-  replicas: 3
+  replicas: 5
   template:
     spec:

Dit is waar begrijpen het wint van gokken. Je ziet het veld, de oude waarde, de nieuwe waarde, en je kunt beginnen te vragen wie of wat het deed.

Eén ding om scherp te krijgen: refresh vs sync

Deze twee operaties klinken hetzelfde en doen heel verschillende dingen, en ze door elkaar halen veroorzaakt ongelukken.

Refresh vergelijkt Git met het cluster en werkt de status bij. Het verandert niets.

argocd app get frontend --refresh

Sync past de Git staat toe op het cluster. Het verandert dingen.

argocd app sync frontend

Refresh is veilig en draait standaard zelf elke drie minuten. Sync wijzigt live staat, dus doe het bewust (of laat automatisering het doen, met kennis van wat dat betekent).

De volledige plaat: mijn drift-strategie

Zo splits ik dit eigenlijk over omgevingen. Niets ervan is exotisch; het zijn gewoon de lagen hierboven, toegepast met een beetje oordeel.

Voor development / staging

  • selfHeal: true om alle drift terug te draaien
  • prune: true om orphaned resources op te ruimen
  • snelle feedback, geen ceremonie, pure GitOps

Voor production (meeste apps)

  • selfHeal: true en prune: true
  • alerts op elk OutOfSync event
  • en cruciaal: onderzoek waarom het driftte in plaats van de heal het laten overplakken

Voor production (kritiek of speciaal)

  • selfHeal: false, menselijke review vereist
  • prune: false, alleen handmatige verwijdering
  • strikte alerts en een expliciete sync approval voordat er iets verandert

Voor HPA-managed apps

ignoreDifferences:
  - group: apps
    kind: Deployment
    jsonPointers:
      - /spec/replicas
syncPolicy:
  automated:
    selfHeal: true  # Voor andere velden

Als drift opduikt, jaag op de bron

De drift helen fixt het symptoom. Uitvinden waarom het gebeurde fixt het probleem:

  1. Check audit logs: wie runde kubectl?

    kubectl get events --field-selector reason=Update
    
  2. Check controller logs: deed een operator het?

  3. Check admission webhooks: muteert er iets resources bij admission?

    kubectl get mutatingwebhookconfigurations
    
  4. Check de diff: wat veranderde er precies?

    argocd app diff app-name
    

Beter dan detecteren: laat het niet driften

Detectie is het vangnet. De goedkopere winst is de meeste drift om te beginnen onmogelijk maken:

  1. RBAC restricties: beperk wie überhaupt resources kan wijzigen

    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
      name: readonly
    rules:
      - apiGroups: ["*"]
        resources: ["*"]
        verbs: ["get", "list", "watch"]  # Geen create/update/delete
    
  2. Policy enforcement: gebruik Kyverno om handmatige wijzigingen botweg te blokkeren

    apiVersion: kyverno.io/v1
    kind: ClusterPolicy
    metadata:
      name: require-gitops
    spec:
      rules:
        - name: block-manual-changes
          match:
            resources:
              kinds:
                - Deployment
          exclude:
            subjects:
              - kind: ServiceAccount
                name: argocd-application-controller
          validate:
            message: "Wijzigingen moeten via GitOps"
            deny: {}
    
  3. Training: krijg het team in de gewoonte om Git te wijzigen, niet het cluster. Tooling helpt, maar een gedeelde norm helpt meer.

Track drift als metric

Als je Prometheus draait, behandel drift als iets dat je over tijd meet, niet alleen als een momentane status:

# Prometheus query voor out-of-sync apps
count(argocd_app_info{sync_status="OutOfSync"})

En alert wanneer het langer non-zero blijft dan je een sync zou verwachten te duren:

- alert: GitOpsDriftDetected
  expr: count(argocd_app_info{sync_status="OutOfSync"}) > 0
  for: 10m
  labels:
    severity: warning
  annotations:
    summary: "GitOps drift gedetecteerd"
    description: "Eén of meer applicaties zijn OutOfSync met Git"

Mijn checklist voor drift-vrije GitOps

[ ] selfHeal ingeschakeld voor meeste applicaties
[ ] ignoreDifferences geconfigureerd voor HPA-managed replicas
[ ] Notificaties ingesteld voor OutOfSync events
[ ] RBAC beperkt directe cluster wijzigingen
[ ] Policy enforcement voorkomt handmatige wijzigingen
[ ] Monitoring alerteert op drift
[ ] Team getraind op GitOps workflow

Kijk terug naar waar we begonnen: een dashboard met een groen of rood lampje naast elke app. Alles sindsdien maakt dat signaal alleen maar betrouwbaar en zorgt dat je erop handelt voordat het je iets kost. Drift is de stille erosie van de ene garantie die GitOps je geeft, namelijk dat Git de realiteit reflecteert. Detecteer het, heel het automatisch waar dat veilig is, en graaf de rest van de tijd uit waarom het gebeurde. Dat laatste stuk houdt het cluster ervan een black box te worden die je zelf hebt gebouwd.