Aller au contenu
CI/CD & Automatisation medium

Services CI et cache avancé GitLab

17 min de lecture

logo gitlab

Vos tests d'intégration ont besoin d'une vraie base de données, pas d'un mock ? GitLab CI/CD lance des services (PostgreSQL, Redis, Elasticsearch…) comme conteneurs adjacents à votre job. Votre code s'y connecte comme s'il était en local. Combiné à un cache avancé bien configuré, vos pipelines sont à la fois complets et rapides.

Le module Artifacts et cache (V1-06) couvrait les bases. Ce guide approfondit les cas avancés : services Docker, stratégies de clé de cache, policies et cache distribué.

  • Lancer des services (PostgreSQL, Redis, MongoDB) dans un job CI
  • Configurer la connexion entre le job et le service (hostname, variables)
  • Maîtriser les policies de cache : pull-push, pull, push
  • Concevoir des clés de cache robustes (fichier de lock, combinaisons)
  • Gérer le cache distribué (S3) pour les runners autoscalés
  • Diagnostiquer les problèmes de services et de cache

Vous avez des tests unitaires qui passent, mais vos tests d'intégration échouent parce qu'ils ont besoin d'une base de données. Vous pourriez mocker, mais les tests ne seraient pas réalistes. Vous avez besoin d'un PostgreSQL (ou Redis, ou MongoDB) réel dans votre pipeline.

En parallèle, votre cache est imprévisible : parfois il est là, parfois il est vide. Les builds prennent 3 minutes avec cache, 12 sans. Vous voulez comprendre pourquoi et le rendre fiable.

  • Vos tests d'intégration ont besoin d'une base SQL ou NoSQL
  • Vous testez un microservice qui dépend de Redis pour les sessions
  • Votre app a un healthcheck qui vérifie la connexion à la base
  • Votre cache disparaît quand le runner change
  • Vous avez des runners autoscalés et le cache local ne survit pas

Quand vous déclarez services: dans un job, GitLab Runner :

  1. Démarre les conteneurs de services avant le job
  2. Attend que les services soient prêts (healthcheck)
  3. Crée un réseau partagé entre le job et les services
  4. Rend chaque service accessible via un hostname dérivé du nom d'image

L'exemple le plus courant : des tests d'intégration avec PostgreSQL.

.gitlab-ci.yml
test:integration:
stage: test
image: node:20
services:
- name: postgres:16-alpine
alias: db
variables:
POSTGRES_DB: testdb
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpass
variables:
DATABASE_URL: "postgresql://testuser:testpass@db:5432/testdb"
script:
- npm ci
- npm run test:integration

