Het principe
Productie verandert alleen via Git. Op het werk en thuis werk ik volgens hetzelfde patroon:
- Wijzigingen gaan naar een ontwikkelbranch, met validatie in CI.
- Een merge request naar
mainbeschrijft wat er verandert, wat de impact is en hoe je terugdraait. - Een mens merget. Pas daarna rolt het uit.
- Een controller (Flux op het werk, een reconciler of runner thuis) brengt de werkelijkheid in lijn met
main.
Geen losse feature-branches die wekenlang openstaan, geen handmatige kubectl apply die niemand kan terugvinden.
Op het werk: Flux
Meerdere repositories per domein: platform-infrastructuur, observability, message brokers, beheer. Elk met een eigen Flux-Kustomization die main volgt. Helm-charts komen via HelmReleases, dashboards als ConfigMaps uit Git, en alertregels met promtool-tests in de pipeline.
Valkuilen die ik tegenkwam
- Een ontbrekende ConfigMap blokkeert de hele HelmRelease. Verwijst
valuesFromnaar iets dat niet bestaat, dan wordt geen enkele wijziging aan die release meer uitgerold, ook niet de wijzigingen die er niets mee te maken hebben.valuesFromen de bestanden waarnaar het verwijst, wijzig je dus samen. pending-upgradeis een stille blokkade. Flux reconcilet zichtbaar, maar er gebeurt niets. Kijk in de Helm-storage en vergelijk de digests.- Immutable velden. Een StatefulSet met een ander volumeClaimTemplate kan Flux niet patchen.
kubectl delete sts --cascade=orphanlaat de pods en volumes staan, en Flux maakt de StatefulSet opnieuw aan. - Prune tegen een beschermde namespace. Een ValidatingAdmissionPolicy beschermt kritieke namespaces tegen verwijderen. Haal je een namespace uit Git terwijl het label er nog op staat, dan faalt de prune, en blijft de Kustomization op
Ready=Falsestaan. Nieuwe wijzigingen worden dan wel toegepast, maar de status liegt niet: eerst het label weghalen, dan prunen. - Grafana-dashboards via een sidecar schreven op een nieuwe Grafana-versie het bestand na de eerste provisioning opnieuw. Dashboards als ConfigMap mounten werkt voorspelbaar.
- Istio: wie eerst komt, wint. Twee VirtualServices voor dezelfde host op dezelfde gateway worden samengevoegd op volgorde van aanmaken. Een nieuwe route naast een bestaande
prefix: /verliest dus. Zet specifieke routes in dezelfde VirtualService, bovenaan. - Branches die uit elkaar lopen. Als
developmentenmainuit elkaar zijn gegroeid, trek ik ze gelijk zonder force-push en zet ik oude branches als tag in een archief. Zo gaat er niets verloren, en is de geschiedenis leesbaar.
Productiewijzigingen
Een merge request voor productie bevat altijd: wat er verandert, wat helm template of kustomize build laat zien, en hoe je terugdraait. Een server-side dry-run tegen het cluster vangt schemafouten vóór de merge. Het model dat mij helpt, mag MR's aanmaken, maar niet mergen.
Thuis: Gitea Actions
Alles in het homelab staat in een eigen Gitea:
| Repository | Uitrol |
|---|---|
| Monitoring (Prometheus, Alertmanager, regels, runbooks) | Merge naar main → validatie → deploy via SSH |
| Reverse proxy (een vhost per dienst) | Merge naar main → nginx -t → deploy |
| Agents op alle hosts | Ansible vanuit de repository (uitrol wordt opnieuw ingericht) |
| Mediastack | Docker Compose in Git |
| Deze website | Reconciler op de server |
Branch protection op development: geen force-push. Validatie draait op elke pull request.
De website als voorbeeld
Deze site laat zien hoe ver je kunt gaan op een thuisserver:
- CI valideert broncode, content, dependencies (
npm audit), runtimegedrag met een rooktest, en de container (Trivy). - De container draait als non-root met een read-only root filesystem.
- Op de server haalt een pull-gebaseerde reconciler alleen gevalideerde
main-commits op, bouwt een eigen image per commit, activeert die, en doet interne en publieke nacontroles. Mislukt er iets, dan gaat hij automatisch terug naar de vorige release.
Wat het oplevert
- Elke wijziging is terug te vinden: wie, wat, wanneer en waarom.
- Terugdraaien is een revert.
- Validatie vangt fouten vóór productie, niet erna.
- Ook een AI-assistent werkt binnen dezelfde vangrails: hij kan voorstellen, maar niets uitrollen.