Réduire le coût et la durée de vos pipelines GitLab avec des runners autoscalés
Runners Kubernetes ou spot, cache pnpm et Docker, jobs parallèles avec needs, rules ciblées : réduisez la facture et la durée de vos pipelines GitLab CI.
Une pipeline lente coûte deux fois : en factures d'infrastructure, et en temps d'attente des développeurs. Sur certaines missions, les optimisations décrites ci-dessous nous ont permis de gagner jusqu'à 40 % de temps de build, tout en réduisant la facture de calcul. Aucune n'est exotique : il s'agit surtout d'arrêter de payer pour du calcul inutilisé et de ne plus refaire ce qui a déjà été fait.
Commencer par mesurer
Avant de modifier quoi que ce soit, établissez une base de référence. Sans elle, impossible de savoir si un changement aide.
- La durée médiane et le 90e centile des pipelines de la branche principale.
- Le temps passé en file d'attente avant le démarrage d'un job.
- Les jobs les plus longs et les plus fréquents (ce sont eux qui paient).
- Le coût de calcul mensuel des runners, ramené au nombre de pipelines.
GitLab expose ces données via les analyses CI/CD et l'API. Notez-les avant, comparez après chaque changement, et ne cumulez pas dix modifications d'un coup : vous ne saurez plus laquelle a fonctionné.
Des runners qui s'adaptent à la charge
Des machines allumées en permanence pour des pipelines qui tournent quelques heures par jour sont la première source de gaspillage. L'autoscaling règle le problème : les ressources n'existent que pendant les jobs.
Runners sur Kubernetes
L'exécuteur Kubernetes lance chaque job dans un pod éphémère. Combiné à un autoscaler de nœuds (Cluster Autoscaler ou Karpenter), le cluster grossit quand la file se remplit et se réduit à presque rien la nuit. Voici un extrait de values.yaml pour le chart Helm gitlab-runner :
gitlabUrl: https://gitlab.example.org/
concurrent: 20
checkInterval: 3
rbac:
create: true
runners:
secret: gitlab-runner-token
config: |
[[runners]]
[runners.kubernetes]
namespace = "gitlab-runner"
image = "alpine:3.21"
privileged = false
cpu_request = "500m"
memory_request = "1Gi"
cpu_limit = "2"
memory_limit = "4Gi"
[runners.kubernetes.node_selector]
"workload" = "ci"
[[runners.kubernetes.node_tolerations]]
key = "ci"
operator = "Equal"
value = "true"
effect = "NoSchedule"
Le jeton du runner est lu depuis un secret Kubernetes, jamais écrit dans les values. Le node_selector et la tolérance dirigent les jobs vers un pool de nœuds dédié, facile à réduire à zéro. Définissez toujours des requests et des limits : sans elles, un job gourmand déstabilise ses voisins et l'autoscaler dimensionne mal.
Pour construire des images sans mode privilégié, préférez Kaniko, BuildKit en mode rootless ou Buildah plutôt que Docker-in-Docker.
Instances spot ou préemptibles
Les instances spot (AWS), préemptibles (GCP) ou Spot VMs (Azure) coûtent nettement moins cher que le tarif à la demande, avec une contrepartie : le fournisseur peut les reprendre à tout moment. Les jobs de CI s'y prêtent bien, à condition de :
- les rendre idempotents, donc relançables sans effet de bord ;
- activer les relances automatiques ciblées sur les échecs d'infrastructure ;
- garder un petit pool à la demande pour les jobs critiques, comme le déploiement en production.
default:
retry:
max: 2
when:
- runner_system_failure
- stuck_or_timeout_failure
- scheduler_failure
On ne relance ainsi que les échecs dus à la plateforme, jamais les tests rouges.
Ne plus rien refaire : le cache
Cache des dépendances pnpm
Le magasin de pnpm est un excellent candidat : il est adressé par contenu et partagé entre les projets. En le plaçant dans le cache GitLab, un pnpm install devient presque instantané.
variables:
PNPM_STORE: "$CI_PROJECT_DIR/.pnpm-store"
.node-base:
image: node:22-alpine
before_script:
- corepack enable
- pnpm config set store-dir "$PNPM_STORE"
cache:
key:
files:
- pnpm-lock.yaml
paths:
- .pnpm-store
policy: pull
install-deps:
extends: .node-base
stage: .pre
script:
- pnpm install --frozen-lockfile
cache:
key:
files:
- pnpm-lock.yaml
paths:
- .pnpm-store
policy: pull-push
La clé de cache dérive du fichier pnpm-lock.yaml : le cache n'est renouvelé que si les dépendances changent. Un seul job écrit (pull-push), tous les autres se contentent de lire (pull), ce qui évite les écritures concurrentes.
Cache des layers Docker
Pour les builds d'images, un registre de cache évite de reconstruire les couches inchangées :
docker buildx build \
--cache-from type=registry,ref="$CI_REGISTRY_IMAGE:buildcache" \
--cache-to type=registry,ref="$CI_REGISTRY_IMAGE:buildcache",mode=max \
--tag "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" \
--push .
Ce cache n'est efficace que si le Dockerfile est bien ordonné : installer les dépendances avant de copier le code source, pour que la couche lourde ne soit invalidée que lorsque le lockfile change.
Paralléliser avec needs et cibler avec rules
Par défaut, GitLab attend la fin de tous les jobs d'une étape avant de passer à la suivante. Le mot-clé needs construit un graphe de dépendances : un job démarre dès que ceux dont il dépend sont terminés.
lint:
extends: .node-base
stage: test
needs: ["install-deps"]
script: pnpm lint
unit-tests:
extends: .node-base
stage: test
needs: ["install-deps"]
parallel: 4
script: pnpm test --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL
build:
extends: .node-base
stage: build
needs: ["install-deps"]
script: pnpm build
rules:
- changes:
- src/**/*
- package.json
- pnpm-lock.yaml
Ici, lint, tests et build s'exécutent en même temps, et les tests sont répartis en quatre tranches avec parallel. Les rules avec changes évitent de lancer un build quand seule la documentation a été modifiée. Dans un monorepo, c'est souvent le gain le plus spectaculaire.
Quelques règles complémentaires :
- annuler automatiquement les pipelines redondants avec
interruptible: true; - ne pas lancer les jobs lourds (e2e, scans complets) sur chaque commit de branche, mais en merge request ou de façon planifiée ;
- éviter les pipelines doublons entre branche et merge request avec
workflow:rules.
Des images légères
Une image de 1,5 Go se télécharge à chaque job sur un runner éphémère. Passer à des variantes Alpine ou « slim », construire une image d'outillage dédiée avec uniquement ce qui sert, et héberger ces images dans un registre proche des runners ou derrière un proxy de cache réduit sensiblement le temps de démarrage. C'est un gain discret mais multiplié par chaque job de chaque pipeline.
Vérifier le résultat
Après chaque changement, comparez à votre base de référence : durée médiane, temps d'attente, coût par pipeline. Gardez ce qui fonctionne, retirez ce qui complexifie sans rapporter. L'optimisation de la CI est un travail continu, pas un chantier ponctuel.
Besoin d'un coup de main ?
Dimensionner des runners autoscalés, fiabiliser l'usage du spot ou auditer vos pipelines pour identifier les gains les plus rentables fait partie de nos missions habituelles. Pour en parler, vous pouvez nous contacter.
Un projet similaire ?
Parlons-en : un expert vous répond sous 24 heures ouvrées.
À lire aussi
Application mobile : native, Flutter, PWA ou Tauri 2, comment choisir ?
Native, cross-platform, PWA ou Tauri 2 : comparatif concret selon l'accès matériel, le hors-ligne, la maintenance et la publication sur les stores.
Sécuriser une pipeline GitLab CI/CD sans ralentir vos livraisons
SAST, scan de dépendances et d'images, secrets, SBOM, signature cosign : comment intégrer la sécurité dans GitLab CI/CD sans freiner vos mises en production.
SEO technique avec Nuxt : le guide pour un site rapide et bien référencé
Rendu SSR ou pré-rendu, useSeoMeta, canoniques, sitemap, JSON-LD, Core Web Vitals : les réglages SEO techniques à maîtriser sur un site Nuxt 4.