“We willen voor november naar Kubernetes migreren.”

Het was september. De klant was een e-commerce bedrijf. Hun grootste sales event van het jaar — Black Friday — was eind november. Ik zei nee. Ze vroegen of ik iemand kende die het misschien toch zou willen doen.

Dat deed ik. Een collega platform engineer — iemand die ik respecteer, zeer capabel. Ik maakte de introductie, maar waarschuwde hem voor de timeline. Hij nam de opdracht aan, documenteerde dezelfde zorgen die ik had, liet ze aftekenen. De klant ging toch door.

Wat er daarna gebeurde is zijn verhaal, gedeeld met toestemming. Het is een waarschuwend verhaal over waarom je nooit naar Kubernetes moet migreren zonder goede resource metrics. Laat me vertellen wat hij heeft meegemaakt, en daarna bespreken hoe je dit wel goed doet.

De Black Friday Ramp

De klant had een monolithische PHP applicatie die draaide op vier dedicated servers. Twee app servers, één database server, één voor Redis en background jobs. Simpel, stabiel, saai — het had Black Friday traffic drie jaar lang aan kunnen.

Maar de CTO was naar een conferentie geweest. Kubernetes was de toekomst. Microservices waren de toekomst. Ze wilden alles containerizen en deployen naar EKS voor Black Friday om “beter te kunnen schalen.”

Wat context: de CTO was nieuw, specifiek binnengehaald om innovatie te pushen. Het bedrijf had bijna tien jaar op dezelfde tech stack gedraaid — winstgevend, maar stilstaand. De board wilde modernisering, en de CTO had een win nodig om te bewijzen dat de investering het waard was.

Maar hier is waarom de timeline geen pure overmoed was: hun legacy hosting contract liep af op 1 december. De provider was overgenomen, de nieuwe eigenaren waren het platform aan het uitfaseren, en ze wilden niet verlengen. De opties waren: migreren voor Black Friday, of maand-tot-maand op noodhosting tegen 3x de kosten terwijl je probeert te migreren in december — de enige maand waarin code freezes alles moeilijker maken en elke outage catastrofaal is.

De CTO deed de rekensom. Een risicovolle migratie in oktober met twee weken om te stabiliseren voor Black Friday versus een zekere cash drain en een nog risicovollere december migratie. De CFO tekende af. De CEO tekende af. Het was niet roekeloos — het was een berekend risico. Ze rekenden alleen verkeerd omdat ze de data niet hadden om goed te rekenen.

De Waarschuwingssignalen Die Mijn Collega Aankaartte

  1. Geen metrics. Ze hadden basic Nagios monitoring — CPU, memory, disk. Geen applicatie-level metrics. Geen request latency histogrammen. Geen begrip van hoe resources correleerden met traffic.

  2. Geen load testing. Ze hadden de applicatie nooit load getest. Hun enige datapunt was “het overleefde vorig jaar Black Friday.”

  3. Geen tijd voor goede sizing. Kubernetes resource requests en limits vereisen data. Zonder metrics zouden ze gokken.

  4. Geen rollback plan. De oude infrastructuur werd ontmanteld. Als Kubernetes faalde, was er niets om naar terug te vallen.

Hij documenteerde dit allemaal. Hij adviseerde om een manier te vinden om het hosting contract te verlengen, zelfs tegen hogere kosten, om tijd te kopen voor goede voorbereiding. Ze weigerden. De rekensom was gemaakt, de executives hadden afgetekend, en de beslissing stond vast.

Wat Ze Deployen

Zonder goede metrics maakten ze educated guesses gebaseerd op de huidige server specs:

# Wat ze deployen (DOE DIT NIET)
resources:
  requests:
    memory: "512Mi"
    cpu: "250m"
  limits:
    memory: "1Gi"
    cpu: "500m"

Je vraagt je misschien af: waarom überhaupt CPU limits zetten? Mijn collega kent de argumenten ertegen. Het antwoord: het platform team van de klant had een policy die vereiste dat alle deployments zowel requests als limits specificeerden voor alle resources. De reden? Bij het vorige bedrijf van de CTO had een cryptominer wekenlang op hun servers gedraaid voordat iemand het doorhad. Het was een van de eerste policies die ze implementeerden na hun aantreden — als alles CPU limits heeft, kan een rogue process geen onbegrensde resources consumeren. Logische redenering, maar het gaat ervan uit dat je weet welke limits je moet zetten. Zonder historische data om tegen in te gaan, en met de tijdsdruk, ging mijn collega erin mee. Achteraf gezien maakte deze policy — gecombineerd met de gegokde waarden — alles erger.

Ze zetten Horizontal Pod Autoscaler (HPA) op gebaseerd op CPU utilization:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-app
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web-app
  minReplicas: 4
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

Zag er redelijk uit. Was het niet.

Black Friday Ochtend

