Bonnes pratiques Gitlab CI

Logo Gitlab

À E-voyageurs Technologies, je travaille au sein d'une équipe en charge de l'usine logicielle, qui administre depuis plusieurs années une instance Gitlab self-hosted.

Cet article contient quelques-unes de nos recommandations à l'intention des utilisateurs de notre Gitlab, ayant pour but à la fois améliorer les performances de leurs pipelines, et limiter leur impact en termes de ressources sur cette instance Gitlab partagée entre des dizaines d'équipes. Un dernier volet rassemble quelques points de sécurité.

Ces conseils sont essentiellement issus de mon expérience au fil des années, mais recoupent également des recommandations officielles de Gitlab. J'espère qu'en les partageant ici ils pourront être utiles à la communauté qui gravite autour de ce bel outil. Merci à Christophe, Etienne, Gilles, Jérôme & Raphaël pour la relecture.

Shallow cloning avec GIT_DEPTH=1

Afin de raccourcir vos temps d'exécution et limiter la quantité de données transitant sur le réseau, vous pouvez définir cette variable d'environnement afin que Gitlab ne récupère qu'un seul commit d'historique de votre repository avant d'exécuter votre pipeline.

Cette variable peut-être définie dans le .gitlab-ci.yml, au niveau des variables CI/CD de votre repo, ou même plus largement au niveau des variables de CI/CD de votre groupe Gitlab.

Plus d'information ici : https://docs.gitlab.com/ee/ci/yaml/#configure-runner-behavior-with-variables

De même, si vous clonez manuellement un repo git dans vos pipelines, pensez à employer git clone --depth 1.

Pour les très gros repos, Gitlab suggère quelques optimisations possibles, notamment en configurant GIT_CLEAN_FLAGS & GIT_FETCH_EXTRA_FLAGS : https://docs.gitlab.com/ee/ci/large_repositories/

Cache

Il est vraiment simple et très efficace de mettre en cache entre vos builds successifs les dépendances de vos applications rapatriées par votre outil favori (Maven, Gradle, Go, npm, pip...) :

default:
  cache:
    paths:
      - .cache/ansible
      - .cache/pip
      - .gradle/caches
      - .gradle/wrapper
      - .m2/repository
      - cache/bundler  # Ruby
      - node_modules

Le dossier de cache doit obligatoirement être relatif au dossier de build de votre pipeline. Pour certaines technos, cela requiert de configurer le dossier de cache employé par votre package manager via des variables d'environnement :

variables:
  ANSIBLE_ROLES_PATH: "$CI_PROJECT_DIR/.cache/ansible"
  GEM_PATH: "$CI_PROJECT_DIR/cache/bundler/ruby/2.1.0"
  GRADLE_HOME: "$CI_PROJECT_DIR"
  MAVEN_OPTS: "-Duser.home=$CI_PROJECT_DIR"
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"

Définir une cache:key est souvent une bonne idée, ainsi éventuellement qu'une CACHE_FALLBACK_KEY. Pour plus de détails, vous pouvez vous référer aux Good caching practices & Common use cases for caches de la documentation Gitlab CI.

Notez également que le niveau de compression du cache est configurable via la variable CACHE_COMPRESSION_LEVEL, et la vitesse de compression via FF_USE_FASTZIP.

Enfin, pour approfondir le sujet, vous pouvez lire la manière dont Gitlab optimise sa gestion de cache pour son propre projet git : https://docs.gitlab.com/ee/development/pipelines.html#caching-strategy

Artefacts

Lorsque vous faites transiter des artefacts entre différentes étapes de vos pipelines, pensez à toujours configurer une courte durée de rétention maximale afin de limiter l'empreinte disque de vos pipelines :

artifacts:
  paths:
    - target/myapp.jar
  expire_in: 1 day

Notez également que le niveau de compression des artefacts est configurable via la variable ARTIFACT_COMPRESSION_LEVEL, et la vitesse de compression via FF_USE_FASTZIP.

Enfin, pour approfondir le sujet, vous pouvez lire la manière dont Gitlab optimise sa gestion d'artefacts pour son propre projet git : https://docs.gitlab.com/ee/development/pipelines.html#artifacts-strategy

Retry

Si certaines étapes de votre pipeline ont tendance à tomber en échec de temps en temps, par exemple à cause d'une instabilité irrégulière d'un service externe, vous pouvez configurer cette étape pour se relancer un 2e fois en cas d'échec :

retry: 1

Plus de détails ici : https://docs.gitlab.com/ee/ci/yaml/#retry

⚠️ ATTENTION : retry peut avoir des effets secondaires néfastes, comme ajouter de la charge sur Gitlab ou de marteler inutilement des services externes. Ne le mettez en place que si cela résout effectivement des instabilités temporaires. Bien souvent, résoudre le problème "de fond" de l'instabilité sera la meilleure solution.

