Er is een heel specifiek soort frustratie die ontstaat wanneer je een aanpassing van slechts één regel naar de productie omzet en vervolgens 45 minuten lang moet toekijken hoe je CI/CD-pijplijn zich in alle bochten wringt voordat de implementatie eindelijk plaatsvindt. Je kent het gevoel wel: je hebt een typefout in de tekst van een knop gecorrigeerd, de wijziging opgeslagen, en nu zit je vast en moet je wachten terwijl je pijplijn elke Docker-image opnieuw opbouwt, je volledige testsuite opnieuw uitvoert en voor de duizendste keer deze week de afhankelijkheden scant.
Ik heb het meegemaakt. Sterker nog, ik heb zelf zo'n tergend trage pijplijn gebouwd. Bij mijn vorige bedrijf duurde ons implementatieproces zo lang dat ontwikkelaars hun code pushten, gingen lunchen en terugkwamen om te kijken of het klaar was. We maakten grapjes dat het een feature was – het dwong ons om pauzes te nemen. Maar de realiteit was dat onze trage pijplijn de productiviteit om zeep hielp en ons ontmoedigde om vaak te implementeren.
Het ergste? Ongeveer 80% van die tijd was volkomen onnodige verspilling.
Het probleem: we optimaliseren voor volledigheid, niet voor snelheid
Dit is de typische ontwikkeling van een CI/CD-pijplijn die ik meerdere keren heb gezien (en meegemaakt):
Maand 1: Eenvoudige pijplijn. Tests uitvoeren, bouwen, implementeren. Duurt 5 minuten. Iedereen is tevreden.
Maand 3: Iemand voegt linting toe. Nu 7 minuten. Nog steeds redelijk.
Maand 6: Het beveiligingsteam eist het scannen van afhankelijkheden. Voeg dat toe. Nu 12 minuten.
Maand 9: Voeg end-to-end-tests toe omdat er een bug doorheen is geglipt. 25 minuten.
Maand 12: Voeg code coverage-rapporten, SAST-scanning, containerscanning en compliance-controles toe. 45 minuten en het loopt op.
Elke toevoeging is op zichzelf logisch. Maar niemand vraagt ooit: "Moeten we dit allemaal bij elke commit uitvoeren?" We blijven gewoon controles op elkaar stapelen totdat de pijplijn een bottleneck wordt in plaats van een versneller.
Wat de meeste pijplijnen daadwerkelijk vertraagt
Na het controleren van tientallen CI/CD-pijplijnen heb ik ontdekt dat de grootste tijdverspillers meestal in drie categorieën vallen:
1. Elke keer alles helemaal opnieuw opbouwen
De meeste pijplijnen die ik heb gezien, bouwen Docker-images bij elke commit volledig opnieuw op, zelfs als 90% van de afhankelijkheden niet is veranderd. Ze installeren npm-pakketten opnieuw, downloaden Maven-afhankelijkheden opnieuw en compileren code die al weken niet is aangeraakt.
2. De volledige testsuite sequentieel uitvoeren
Ik heb gezien dat pijplijnen 2.000 unit-tests achter elkaar uitvoerden, wat 15 minuten duurde, terwijl diezelfde tests parallel over meerdere workers in minder dan 3 minuten konden worden uitgevoerd.
3. Het uitvoeren van dure controles die zelden problemen opleveren
Beveiligingsscans en uitgebreide E2E-tests zijn belangrijk, maar heeft je typo-fix echt een volledige penetratietest nodig voordat deze live kan gaan?
Drie praktische oplossingen die je deze week kunt implementeren
Ik laat je drie optimalisaties zien die de pijplijntijden in meerdere projecten waaraan ik heb gewerkt consequent met 50-70% hebben verkort:
Oplossing #1: Laag uw Docker-builds op een slimme manier
In plaats van dit veelvoorkomende patroon:
# Traag Dockerfile - installeert elke keer alles opnieuw
FROM node:18
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
CMD ["npm", "start"]
Structureer je Dockerfile om cache-hits te maximaliseren:
# Snel Dockerfile - bouwt alleen opnieuw wat is gewijzigd
FROM node:18
WORKDIR /app
# Kopieer eerst afhankelijkheidsbestanden
COPY package*.json ./
RUN npm ci --only=production
# Kopieer de broncode als laatste (verandert het vaakst)
COPY . .
RUN npm run build
CMD ["npm", "start"]
Deze eenvoudige herschikking betekent dat als je alleen de applicatiecode hebt gewijzigd, Docker de gecachete npm install-laag hergebruikt in plaats van al je afhankelijkheden opnieuw te downloaden. Ik heb gezien dat deze ene wijziging de bouwtijd terugbracht van 8 minuten naar 2 minuten.
Gebruik voor nog betere resultaten multi-stage builds:
# Build-fase
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Productiefase
FROM node:18-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/index.js"]
Nu is je uiteindelijke image kleiner en zijn je builds sneller, omdat je geen buildtools meeneemt naar de productieomgeving.
Oplossing #2: Paralleliseer je tests op grote schaal
De meeste CI-platforms ondersteunen parallelle uitvoering, maar ontwikkelaars maken er zelden effectief gebruik van. Hier is een GitHub Actions-voorbeeld dat tests parallel uitvoert:
# Trage aanpak - sequentieel
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm test # Alle 2000 tests worden sequentieel uitgevoerd: 15 min
- run: npm run test:e2e # E2E-tests na unit-tests: +10 min
Splits ze in plaats daarvan op:
# Snelle aanpak - parallel
jobs:
unit-tests:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4] # Testen verdeeld over 4 workers
steps:
- uses: actions/checkout@v3
- run: npm test -- --shard=${{ matrix.shard }}/4
e2e-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm run test:e2e
lint:
runs-on: ubuntu-latest
stappen:
- gebruikt: actions/checkout@v3
- uitvoeren: npm run lint
Deze taken worden gelijktijdig uitgevoerd in plaats van op elkaar te wachten. De totale pijplijntijd wordt de duur van je traagste taak, niet de som van alle taken.
Oplossing #3: Gebruik slimme voorwaardelijke uitvoering
Niet elke wijziging vereist elke controle. Implementeer op pad gebaseerde activering:
# Voer alleen dure controles uit wanneer relevante bestanden veranderen
jobs:
security-scan:
runs-on: ubuntu-latest
# Voer alleen uit bij wijzigingen in afhankelijkheden of in de hoofdbranch
if: |
contains(github.event.head_commit.modified, 'package.json') ||
github.ref == 'refs/heads/main'
steps:
- run: npm audit
- run: docker scan
deploy:
runs-on: ubuntu-latest
# Implementeer alleen vanuit de hoofdtak
if: github.ref == 'refs/heads/main'
steps:
- run: ./deploy.sh
Voor feature-branches met uitsluitend wijzigingen in de documentatie, sla tests volledig over:
jobs:
test:
if: |
!contains(github.event.head_commit.message, '[skip ci]') &&
!startsWith(github.event.head_commit.message, 'docs:')
De resultaten: van 45 minuten naar 12 minuten
Toen ik deze drie optimalisaties toepaste op een echte projectpijplijn, gebeurde het volgende:
Voorheen:
- Docker-image bouwen: 8 minuten
- Tests sequentieel uitvoeren: 15 minuten
- Beveiligingsscans: 12 minuten
- E2E-tests: 10 minuten
- Totaal: 45 minuten
Na:
- Docker-image bouwen (in cache): 2 minuten
- Tests uitvoeren (4 parallelle workers): 4 minuten
- Beveiligingsscans (voorwaardelijk): 0-6 minuten
- E2E-tests (parallel met unit-tests): 0 minuten (overlappend)
- Totaal: 6-12 minuten, afhankelijk van de wijzigingen
Dat is een vermindering van 60-73% in de implementatietijd, bereikt in ongeveer een halve dag werk.
Het grotere plaatje: snelle pijplijnen maken betere werkwijzen mogelijk
Dit is waarom het om meer gaat dan alleen tijdwinst: wanneer je pijplijn snel is, willen ontwikkelaars daadwerkelijk vaak implementeren. Als het 45 minuten duurt, bundel je wijzigingen om het gedoe te vermijden, wat ironisch genoeg implementaties riskanter en moeilijker te debuggen maakt.
Snelle pijplijnen maken het volgende mogelijk:
- Het vol vertrouwen implementeren van kleine wijzigingen
- Snelle hotfix-uitrol wanneer de productie uitvalt
- Daadwerkelijke continue implementatie in plaats van "implementeren wanneer we zin hebben om te wachten"
- Grotere tevredenheid onder ontwikkelaars (serieus, dit is belangrijk)