Traffic begon te stijgen om 6 uur ’s ochtends. Om 8 uur waren ze op 3x normale traffic. De HPA deed zijn werk — pods schaalden van 4 naar 12. Alles zag er goed uit.

Toen sloeg 10 uur toe.

Traffic sprong naar 8x normaal. De HPA probeerde te schalen naar 20 pods. Maar dit wist niemand: de applicatie was memory-bound, niet CPU-bound. Elke pod raakte de 1Gi memory limit en werd OOMKilled. De HPA zag lage CPU utilization (omdat pods stierven voordat ze CPU konden gebruiken) en dacht dat alles in orde was.

Nieuwe pods zouden starten, de PHP opcache laden, opwarmen, traffic gaan serveren, zonder memory komen te zitten, sterven. Herhaal. Ze hadden 20 pods die constant door CrashLoopBackOff cycleden.

Het Echte Probleem

De memory footprint van de applicatie varieerde wild op basis van het type request:

  • Browse categorie: ~200MB
  • Bekijk product: ~300MB
  • Voeg toe aan winkelwagen: ~400MB
  • Checkout: ~800MB+

Op Black Friday was checkout traffic 5x normaal. Elke pod probeerde checkout requests te handelen die 800MB nodig hadden, maar ze hadden ze gelimiteerd tot 1Gi zonder marge.

Erger nog: ze hadden CPU requests te laag gezet. Wanneer pods wel overleefden, werden ze gethrottled. Response times gingen van 200ms naar 8 seconden. Gebruikers begonnen pagina’s te refreshen. Meer requests. Meer memory pressure. Doodsspiraal.

De Nasleep

  • 4 uur degraded service tijdens piek Black Friday uren
  • Geschat verloren omzet: €240.000
  • Emergency scale-up van de node pool (wat 20 minuten duurde om te provisionen)
  • Handmatige interventie om memory limits te verhogen (wat een redeployment vereiste)
  • Post-incident review met zeer ongemakkelijke executives

De applicatie was prima. Kubernetes was prima. De configuratie was het probleem — en de configuratie was fout omdat ze de data niet hadden om het goed te doen.

De Juiste Manier: Data-Driven Resource Sizing

Laat me je tonen hoe dit gedaan had moeten worden.

Stap 1: Instrumenteer Voordat Je Migreert

Je kunt Kubernetes resources niet sizen zonder het resource verbruikspatroon van je applicatie te begrijpen. Minimaal heb je nodig:

Memory metrics:

  • Baseline memory gebruik (idle)
  • Working set memory onder load
  • Piek memory tijdens traffic spikes
  • Memory verbruik per request type (als het varieert)

CPU metrics:

  • CPU utilization onder normale load
  • CPU utilization tijdens pieken
  • Request latency bij verschillende CPU levels
  • Of de app CPU-bound of IO-bound is

Request patronen:

  • Requests per seconde (RPS)
  • Request latency percentielen (p50, p95, p99)
  • Traffic patronen per tijd van de dag, dag van de week
  • Historische piek traffic events

Stap 2: Load Test in Isolatie

Voordat je Kubernetes aanraakt, load test je applicatie op een enkele container:

# Draai de app in een container met ruime limits
docker run -d --name load-test \
  --memory 4g \
  --cpus 2 \
  -p 8080:8080 \
  your-app:latest

# Monitor resource gebruik tijdens load test
docker stats load-test

Gebruik een load testing tool om realistische traffic te simuleren:

# Voorbeeld met k6
k6 run --vus 100 --duration 10m load-test.js

Noteer:

  • Memory high-water mark
  • CPU utilization percentage
  • Waar performance degradeert

Stap 3: Bereken Requests en Limits

Gebaseerd op je load test data, bereken goede resource specificaties:

Memory:

requests.memory = p95 memory gebruik + 20% buffer
limits.memory = piek memory gebruik + 30% buffer

CPU:

requests.cpu = gemiddelde CPU onder verwachte load
limits.cpu = piek CPU tijdens burst (of weglaten om bursting toe te staan)

Voor de e-commerce klant zou goede sizing zijn geweest:

# Wat ze HADDEN MOETEN deployen
resources:
  requests:
    memory: "1Gi"      # p95 was ~800MB
    cpu: "500m"        # Gemiddelde onder load
  limits:
    memory: "2Gi"      # Piek tijdens checkout was ~1.5GB
    # Geen CPU limit - sta bursting toe

Let op: ik ben voorstander van geen CPU limits zetten. CPU is compressible — als je er doorheen raakt, word je gethrottled. Memory niet — als je er doorheen raakt, word je gekilld. Het Google SRE book en veel practitioners adviseren tegen CPU limits in de meeste gevallen.

Stap 4: Gebruik Vertical Pod Autoscaler voor Rightsizing

De Vertical Pod Autoscaler (VPA) observeert daadwerkelijk resource gebruik en adviseert (of past automatisch toe) betere resource specificaties.

Installeer VPA:

