Nicolas Le Borgne

Développeur

Build reproductible localement

Le 13 mars 2022

Quand les tests sont verts en local et rouge en CI, on se retrouve dans une situation délicate ... ce serait dommage de commiter n fois en espérant mettre le doigt sur le problème, ou pire, dire que "ça marche sur mon poste !". 😅

Il peut être intéressant d'utiliser un outil pour envelopper le processus de build afin de se donner la capacité de le lancer en local, ainsi une erreur serait reproductible. Pour les projets construisant des artefacts comme des binaires, l'outil permettra en plus aux développeurs de les construire de manière iso sur leur poste !

* ba3f522 fix: debug ci
* 21a5524 fix: debug ci
* 4546123 fix: debug ci
* 2377c0a fix: debug ci
* 30af397 fix: debug ci
* 86ea9b2 fix: debug ci
* b18f30f fix: debug ci
* ... 😰

Le cahier des charges

L'outil devrait généralement être capable de :

  • Lancer des commandes dans un environnement isolé
  • Gérer du cache
  • Gérer les dépendances privées facilement
  • Lancer des services dépendants, avec une configuration indépendante
  • Récupérer des artéfacts

Quelles solutions ?

J'ai pu rencontrer les solutions suivantes :

Le script bash prend du temps à écrire, à maintenir et est spécifique. 🤦‍♂️

Wercker est un bon outil mais il est lent et plus maintenu. 🤦‍♂️

Comme Wercker, le runner gitlab est intéressant, mais il ne prend en compte que les changements commités. 🤦‍♂️

En ce moment, j'utilise Earthly pour lequel je n'ai pas encore trouvé de défaut. Je vous propose de regarder un exemple, sans trop paraphraser la documentation. ✨

Earthly

Les usages courants nécessitent simplement l'écriture d'un Earthfile, avec une syntaxe à mi-chemin entre le Dockerfile et le Makefile, un exemple très simple recoupant notre cahier des charges :

FROM organization/php:8.0
WORKDIR /code

deps:
    ENV XDG_CACHE_HOME=/code/build/cache
    COPY --dir . .
    RUN --ssh make install
    SAVE ARTIFACT ./build/cache AS LOCAL ./build/cache

lint:
    FROM +deps
    RUN make lint

test:
    FROM +deps
    WITH DOCKER \
        --compose docker-compose.yml \
        --service postgres \
      RUN while ! pg_isready --host=localhost --port=5432 --dbname=request --username=idea; do sleep 1; done ;\
      make test
    END
    SAVE ARTIFACT ./build/coverage AS LOCAL ./build/coverage
    SAVE ARTIFACT ./testreport.xml AS LOCAL ./testreport.xml

Environnement isolé

D'entrée de jeu, on remarque l'instruction FROM qui définit l'image docker de base dans laquelle seront lancé les commandes.

Dépendances privées

Les dépendances privées nécessitent un moyen de s'authentifier, Earthly propose de se binder directement sur votre ssh local, via la variable d'environnement SSH_AUTH_SOCK. Il s'agit du flag --ssh dans la cible deps:.

Cache

Pour cacher les dépendances installées, on va aller surcharger la configuration du gestionnaire de dépendances. Ici, composer suit la XDG Base Directory Specification on peut alors exploiter la variable d'environnement XDG_CACHE_HOME. Le cache est alors traité comme un artefact et sera copié lors d'un lancement n+1.

Lancer des services dépendants

On remarque que la cible test: utilise un fichier docker-compose.yml: WITH DOCKER ! On lance dans le cas présent uniquement un service postgres, qui sera rendu accessible depuis notre image spécifiée lors du FROM.

Artefacts

Comme déjà abordé pour le cache, Earthly nous permet de sauvegarder des artefacts: SAVE ARTEFACT AS LOCAL, comme notre couverture de code ou un rapport de test. Ces fichiers seront donc accessibles en local, et potentiellement exploitable par votre service de build.

Le local OK, mais le serveur de build maintenant ?

La documentation détaille plusieurs exemples d'intégration avec des services de build : https://docs.earthly.dev/ci-integration/vendor-specific-guides.

Je vous rajoute une proposition pour gitlab-ci ci-dessous :

image: organization/image:tag

services:
  - docker:dind

cache:
  key: $CI_PROJECT_ID
  paths:
    - ./build/cache

before_script:
  - docker info

lint:
  script:
    - ./build.sh "+lint"

test:
  script:
    - ./build.sh "+test"
  artifacts:
    paths:
      - ./build/coverage
      - ./build/testreport.xml
    expire_in: 30 days
    reports:
      junit: ./build/testreport.xml

Et je vous mets le script ./build.sh qui facilite l'utilisation d'Earthly aussi bien en local qu'en ci 🙃.

#!/usr/bin/env bash

set -e

pushd $(dirname $0) 1> /dev/null
EARTHLY='./build/bin/earthly'
mkdir -p $(dirname $EARTHLY)

if [ ! -e $EARTHLY ] && [ $(uname) = "Darwin" ]
then
    curl -L https://github.com/earthly/earthly/releases/download/v0.6.6/earthly-darwin-amd64 -o $EARTHLY
fi

if [ ! -e $EARTHLY ] && [ $(uname) = "Linux" ]
then
    curl -L https://github.com/earthly/earthly/releases/download/v0.6.6/earthly-linux-amd64 -o $EARTHLY
fi

if [ ! -f "$EARTHLY" ]; then
    echo "Your system may not be supported"
    popd
    exit 1
fi

chmod +x $EARTHLY

TIMESTAMP=$(date +%s)

export FORCE_COLOR=1
$EARTHLY --allow-privileged --verbose $@ 2>&1 | tee ./build/$TIMESTAMP.log
echo "Log written in ./build/$TIMESTAMP.log"
popd 1> /dev/null

La chaine d'outils

flowchart LR classDef default fill:#a8a8b3,color:#f7f7f8,stroke:#fff; gitlab([gitlab-ci]) --> earth([earthly]) earth([earthly]) --> make([make]) make([make]) --> lib([librairies])

L'exemple présenté respecte la chaine d'outillage résumé ci-dessus. Afin de garder une consistance et un interêt dans la mise en place d'un outil comme Earthly, il faut bien entendu s'assurer qu'elle soit respectée.

Conclusion

Je pense qu'utiliser un outil de ce genre à un réel intérêt méthodologique. En plus de les rendre reproductibles, ils apportent un point de découplage vous permettant de changer votre solution de build (oui oui, on est d'accord que c'est pas tous les jours 😅) mais surtout vous permets de concevoir, tester, debugger vos pipelines localement, en vous affranchissant de votre gestion des sources, fini les commits debug ci par dizaine !

© 2021 Nicolas Le Borgne