Évitez les déploiements depuis les forks

Il est courant d'effectuer certaines étapes d'une pipeline uniquement sur la branche master / main, comme la publication de livrables (telle une image Docker) ou le déclenchement d'action de déploiement.

La syntaxe suivante est alors souvent employée :

only:
  - master

Néanmoins, le risque avec cette règle ci-dessus est que ces actions seront tout même effectuées sur les branches master / main des forks de votre repo !

N'importe quel contributeur de votre projet peut alors déclencher un déploiement par mégarde en déclenchant la pipeline de leur fork. Bien sûr, dans de nombreux cas ce déploiement sera un échec si, par exemple, votre pipeline nécessite des variables auxquels les forks n'ont pas accès.

Néanmoins, pour éviter tout risque, vous pouvez employer la syntaxe suivante qui indique précisément le groupe Gitlab du repo autorisé à effectuer les actions de déploiement :

rules:
  - if: '$CI_PROJECT_PATH == "group-name/repo-name"'

Sécurité - Lockez vos versions pour rendre vos builds reproductibles

Selon le package manager que vous employez, les outils varient, mais l'idée est la même : versionner sous git votre arbre complet de dépendances, avec un fichier de lock, pour que la construction de votre livrable dans une version donnée ne puisse jamais changer si une de vos dépendances est mise à jour plus tard sur un registry.

Plus d'infos sur l'approche générale :

Logo Reproductible Builds

  • Avec Maven (Java) :

  • Avec npm (NodeJS) :

    • Invoquez "npm ci" plutôt que "npm install" dans vos pipelines afin d'employer le package-lock.json et d'assurer que vos builds sont toujours identiques (article explicatif en anglais)
    • Si possible, lorsque vous publiez vos propres packages, employez des @scopes. Si vous publiez vos packages "scopés" sur un registry privé, pensez à créer également le @scope sur https://npm.org (sans pour autant y publier de package), pour évitez tout risque de Dependency Confusion.
    • Invoquez npm audit dans vos pipelines pour détecter d'éventuelles vulnérabilités dans vos dépendances : en cas de code de retour non 0, la pipeline doit s'interrompre. retire constitue un outil alternatif ayant la même utilité.
  • Avec pip (Python) :

    • Lockez votre arbre de dépendances, par exemple avec pip-compile
    • Invoquez safety dans vos pipelines pour détecter d'éventuelles vulnérabilités dans vos dépendances : en cas de code de retour non 0, la pipeline doit s'interrompre
    • Invoquez le linter de sécurité bandit dans vos pipelines : en cas de code de retour non 0, la pipeline doit s'interrompre
  • Avec CocoaPods (iOS) :

    • Lockez les versions de vos pods, et versionnez votre Podfile.lock sous git (documentation officielle)
    • Pour évitez une Dependency Confusion, déclarez directement l’URL du repo git qui héberge le pod au niveau de l’import (documentation officielle). Exemple :
pod 'Org/MyPod', :git => 'git@gitlab.example.com:org/my-pod.git', :tag => '1.2.3'

Sécurité - Services intégrés

Pour finir, nous encourageons nos utilisateurs à intégrer dans leurs pipelines les services suivants, qui font également partie de notre usine logicielle :

  • Ne stockez aucun secret dans votre code source

Ne versionnez dans votre repository git aucun credential sensible : mot de passe, token, certificat privé...

Une solution recommandée pour stocker vos secrets et les employer de manière sécurisée dans vos pipelines est HashiCorp Vault, dont une instance est mise à disposition.

Logo Vault

  • Configurez Renovate Bot sur vos repos Afin que les dépendances de leurs applications soient le plus à jour possible, nous mettons à disposition de nos utilisateurs Renovate Bot.

Logo Renovate

  • Faites analyser votre code par Sonar

Nous mettons à disposition de nos utilisateurs une instance SonarQube, qui intègre un ensemble de checks permettant de détecter des failles de sécurité.

L'image Docker sonar-scanner-cli peut être employée pour soumettre à Sonar vos couvertures de tests depuis une pipeline Gitlab. Voici un exemple d'usage dan un repo Python, une fois les tests exécutés lors d'une étape précédente :

image:
  name: sonarsource/sonar-scanner-cli:4.6
  entrypoint: [""]
script:
  - sonar-scanner -Dsonar.projectKey=... -Dsonar.projectName=... -Dsonar.projectVersion=...
                  -Dsonar.sources=. -Dsonar.host.url=$SONAR_HOST_URL
                  -Dsonar.python.coverage.reportPaths=cover/*coverage*.xml -Dsonar.coverage.exclusions=**/tests/**

Logo SonarQube

Et vous, avez-vous des recommandations à partager autour de Gitlab CI ?