# Clone de autoscaler repo
git clone https://github.com/kubernetes/autoscaler.git
cd autoscaler/vertical-pod-autoscaler

# Installeer VPA componenten
./hack/vpa-up.sh

Maak een VPA resource in “Off” mode om aanbevelingen te krijgen zonder auto-toepassen:

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: web-app-vpa
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web-app
  updatePolicy:
    updateMode: "Off"  # Alleen adviseren, niet toepassen

Na een paar dagen draaien, check aanbevelingen:

kubectl describe vpa web-app-vpa

Output:

Recommendation:
  Container Recommendations:
    Container Name: web-app
    Lower Bound:
      Cpu:     200m
      Memory:  800Mi
    Target:
      Cpu:     450m
      Memory:  1200Mi
    Upper Bound:
      Cpu:     800m
      Memory:  2Gi

De “Target” waarden zijn VPA’s aanbevolen requests. Gebruik deze als je baseline, voeg dan passende marge toe voor limits.

Stap 5: HPA + VPA Samen (Voorzichtig)

HPA en VPA kunnen conflicteren. HPA schaalt horizontaal op basis van metrics; VPA verandert resource requests. Als VPA requests verhoogt, denkt HPA misschien dat pods onderbenut zijn en schaalt down.

De veilige aanpak:

  1. Gebruik VPA in “Off” mode om aanbevelingen te krijgen
  2. Pas aanbevelingen handmatig toe als je baseline
  3. Gebruik HPA voor horizontale schaling op basis van custom metrics (niet alleen CPU)
  4. Herevalueer periodiek met VPA aanbevelingen

Voor de e-commerce klant zou betere HPA configuratie zijn geweest:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-app
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web-app
  minReplicas: 8    # Hoger minimum voor Black Friday
  maxReplicas: 50   # Meer marge
  metrics:
  - type: Resource
    resource:
      name: memory  # Schaal op memory, niet alleen CPU
      target:
        type: Utilization
        averageUtilization: 70
  - type: Pods
    pods:
      metric:
        name: http_requests_per_second  # Custom metric
      target:
        type: AverageValue
        averageValue: "100"

De Checklist: Voordat Je Migreert

Voordat je naar Kubernetes migreert, beantwoord deze vragen met data:

VraagDatabron
Wat is de memory baseline van de applicatie?Monitoring (Prometheus, Datadog, etc.)
Wat is piek memory onder load?Load testing
Is de app CPU-bound of memory-bound?Load testing + profiling
Wat is p99 latency bij verwachte load?Load testing
Wat is het traffic patroon (dagelijks, wekelijks, seizoensgebonden)?Historische metrics
Wat is de hoogste traffic spike in het afgelopen jaar?Historische metrics
Hoe gedraagt de app zich bij 2x, 5x, 10x load?Load testing

Als je deze vragen niet kunt beantwoorden, ben je niet klaar om te migreren.

Wat Ze Anders Deden de Tweede Keer

Na de Black Friday ramp ging de klant alsnog akkoord met de dure noodhosting optie die ze hadden willen vermijden. Drie keer de kosten, maar het kocht ze tijd. Mijn collega gebruikte december om de migratie goed te doen:

  1. Alles geïnstrumenteerd. Prometheus, applicatie metrics, custom metrics voor checkout flow.

  2. Uitgebreid load getest. Ze simuleerden Black Friday traffic patronen voordat ze migreerden.

  3. Begonnen met ruime resources. Eerst overprovisioned, daarna VPA gebruikt om te rightsizen.

  4. Gevalideerd met canary deployments. Nieuwe configuratie naast de oude gedraaid, gedrag vergeleken.

  5. Rollback optie behouden. De noodhosting bleef 30 dagen beschikbaar als fallback na de migratie.

De migratie eind december — na de kerstdrukte — was saai. Niets brak. Dat is het doel.

Conclusie

Kubernetes is krachtig, maar het is geen magie. Het zal je scaling problemen niet oplossen als je de resource requirements van je applicatie niet begrijpt. En je kunt die requirements niet begrijpen zonder data.

Het migratie playbook:

  1. Instrumenteer eerst. Verzamel minstens 2-4 weken metrics voordat je migratieplanning begint.
  2. Load test grondig. Simuleer je worst-case scenarios.
  3. Size conservatief. Overprovision initieel, dan rightsizen met data.
  4. Gebruik VPA voor inzichten. Zelfs als je niet auto-applied, de aanbevelingen zijn waardevol.
  5. Houd een rollback plan. Tot je bewezen hebt dat de nieuwe setup piek load aankan.

Wees niet de engineer die twee weken voor Black Friday naar Kubernetes deployt met gegokde resource limits. Mijn collega heeft die les op de harde manier geleerd — en ik ben dankbaar dat hij het verhaal deelde zodat anderen dat niet hoeven.

Neem de tijd om het goed te doen. Je toekomstige zelf — en je on-call rotation — zal je dankbaar zijn.


Gerelateerde posts: