GitOps in het homelab, en de twee plekken waar het stukliep

Mijn homelab draait op vier NUCs met Proxmox eronder. Daarin staan zeven virtuele machines waarop k3s draait: drie control-plane nodes met etcd en vier workers. Rancher zit er als beheerlaag bovenop en Argo CD zorgt dat wat op het cluster hoort te staan er ook echt staat.

Het doel is niet dat het mooi is. Het doel is dat ik het opnieuw kan opbouwen zonder te hoeven onthouden wat ik de vorige keer had aangeklikt. Onderweg daarnaartoe liep ik tegen twee dingen aan die in geen enkele quickstart staan. Dit stuk gaat vooral daarover.

Waar de knip ligt

GitOps belooft dat je cluster is wat er in git staat. Dat werkt uitstekend, zolang er al een cluster is waarop Argo CD draait. Die allereerste stap kun je niet met GitOps doen, want er is nog niets dat manifesten uitrolt.

Bij mij ligt de grens daarom op precies dat punt. Ansible brengt de machines van leeg naar een draaiend cluster met Argo CD erop: Debian-voorbereidingen, swap uit, k3s in HA, de CNI, kube-vip voor een virtueel API-adres, Longhorn voor storage, cert-manager, Rancher, en als laatste Argo CD met de Application-resources die naar de repository wijzen. Eén playbook. Daarna komt alles uit git en raak ik Ansible in principe niet meer aan.

Cilium moet voor kube-vip

k3s start bij mij zonder flannel en zonder kube-proxy, omdat Cilium dat werk overneemt:

k3s server --cluster-init \
  --flannel-backend=none \
  --disable-network-policy \
  --disable=kube-proxy

kube-vip draait als DaemonSet en gebruikt leader election om te bepalen welke node het virtuele API-adres opeist. Voor die leader election praat het met de Kubernetes API. Daar zit het probleem: zonder kube-proxy en zonder werkende CNI is er geen ClusterIP-routing. De Service waarop de API bereikbaar hoort te zijn bestaat wel, maar er is niets dat verkeer ernaartoe stuurt.

Het gevolg is vervelend om te debuggen, want er gaat niets zichtbaar stuk. De DaemonSet komt omhoog, de pods staan op Running, en de VIP wordt nooit geclaimd. Je zit te kijken naar een gezond ogend cluster waarvan het API-adres niet reageert.

De oplossing is puur een kwestie van volgorde: eerst de CNI, dan pas kube-vip.

- name: Install Cilium CNI (before kube-vip)
  hosts: k3s_server_primary
  roles:
    - role: helm_cli
    - role: cilium

- name: Configure kube-vip for a highly-available API endpoint
  hosts: k3s_server_primary
  roles:
    - role: kube_vip

Laat je flannel gewoon staan, dan heb je hier geen last van. Dat verklaart ook waarom je het in de meeste handleidingen niet tegenkomt: die gaan uit van de standaardopstelling, en dit is precies het randje daarbuiten.

Rancher en Argo CD die elkaar bestrijden

Het tweede probleem kostte me meer tijd, omdat het er in het begin uitziet als iets dat vanzelf overgaat.

Rancher schrijft annotaties op resources die het beheert of zelfs alleen maar ziet. Op Deployments en DaemonSets komt cattle.io/status te staan, en op Services en Ingresses daarnaast field.cattle.io/publicEndpoints met de poorten die naar buiten staan.

Argo CD vergelijkt de toestand in het cluster met wat er in git staat. Die annotaties staan niet in git. Dus is de applicatie OutOfSync.

Met selfHeal: true zet Argo CD het netjes recht, waarna Rancher de annotatie er weer op zet, waarna Argo CD opnieuw ingrijpt. Je herkent het aan applicaties die eindeloos tussen Synced en OutOfSync wisselen en aan een eventlog die volloopt met sync-acties die niets oplossen.

De uitweg is Argo CD vertellen dat het die specifieke velden met rust moet laten:

spec:
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jqPathExpressions:
        - .metadata.annotations."cattle.io/status"
    - group: ""
      kind: Service
      jqPathExpressions:
        - .metadata.annotations."cattle.io/status"
        - .metadata.annotations."field.cattle.io/publicEndpoints"
    - group: networking.k8s.io
      kind: Ingress
      jqPathExpressions:
        - .metadata.annotations."cattle.io/status"
        - .metadata.annotations."field.cattle.io/publicEndpoints"

Het voelt als een pleister, en dat is het ook. Maar het is wel de juiste: die annotaties zijn van Rancher, ze horen niet in git thuis, en er valt niets te winnen door er een gevecht over te voeren. Je zegt alleen wie waarover gaat.

Wat er nog niet klopt

Secrets zitten bij mij nog in Rancher. Dat werkt, maar het betekent dat een deel van mijn configuratie buiten git valt, precies wat ik met de rest juist wilde voorkomen. HashiCorp Vault staat op de lijst, met external-secrets ertussen zodat de manifesten in git alleen nog verwijzingen bevatten. Tot die tijd is het eerlijke antwoord op de vraag of alles uit git komt: bijna.

Wat het oplevert

Vooral rust. Een applicatie uitzetten is bij mij het bijbehorende Application-bestand naar een disabled/ map verplaatsen. Een node opnieuw opbouwen is het playbook draaien en even wachten. En als ik over een half jaar wil weten waarom iets staat zoals het staat, hoef ik het niet te reconstrueren uit mijn geheugen, want het staat in de commit-historie.

Dat laatste is voor een homelab misschien overdreven. Voor de platformen waar ik overdag aan werk is het het verschil tussen een omgeving die je beheert en een omgeving die jou beheert.