Optimiser sa CI Gitlab
Les CI c'est bien, une CI optimisée c'est mieux. Voici quelques tips pour l'optimiser. Il y a également des optimisations spécifiques aux différents langages.
Clone superficiel
Sur un gros repo, le git clone peut représenter 30-50% du temps d'un job. GIT_DEPTH: 1 ne récupère que le dernier commit — suffisant dans 95% des cas.
Si un job a besoin de l'historique (génération de changelog, git describe...), on surcharge localement :
Annuler les pipelines obsolètes
Quand on push plusieurs commits rapprochés, les vieux pipelines continuent de tourner pour rien. interruptible: true les annule dès qu'un nouveau pipeline démarre sur la même branche.
On peut aussi le définir job par job. Les jobs de deploy restent souvent non-interruptibles pour éviter un rollback partiel.
Skip Docker
Pour gagner du temps, on peut ne pas utiliser Docker afin de ne pas attendre que le daemon Docker soit prêt. On peut à la place utiliser buildah. La syntaxe est identique à celle de Docker :
Docker Build:
before_script:
# to skip default before_script
- buildah info
- buildah login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
stage: package
image: quay.io/buildah/stable:latest
script:
- buildah build
-t ${CI_REGISTRY_IMAGE}/gprg:prod
-t ${CI_REGISTRY_IMAGE}/gprg:latest .
- buildah push ${CI_REGISTRY_IMAGE}/gprg
Optimisation niveau runner
Le runner GitLab expose des feature flags via des variables CI. À partir de la version 13.6, Fastzip compresse/décompresse bien plus vite les artefacts et le cache.
variables:
FF_USE_FASTZIP: "true"
ARTIFACT_COMPRESSION_LEVEL: "fast"
CACHE_COMPRESSION_LEVEL: "fast"
2 autres feature flags utiles :
FF_TIMESTAMPS: "true"— ajoute des timestamps dans les logs, pratique pour identifier où un job rameFF_ENABLE_BASH_EXIT_CODE_CHECK: "true"— force la détection d'erreur dans les pipes bash (cmd1 | cmd2masque sinon l'exit code decmd1)
variables:
FF_USE_FASTZIP: "true"
FF_TIMESTAMPS: "true"
FF_ENABLE_BASH_EXIT_CODE_CHECK: "true"
ARTIFACT_COMPRESSION_LEVEL: "fast"
CACHE_COMPRESSION_LEVEL: "fast"
Divers
build-job:
stage: build
script:
- echo Hello World > hello-world.txt
artifacts:
expire_in: 1 hour
paths:
- hello-world.txt
test-job:
stage: test
variables:
GIT_STRATEGY: none
script:
- cat hello-world.txt
Cet exemple basique met plusieurs éléments en avant :
GIT_STRATEGY: on ne clone pas le repository Git. Pas besoin de le cloner si on n'en a pas besoin.
On utilise un artifact, expirant dans 1h, pour stocker le résultat d'une commande.
Optimisations par langages
On peut également faire des optimisations de CI en fonction des langages (généralement en jouant sur les caches).
PHP
Dans le cadre de l'utilisation de composer, il peut être intéressant de mettre en cache le résultat d'un composer install. Il faut l'indiquer de cette manière dans le YAML :
Python
De la même manière qu'avec PHP & composer, on peut mettre en cache le .pip d'un projet Python mais également le dossier venv :
# Change pips cache directory to be inside the project directory since we can
# only cache local items.
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
# Pips cache doesnt store the python packages
# https://pip.pypa.io/en/stable/reference/pip_install/#caching
#
# If you want to also cache the installed packages, you have to install
# them in a virtualenv and cache it as well.
cache:
paths:
- .cache/pip
- venv/
D'autres caches sont possibles pour d'autres langages (Go, Ruby...), voir la documentation officielle.
Cache policy
Par défaut, chaque job fait un pull + push du cache. Les jobs de test ou de lint n'ont pas besoin de repusher — ils consomment juste le cache généré par le build.
build-job:
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- vendor/
policy: pull-push # default, génère le cache
test-job:
cache:
key: $CI_COMMIT_REF_SLUG
paths:
- vendor/
policy: pull # consomme uniquement, pas de re-upload
Sur un pipeline avec 5 jobs de test en parallèle, on économise 4 uploads inutiles.
Héritage des variables
Tous les jobs héritent par défaut de toutes les variables globales. Sur un pipeline avec beaucoup de secrets ou de variables d'env, ça pollue et ça ralentit. On peut couper l'héritage et être explicite :
test-job:
inherit:
variables: false # n'hérite d'aucune variable globale
variables:
NODE_ENV: test # uniquement ce dont on a besoin
DAG — parallélisme réel
Les stages s'exécutent en séquence même si les jobs sont indépendants. Avec needs:, on court-circuite les stages et on lance un job dès que ses dépendances sont terminées.
lint:
stage: test
script: eslint .
unit-tests:
stage: test
script: jest
build:
stage: build
needs: [lint, unit-tests] # démarre sans attendre les autres jobs du stage test
script: docker build .
deploy:
stage: deploy
needs: [build]
script: kubectl apply -f k8s/
needs: [] (liste vide) = le job démarre immédiatement, sans attendre aucun autre job.