Clé de compréhension : le service est accessible via le hostname db (défini par alias). Sans alias, le hostname serait postgres (dérivé du nom d'image, en remplaçant / et : par -).

Redis ne demande aucune variable d'environnement pour démarrer, contrairement à PostgreSQL : l'image officielle accepte les connexions sans authentification dès le lancement. La seule chose à retenir de cet exemple est donc la construction de l'URL de connexion, qui reprend l'alias du service et le port exposé par défaut, 6379. Si vous voulez un mot de passe, il faut le passer en commande au service, pas en variable.

test:integration:
stage: test
image: python:3.12@sha256:7ad6d21a25a94b2c00e685e82c2fd298de814353d9ee0e3f7f2cd4fca063df60
services:
- name: redis:7-alpine
alias: cache
variables:
REDIS_URL: "redis://cache:6379"
script:
- pip install -r requirements.txt
- pytest tests/integration/

Votre application dépend de PostgreSQL et Redis ? Déclarez les deux :

test:integration:
stage: test
image: node:20@sha256:8f693eaa7e0a8e71560c9a82b55fd54c2ae920a2ba5d2cde28bac7d1c01c9ba5
services:
- name: postgres:16-alpine
alias: db
variables:
POSTGRES_DB: app_test
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
- name: redis:7-alpine
alias: cache
- name: elasticsearch:8.12.0
alias: search
variables:
discovery.type: single-node
xpack.security.enabled: "false"
variables:
DATABASE_URL: "postgresql://app:secret@db:5432/app_test"
REDIS_URL: "redis://cache:6379"
ELASTICSEARCH_URL: "http://search:9200"
script:
- npm ci
- npm run test:integration

Les services ne démarrent pas instantanément. PostgreSQL a besoin de quelques secondes pour être prêt. Deux approches :

test:integration:
services:
- name: postgres:16-alpine
alias: db
before_script:
- apt-get update && apt-get install -y postgresql-client
- |
for i in $(seq 1 30); do
pg_isready -h db -p 5432 && break
echo "Waiting for PostgreSQL... ($i/30)"
sleep 1
done
- npm ci
script:
- npm run test:integration

La ligne qui décide de tout dans ce tableau est la durée de vie. Le cache est un mécanisme best effort : GitLab ne garantit jamais qu'il sera là au job suivant, et un pipeline qui échoue quand le cache est vide est un pipeline cassé, pas un pipeline lent. Les artifacts, eux, sont conservés le temps déclaré par expire_in et constituent le seul moyen fiable de transmettre un résultat d'un job à l'autre. Si vous hésitez entre les deux, posez-vous cette question : est-ce que le job suivant échoue sans ce fichier ? Si oui, c'est un artifact.

CacheArtifacts
ButAccélérer les builds suivantsTransmettre des résultats entre jobs
Durée de vieBest effort (peut disparaître)Garanti pendant expire_in
StockageLocal au runner (ou distribué)Stocké par GitLab (object storage)
Cas d'usagenode_modules/, .pip/, vendor/dist/, rapports JUnit, binaires

Détails dans Artifacts et cache (V1-06)

La clé de cache détermine quand le cache est réutilisé ou invalidé.

cache:
key:
files:
- package-lock.json
paths:
- node_modules/

Le meilleur choix dans la majorité des cas. Le cache est invalidé uniquement quand les dépendances changent. Deux branches avec le même package-lock.json partagent le cache.

Quand le cache exact n'existe pas, GitLab peut rechercher un cache de repli. Le point à comprendre avant d'écrire la moindre ligne : fallback_keys contient des noms de clés littéraux, pas des motifs. Une clé de repli ne remonte donc un cache que si un autre pipeline en a réellement poussé un sous ce nom exact. C'est la raison pour laquelle l'exemple ci-dessous n'utilise pas key.files : une clé calculée par hachage produit un nom imprévisible, sur lequel aucune clé de repli ne pourra jamais tomber juste.

cache:
key: "npm-$CI_COMMIT_REF_SLUG"
fallback_keys:
- "npm-$CI_DEFAULT_BRANCH"
- "npm-default"
paths:
- node_modules/

L'ordre de recherche, depuis une branche feature/paiement :

  1. npm-feature-paiement, la clé exacte de la branche courante
  2. npm-main, écrite par le dernier pipeline de la branche par défaut
  3. npm-default, filet de sécurité qu'un job de maintenance doit alimenter

La deuxième entrée est celle qui rend le mécanisme utile : une branche fraîchement créée récupère le node_modules/ de la branche par défaut au lieu de repartir d'un cache vide.

La policy répond à une question de coût, pas de correction : chaque écriture de cache compresse un répertoire et l'envoie au stockage, ce qui prend du temps même quand rien n'a changé. Sur un pipeline où cinq jobs déclarent le même node_modules/, laisser la valeur par défaut pull-push partout revient à compresser et téléverser cinq fois le même contenu. Basculer les jobs consommateurs en pull supprime ces écritures inutiles et élimine au passage les écrasements concurrents entre jobs parallèles.

PolicyComportementUsage
pull-pushLit le cache au début, écrit à la fin (défaut)Le job qui alimente le cache (souvent le premier)
pullLit le cache, n'écrit pasLes jobs suivants (évite les écritures concurrentes)
pushN'utilise pas le cache existant, écrit à la finRegénérer un cache corrompu
Exemple : un job alimente, les autres consomment
lint:
stage: lint
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
policy: pull-push # Premier job → alimente le cache
script:
- npm ci
- npm run lint
test:
stage: test
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
policy: pull # Job suivant → lecture seule
script:
- npm test
build:
stage: build
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
policy: pull # Lecture seule aussi
script:
- npm run build

Un job peut déclarer plusieurs caches indépendants :

test:python:
cache:
- key:
files:
- requirements.txt
paths:
- .pip/
- key:
files:
- package-lock.json
paths:
- node_modules/
script:
- pip install -r requirements.txt --cache-dir .pip
- npm ci
- npm test

Par défaut, le cache est stocké localement sur le runner. Si vos runners sont autoscalés (instances éphémères), le cache disparaît avec l'instance. La solution : un cache distribué sur S3 (ou compatible S3).

Configuration dans le runner :

config.toml
[[runners]]
[runners.cache]
Type = "s3"
Shared = true
[runners.cache.s3]
ServerAddress = "s3.amazonaws.com"
BucketName = "gitlab-ci-cache"
BucketLocation = "eu-west-1"
AccessKey = "AKIAIOSFODNN7EXAMPLE"
SecretKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"

Les deux dernières lignes sont les identifiants d'exemple publiés par AWS, elles ne donnent accès à rien. En production, ne recopiez surtout pas ce schéma : config.toml est un fichier en clair sur le runner, lisible par quiconque obtient un shell dessus. Sur EC2 ou EKS, retirez AccessKey et SecretKey et posez AuthenticationType = "iam" pour que le runner utilise le rôle IAM de l'instance. Sur un stockage compatible S3 sans rôle disponible, injectez les clés par variables d'environnement du service runner et restreignez la politique du bucket au strict nécessaire.

Le cache peut devenir volumineux. Point important : il n'existe pas de point d'API REST pour vider le cache d'un runner. La documentation GitLab ne documente que deux moyens, et le bouton de l'interface est le plus direct.

Dans l'interface : Build > Pipelines, puis Clear runner caches (bouton en haut à droite). Au commit suivant, les jobs repartent sur un emplacement de cache neuf.

Le second moyen est de changer la clé de cache, ce qui déplace le cache vers une nouvelle entrée sans supprimer l'ancienne. C'est ce que fait le job planifié ci-dessous avec sa clé force-refresh et sa policy push.

Ne confondez pas avec la suppression des artifacts, qui relève d'un tout autre point d'API : DELETE /projects/:id/artifacts supprime les artifacts éligibles d'un projet, et DELETE /projects/:id/jobs/:job_id/artifacts ceux d'un job précis. Aucun des deux ne touche au cache.

Pour un nettoyage régulier, ajoutez un job scheduled :

clean:cache:
stage: maintenance
cache:
key: "force-refresh"
policy: push
paths:
- node_modules/
script:
- rm -rf node_modules/
- npm ci
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"

Avant de parcourir ce tableau, ouvrez les logs du job et remontez au tout début : le runner y écrit une section repliée par service, avec le résultat de la vérification de port. Un message WARNING: Service ... probably didn't start properly vous oriente immédiatement vers les trois premières lignes du tableau, alors qu'une erreur de connexion sans ce message pointe plutôt vers un problème d'alias ou de variables. Les quatre dernières lignes concernent le cache, où le symptôme visible est toujours une durée de build qui varie sans raison apparente.

SymptômeCause probableSolution
Service connection refusedLe service n'est pas prêtAjouter un wait script ou augmenter wait_for_services_timeout
Hostname du service introuvableMauvais alias ou syntaxe d'imageVérifier alias: et le hostname utilisé dans la connexion
Cache vide sur une nouvelle branchePas de fallback configuréAjouter fallback_keys vers la branche par défaut
Cache incohérent entre jobsÉcriture concurrenteUtiliser policy: pull sur tous les jobs sauf le premier
Cache jamais invalidéClé statique (key: "npm-cache")Utiliser key.files avec un fichier de lock
Out of memory avec servicesTrop de conteneursRéduire les services ou augmenter les ressources du runner
Service MySQL Access deniedVariables d'env non passées au serviceDéclarer les variables sous services[].variables:
  • Les services CI lancent des conteneurs adjacents (PostgreSQL, Redis…) accessibles par hostname
  • Le hostname est l'alias du service ou le nom d'image simplifié
  • Chaque service a ses propres variables (POSTGRES_DB, REDIS_URL…)
  • La clé de cache basée sur un fichier de lock est la stratégie la plus fiable
  • Utilisez policy: pull-push sur le premier job et pull sur les suivants
  • Les fallback keys évitent les cache miss sur les nouvelles branches
  • Le cache distribué (S3/MinIO) est indispensable pour les runners autoscalés
  • Cache = best effort (accélérateur), artifacts = garanti (résultats)

Mettez en oeuvre ces optimisations dans le Lab 12, Accélérer pipeline.

Dix questions pour vérifier que les deux mécanismes du guide sont acquis : le nommage réseau des services et le couple clé/policy du cache. Le seuil de réussite est fixé à 70 %. Si vous butez sur les questions de policy, relisez l'exemple à trois jobs plus haut, c'est le point qui coûte le plus de temps de pipeline en pratique.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

10 questions
5 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn