Iemand opent een cloud console, klikt door een wizard, laat de defaults staan, en er verschijnt een resource. Het dashboard springt op groen en iedereen gaat door. Zo wordt er in 2026 nog steeds een hoop infrastructuur gebouwd, en ik heb er zelf ook zo gebouwd.
Lang hou ik dat niet vol. Klik ik op een knop en verschijnt er infrastructuur, dan voelt het geleend. Ik wil de configuratie ergens opgeschreven hebben waar ik hem terug kan lezen, en ik wil weten waaróm iets bestaat, niet alleen dát het bestaat. De console geeft me een groen vinkje. Meer niet.
Dus daar gaat deze post over. De afstand tussen infrastructuur die je overkomt en infrastructuur die je in je hoofd kunt houden.
Wat click-ops kost
Klik je naar een werkend systeem, dan krijg je een werkend systeem. Je krijgt er een stapel beslissingen bij die niemand heeft opgeschreven.
Elke resource die uit een GUI rolt draagt instellingen mee die je nooit bewust koos en dependencies die je nooit hebt zien ontstaan. State hoopt zich op waar niemand hem audit. Op dag één doet dat geen pijn. Zes maanden later wel, als iemand vraagt waarom een security group 47 regels heeft, of dat verweesde load balancer weg mag, en het eerlijke antwoord is dat niemand het weet.
Redt de documentatie je dan? Nee. Docs beschrijven wat dingen zouden moeten zijn. Verander je iets in de console zonder de wiki bij te werken, dan lopen die twee uit elkaar, en dat doen ze altijd. Ik heb het zelf ook gedaan, vaker dan me lief is. Na een tijdje zit je de wiki te lezen en hoop je maar dat hij nog klopt.
Als alles opgeschreven staat
Stel je nu hetzelfde systeem voor, maar elk onderdeel bestaat als tekst die je kunt lezen en opnieuw kunt aanmaken.
# Dit bestaat omdat we verkeer moeten toestaan van de monitoring namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-prometheus
namespace: production
spec:
podSelector:
matchLabels:
app: api
ingress:
- from:
- namespaceSelector:
matchLabels:
name: monitoring
ports:
- port: 9090
protocol: TCP
De comment legt uit waarom. De code laat zien wat. Allebei in hetzelfde bestand, allebei onder versiebeheer, leesbaar voor iedereen die de repo cloont. Archeologie is later niet nodig, want de reden is nooit van het ding losgeraakt.
Die ene eigenschap, expliciet in plaats van impliciet, is waarom ik Infrastructure as Code vertrouw. Best practice of niet, ik kan het lezen.
De kosten zitten in je hoofd
Even terug naar de rommelige kant, want de kosten hierboven zijn de goedkope. De dure zijn wat click-ops met je hoofd doet.
Je nam een configuratiebeslissing om een reden. Maanden gaan voorbij. Was die timeout-waarde dragend? Beschermde die IP-whitelist tegen iets specifieks, of heeft iemand hem tijdens een incident toegevoegd en daarna vergeten? Niemand weet het nog, dus de instelling blijft eeuwig staan en wordt langzaam het soort technical debt waar iedereen een beetje bang voor is.
En er is altijd één collega die toevallig nog weet waarom het netwerk eruitziet zoals het eruitziet. Breekt er iets, dan kijkt iedereen naar die collega. Dat is een single point of failure, en het is een mens. Mensen vergeten dingen. Weet ik, want die collega ben ik geweest.
Opgeschreven infrastructuur haalt die kennis uit één vermoeid hoofd en zet hem in een bestand dat het hele team kan lezen.
Wat je ervoor terugkrijgt
Zodra het systeem in Git leeft, zijn een paar dingen geen heldendaad meer.
Geschiedenis is geen gok meer. Elke wijziging heeft een commit message en een auteur, dus je kunt een beslissing terug in de tijd volgen:
git log --oneline -- network-policy.yaml
a1b2c3d Voeg network policy toe voor Prometheus scraping
d4e5f6g Beperk tot specifieke poort (was alles toestaan)
g7h8i9j Initiële creatie voor monitoring toegang
Een tweede paar ogen kijkt mee voordat productie het ziet. De klassieke “sta alles toe”-fout zie je in een console nauwelijks. In een diff springt hij van het scherm:
# Pull request diff
- cidrBlocks:
- - 0.0.0.0/0 # GEVAAR: staat al het verkeer toe
+ cidrBlocks:
+ - 10.0.0.0/8 # Alleen intern netwerk
Herstel is een commando in plaats van een gespannen middag. Gaat een cluster dood, dan bouw je hem niet uit je geheugen op:
# Herstel alles
terraform apply
kubectl apply -k ./infrastructure
argocd app sync --all
Het declaratieve deel houdt dit bij elkaar. Jij schrijft op welke state je wilt, en de realiteit daarmee laten kloppen is het werk van het systeem.
# Ik declareer: er moeten 3 replica's zijn
apiVersion: apps/v1
kind: Deployment
spec:
replicas: 3
Een pod gaat dood, Kubernetes ziet dat er twee draaien in plaats van drie, en start een nieuwe zonder dat iemand ergens op klikt. Jij hebt het contract vastgelegd. Het systeem dwingt het af.
Er is ook ruimte om intentie op te schrijven, en niet alleen mechaniek. Tags en comments maken van configuratie een boodschap aan wie het hierna leest, meestal een toekomstige versie van jezelf:
# Terraform: Waarom deze VPC bestaat
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "production"
Environment = "production"
Purpose = "Primaire VPC voor productie workloads"
Owner = "platform-team"
CostCenter = "infrastructure"
}
}
Zo is m’n homelab ruwweg gebouwd. Alles wat daar draait staat in één repo:
homelab/
├── infrastructure/
│ ├── terraform/ # VMs, netwerken, DNS
│ └── kubernetes/ # Cluster configuratie
├── applications/
│ ├── argocd/ # GitOps controller
│ ├── monitoring/ # Prometheus, Grafana, Loki
│ └── services/ # Home Assistant, Forgejo, etc.
└── secrets/
└── external-secrets/ # ExternalSecret manifests (secrets staan in Vault)
Brandt het morgen af, dan bouw ik het opnieuw met drie commando’s en een lege schijf. M’n eigen geheugen heb ik nooit vertrouwd om de configuratie vast te houden, dus die stond er sowieso al niet.
Waarom mensen niet overstappen
Als dit zo duidelijk beter is, waarom bestaat click-ops dan nog? Omdat de bezwaren op dat moment echt voelen. Dit hoor ik het vaakst, en dit vind ik ervan.
“Het is langzamer dan klikken.” In het begin wel, ja. De YAML één keer schrijven kost tien minuten tegenover vijf minuten klikken. Maar die YAML schrijf je één keer en hij werkt in elke omgeving. Het klikken doe je elke keer opnieuw met de hand, en je neemt er niks van mee.
“Niet alles kan Infrastructure as Code zijn.” Klopt. Sommige stappen hebben een mens nodig, en dan schrijf je ze eerlijk op in plaats van te doen alsof ze niet bestaan:
# handmatige-stappen.md
## Externe DNS provider opzetten
1. Log in op Cloudflare dashboard
2. Maak API token met Zone:Edit permissie
3. Voeg token toe aan Vault op `secret/cloudflare/api-token`
Opmerking: Dit is handmatig omdat Cloudflare geen
organisatie-level tokens ondersteunt via API.
Laatst uitgevoerd: 2026-03-15 door @tom
Het doel is dat niks ongedocumenteerd blijft, ook de delen die je niet kunt automatiseren.
“Mijn team gaat dit niet adopteren.” Vraag ze dan niet om alles in één keer. Automatiseer één pijnlijk ding, doe dat goed, en laat het resultaat het werk doen. De drie uur die je niet aan een server hebt zitten herbouwen. De storing die een code review had gevangen. Dat overtuigt sneller dan welk argument ik ooit heb gemaakt.
Begin waar je bent
Je hoeft je infrastructuur niet te herschrijven om hier te komen. Begin met vastleggen wat er al is.
# Exporteer bestaande Kubernetes resources
kubectl get deployment my-app -o yaml > deployments/my-app.yaml
# Importeer bestaande Terraform resources
terraform import aws_instance.web i-1234567890abcdef0
Geef het daarna wat structuur, gesplitst per omgeving, zodat de verschillen zichtbaar zijn in plaats van verstopt:
infrastructure/
├── base/ # Gemeenschappelijke configuratie
│ ├── namespaces.yaml
│ ├── rbac.yaml
│ └── network-policies.yaml
├── environments/
│ ├── development/
│ │ └── kustomization.yaml # Overlays voor dev
│ ├── staging/
│ │ └── kustomization.yaml
│ └── production/
│ └── kustomization.yaml # Productie-specifieke instellingen
└── applications/
├── api/
├── frontend/
└── database/
En stop met handmatig toepassen. Hang het in CI, of beter, geef het aan GitOps en laat een controller de realiteit gelijk houden met de repo:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: infrastructure
spec:
source:
repoURL: https://gitlab.internal/infrastructure
path: environments/production
destination:
server: https://kubernetes.default.svc
syncPolicy:
automated:
selfHeal: true
Waar dit soevereiniteit raakt
Voor mij gaat dit over meer dan nette YAML. Digitale soevereiniteit gaat veel verder dan hardware bezitten. Het betekent dat je de systemen waar je van afhangt begrijpt en in de hand hebt, en wat je niet ziet heb je niet in de hand.
Infrastructure as Code maakt dat concreet. Van provider wisselen wordt een config-wijziging. Alles vanaf nul opbouwen wordt een commando. En voor elk stuk van de stack kan ik “waarom bestaat dit” beantwoorden zonder te zoeken naar wie er het laatst op de knoppen heeft gedrukt, in de hoop dat die het nog weet.
Dát is het deel waar het mij om gaat. Ik begrijp m’n infrastructuur omdat ik hem heb opgeschreven, regel voor regel, in bestanden die ik kan lezen. Breekt er iets, dan open ik geen console. Ik open de repo, en die vertelt me wat er hoort te draaien en waarom. Voor een brein dat een onverklaard systeem als een lichte jeuk ervaart, was dat middagje opschrijven goed besteed.
