Dit wil ik nooit meer doen: een cluster vragen wat het draait en het antwoord niet vertrouwen. Jarenlang was dat normaal. Iemand draaide kubectl apply, misschien vanaf een laptop, misschien vanuit een CI-job die niemand meer kon vinden, en de live staat dreef langzaam weg van alles wat ergens opgeschreven stond. Toen ik op GitOps overstapte, verdween die hele categorie onzekerheid. Ik push naar Git, en het cluster convergeert tot het matcht. Wil ik weten wat gedeployed is, dan lees ik een bestand.

ArgoCD is de tool die ik daarvoor pak. Deze post loopt van de kleinst mogelijke versie tot de config die ik echt draai, zodat je kunt stoppen zodra je genoeg hebt.

Waarom Git als source of truth

Begin bij de oude manier:

Developer → kubectl apply → Cluster

De vraag die dit niet kan beantwoorden is juist de belangrijke: wat is er op dit moment echt gedeployed? Je queryt het cluster en hoopt dat niemand er met de hand aan heeft gezeten. Drift sluipt er stilletjes in. Rollbacks zijn handmatig, en de enige audit trail is “iemand heeft kubectl gedraaid, waarschijnlijk.”

GitOps verplaatst de waarheid een stap naar achteren:

Developer → Git Push → ArgoCD → Cluster

Nu houdt Git de gewenste staat vast, en ArgoCD’s taak is om de werkelijkheid daarop te laten matchen. Verander iets met de hand en ArgoCD zet het terug. Wil je weten wat draait? Lees de repo. Wil je terug? Revert de commit. Dit is hetzelfde instinct dat ik beschreef in Sovereign Infrastructure: ik wil systemen die ik kan inspecteren, niet systemen die ik moet ondervragen. Imperatieve commando’s verdwijnen op het moment dat ze klaar zijn. Declaratieve staat in Git blijft, met een geschiedenis eraan vast.

De simpelste versie

Voordat we het over concepten hebben, zet ik ArgoCD aan zodat je iets hebt om op te klikken. Eén namespace, één manifest.

# Maak namespace
kubectl create namespace argocd

# Installeer ArgoCD
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# Wacht tot pods ready zijn
kubectl wait --for=condition=Ready pods --all -n argocd --timeout=300s

Klaar. ArgoCD draait. Haal nu het wachtwoord op en open de UI:

# Haal het initiële admin wachtwoord op
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d

# Port-forward de UI
kubectl port-forward svc/argocd-server -n argocd 8080:443

# Open https://localhost:8080, login als 'admin' met het wachtwoord hierboven

Genoeg om rond te kijken. Drie concepten maken duidelijk wat je ziet.

De drie concepten die je nodig hebt

Application

De Application is de unit waarmee je werkt. Het beantwoordt drie vragen: waar leven de manifests, waar moeten ze heen, en hoe streng moet ArgoCD ze in sync houden.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/yourorg/yourrepo.git
    targetRevision: main
    path: manifests/my-app
  destination:
    server: https://kubernetes.default.svc
    namespace: my-app
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

Project

Een Project groepeert applicaties en zet access boundaries. Het default project staat alles toe, wat prima is zolang je leert. In productie wil je dingen afschermen zodat het ene team niet in de namespace van een ander team kan deployen.

Sync

Sync is de handeling van het cluster laten matchen met Git. Het komt in drie smaken:

  • Handmatig: jij klikt op de knop
  • Automated: ArgoCD handelt zodra Git verandert
  • Self-healing: ArgoCD draait ook wijzigingen terug die direct tegen het cluster zijn gemaakt

Layer 1: je eerste echte deployment

Tijd om iets te deployen. Ik houd het expres saai: een nginx Deployment en Service, volledig beheerd via Git.

Stap 1: maak een Git repository

Zet een repo op (of hergebruik er een) met deze indeling:

my-gitops-repo/
└── apps/
    └── nginx/
        ├── deployment.yaml
        ├── service.yaml
        └── kustomization.yaml

deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  labels:
    app: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
        ports:
        - containerPort: 80

service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  selector:
    app: nginx
  ports:
  - port: 80
    targetPort: 80

kustomization.yaml:

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - service.yaml

Commit en push.

Stap 2: wijs ArgoCD ernaartoe

# argocd-application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: nginx
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/YOURUSER/my-gitops-repo.git
    targetRevision: main
    path: apps/nginx
  destination:
    server: https://kubernetes.default.svc
    namespace: nginx
  syncPolicy:
    syncOptions:
      - CreateNamespace=true
    automated:
      prune: true
      selfHeal: true

Apply het:

kubectl apply -f argocd-application.yaml

Stap 3: kijk hoe het convergeert

Open de ArgoCD UI en je applicatie verschijnt. Met automated sync aan deployt het direct. Anders klik je op “Sync.”

flowchart LR
    subgraph dashboard["ArgoCD Dashboard - nginx"]
        direction LR
        DEP["Deployment<br/>nginx"] --> RS["ReplicaSet<br/>nginx"]
        RS --> POD["Pod x2<br/>nginx"]
        DEP --> SVC["Service<br/>nginx"]
    end

De UI tekent de resource tree met health en sync status op elke node. Geen giswerk over wat er live staat.

De workflow die kubectl vervangt

Dit is het deel dat mijn gewoontes veranderde. Wil je het aantal replicas omhoog?

  1. Edit in Git: verander replicas: 2 naar replicas: 3 in deployment.yaml
  2. Commit en push
  3. ArgoCD detecteert de wijziging (binnen 3 minuten standaard, of direct met webhooks)
  4. Cluster update vanzelf

Geen kubectl, geen scripts, geen knagende twijfel of ik het commando wel echt heb gedraaid. Git is het register.

Rollback is gewoon reverten

Iets kapot na een wijziging? Revert de commit, push, en ArgoCD loopt het cluster terug.

git revert HEAD
git push

# ArgoCD rollbackt automatisch het cluster

De hele geschiedenis van het cluster zit nu in git log:

git log --oneline apps/nginx/
# a1b2c3d Scale nginx to 3 replicas
# d4e5f6g Initial nginx deployment

Self-healing in actie

Zet selfHeal: true aan en probeer slimmer te zijn:

# Handmatig de deployment schalen
kubectl scale deployment nginx -n nginx --replicas=5

# Wacht even
kubectl get deployment nginx -n nginx
# NAME    READY   UP-TO-DATE   AVAILABLE
# nginx   3/3     3            3

ArgoCD zag de drift en trok het terug naar wat Git zegt. Dit is de feature die de ergste faalmodus in infrastructuur de das om doet: de instelling die iemand vorig jaar veranderde en die niemand meer kent en niemand durft aan te raken. Staat het niet in Git, dan overleeft het niet.

Layer 2: sync policies in detail

De sync policy is waar je instelt hoe uitgesproken ArgoCD wordt:

syncPolicy:
  automated:
    prune: true      # Verwijder resources die uit Git verwijderd zijn
    selfHeal: true   # Draai handmatige wijzigingen terug
    allowEmpty: false # Sync niet als source leeg is
  syncOptions:
    - CreateNamespace=true
    - PruneLast=true  # Prune na andere syncs
    - ApplyOutOfSyncOnly=true  # Alleen gewijzigde resources applyen
  retry:
    limit: 5
    backoff:
      duration: 5s
      factor: 2
      maxDuration: 3m

Mijn advies: begin met automated sync, prune en selfHeal allemaal aan. Dat geeft je de volledige ervaring, waar Git echt de waarheid is en het cluster geen geheimen heeft. Je kunt het altijd versoepelen voor een specifieke applicatie die echt handmatige handling nodig heeft, maar maak dat de uitzondering die je bewust kiest.

Private repositories

Echte repos zijn meestal private, dus ArgoCD heeft credentials nodig:

# Met de CLI
argocd repo add https://github.com/yourorg/private-repo.git \
  --username git \
  --password ghp_yourtoken

# Of als Kubernetes secret
kubectl create secret generic private-repo \
  -n argocd \
  --from-literal=url=https://github.com/yourorg/private-repo.git \
  --from-literal=username=git \
  --from-literal=password=ghp_yourtoken

kubectl label secret private-repo -n argocd \
  argocd.argoproj.io/secret-type=repository

Voor SSH:

argocd repo add git@github.com:yourorg/private-repo.git \
  --ssh-private-key-path ~/.ssh/id_rsa

Helm en Kustomize

ArgoCD spreekt zowel Helm als Kustomize native, dus je hoeft zelden iets met de hand plat te slaan.

Helm charts

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: prometheus
  namespace: argocd
spec:
  source:
    repoURL: https://prometheus-community.github.io/helm-charts
    chart: prometheus
    targetRevision: 25.0.0
    helm:
      values: |
        server:
          persistentVolume:
            enabled: true
            size: 10Gi
  destination:
    server: https://kubernetes.default.svc
    namespace: monitoring

Kustomize overlays

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app-production
spec:
  source:
    repoURL: https://github.com/yourorg/yourrepo.git
    path: apps/my-app/overlays/production
    targetRevision: main
  destination:
    server: https://kubernetes.default.svc
    namespace: production

Wijs het naar een directory met een kustomization.yaml en het regelt de rest.

Fouten die ik mensen zie maken

Prune vergeten aan te zetten

Zonder prune: true blijft een live resource hangen nadat je het manifest uit Git verwijdert. Je haalt het bestand weg, gaat ervan uit dat het weg is, en weken later struikel je over de wees.

Secrets in Git zetten

Commit nooit plain secrets. Pak in plaats daarvan een van deze:

De “alleen deze keer” handmatige wijziging

Op het moment dat je iets met de hand fixt, heb je drift gemaakt. Of commit de fix naar Git, of laat selfHeal het ongedaan maken. Er is geen stil tussengebied dat schoon blijft.

Eén gigantische Application voor alles

Eén Application die je hele platform beheert wordt snel een zooi. Splits per service of team zodat een sync-fout op één plek niet alles blokkeert.

Waar je hierna heen gaat

Je hebt nu een werkende GitOps-loop. De interessante features bouwen erbovenop:

Mijn aanbeveling

  1. Begin met één applicatie en krijg de workflow in je vingers.
  2. Zet automated sync plus selfHeal aan zodat je voelt wat Git-als-waarheid echt doet.
  3. Gebruik een dedicated gitops repo, los van je applicatie broncode.
  4. Stel webhooks in zodat sync triggert op push in plaats van polling.
  5. Merk op wanneer je handmatige controle wilt en vraag wat het systeem mist in plaats van naar kubectl te grijpen.

Ga terug naar dat eerste gevoel dat ik beschreef, een cluster vragen wat het is en het antwoord niet vertrouwen. Zodra het antwoord in Git leeft en het cluster zichzelf eerlijk houdt, is die onzekerheid weg, en wil je er niet meer naar terug.