Contactez-nous
-
devops

Dagger, un moteur de pipelines léger, exécutable du poste de dev jusqu’à la CI/CD

Dagger, un moteur de pipelines léger, exécutable du poste de dev jusqu’à la CI/CD

Sommaire

Dans l’écosystème des plateformes CI/CD (GitHub Actions, GitLab CI, Jenkins, CircleCI, etc.), il n’existe pas de standard. En effet, chaque plateforme impose sa propre syntaxe de définition de pipelines : YAML pour GitHub, Groovy pour Jenkins, etc. La gestion de composants réutilisables diffère aussi : GitHub utilise les actions, Jenkins les shared libraries, etc.

Par ailleurs, en dehors de quelques initiatives comme act, il reste compliqué d'exécuter les pipelines CI/CD sur les postes des développeurs. Les développeurs ont tendance à contourner cette limitation en implémentant une partie des pipelines CI/CD avec des outils standards (makefile, scripts shell, etc.).

Comme nous le verrons dans cet article, Dagger traite ces problématiques en permettant de définir des pipelines (appelés "fonctions" dans le vocabulaire Dagger) utilisant des langages de programmation standards (Python, Go, TypeScript, etc.). Ces pipelines s'exécutent aussi bien sur les plateformes CI/CD existantes que localement sur les postes de développement.

Fonctionnement de Dagger

Dagger repose sur un moteur qui orchestre l'exécution de fonctions. Ce moteur, doté de nombreuses capacités autour de Docker, permet entre autres de :

  • Construire des images Docker
  • Publier des images Docker
  • Démarrer des conteneurs

La syntaxe de Dagger s'inspire des scripts shell, avec un principe de chaînage de fonctions via le symbole |.

Comme le montre la copie d'écran ci-dessous, l'architecture de Dagger s'appuie aussi sur une API GraphQL. Ce qui permet d'interagir avec le moteur Dagger via différents clients :

  • Des SDKs dans différents langages (Go, Python, Java, etc.)
  • Un Dagger CLI pour l'exécution en ligne de commande

 

 

Fonction Helloworld

Pour comprendre les concepts de base de Dagger, commençons par un exemple simple qui illustre la syntaxe et le principe de chaînage des fonctions.

Exemple de fonction Dagger démarrant une image alpine et exécutant echo helloworld :

container | from alpine:latest | # démarre l'image alpine
with-exec echo "helloworld" | # exécute la commande echo dans le conteneur
stdout # affiche le résultat

Construction des images Docker via Dagger CLI

Nous allons à présent construire l'image Docker d’une application Java SpringBoot de deux manières différentes.

Construction de l'image Java

Création d'une image Docker depuis le Dagger CLI :

container | from maven:latest | # démarre l'image maven:latest
with-directory /src . | # monte le répertoire courant dans le conteneur à l'emplacement /src
with-workdir /src | # définit le répertoire de travail à /src
with-exec mvn clean install | # exécute la commande mvn clean install
with-entrypoint mvn spring-boot:run | # définit le point d'entrée du conteneur
publish ttl.sh/dagger-wescale-app:1h # publie l'image sur ttl.sh

Construction via Dockerfile

Alternative utilisant le Dagger CLI et un Dockerfile existant :

directory | # initie un contexte de build
with-file Dockerfile Dockerfile | # ajoute le fichier Dockerfile
with-file pom.xml pom.xml | # ajoute le fichier pom.xml
with-directory src src | # ajoute le répertoire src
docker-build | # construit l'image docker
publish ttl.sh/dagger-wescale-app:1h # publie l'image sur ttl.sh

 

# équivalent dagger : from maven:latest
FROM maven:latest

# équivalent dagger : with-workdir /src
WORKDIR /src

# équivalent dagger : with-directory /src .
COPY . /src

# équivalent dagger : with-exec mvn clean install
RUN mvn clean install

# équivalent dagger : with-entrypoint mvn spring-boot:run
CMD ["mvn", "spring-boot:run"]

Implémentation d'une fonction Helloworld

Pour réutiliser facilement nos pipelines et les versionner, nous allons créer nos propres fonctions Dagger en Python.

Initialisation d'un projet Dagger Python :

dagger init # initialise le projet Dagger
dagger develop --sdk=python # génère du code d'une fonction Dagger en python

Code source de la fonction générée container_echo :

  • démarre un container à partir de l'image alpine:latest
  • exécute la commande echo avec l'argument string_arg
import dagger
from dagger import dag, function, object_type


@object_type
class DaggerTasks:


   @function
   def container_echo(self, string_arg: str) -> dagger.Container:
       """Returns a container that echoes whatever string argument is provided"""
       return dag.container().from_("alpine:latest").with_exec(["echo", string_arg])

Invocation de la fonction

Nous allons maintenant exécuter la fonction container_echo de deux manières différentes. Dans le premier cas, le code du module Dagger est en local. Dans le second cas, le code du module est hébergé sur GitHub.

Invocation de la fonction avec le code en local

Exécution locale de container_echo :

dagger -m ../dagger-tasks call container-echo --string_arg helloworld stdout

Invocation de la fonction avec le code sur Github

Exécution via module GitHub :

dagger -m github.com/elkouhen/dagger-tasks call container-echo --string_arg helloworld stdout

Implémentation d'une fonction "build Maven"

Créons maintenant une fonction build_mvn qui construit l'image Docker d'une application Java SpringBoot et publie l'image Docker sur ttl.sh (un référentiel d'images temporaires).

Code source de la fonction build_mvn :

   @function
   async def build_mvn(self, source: dagger.Directory, image_name: str) -> str:
       """
       Builds a Maven project from the provided source directory and publishes the image to the specified image name.
       :param source: The source directory containing the Maven project.
       :param image_name: The name of the image to publish.
       :return: The reference of the published image.
       """
       return await (dag.container()
                     .from_("maven:latest")  # démarre avec Maven
                     .with_directory("/src", source)  # monte le code source
                     .with_workdir("/src")  # définit le répertoire de travail
                     .with_exec(["mvn", "clean", "install", "-DskipTests"])  # compile le projet
                     .with_entrypoint(["mvn", "spring-boot:run"])  # définit le point d'entrée
                     .publish(image_name))  # publie l'image

Invocation locale de la fonction

Voici comment exécuter la fonction build_mvn en local avec Dagger CLI :

dagger -m ../dagger-tasks call build-mvn --source=. --image-name="ttl.sh/dagger-wescale-app:1h"

Intégration continue GitHub avec Dagger

Un des avantages majeurs de Dagger est sa capacité à s'intégrer dans les workflows CI/CD existants. Voyons comment utiliser nos fonctions Dagger dans GitHub Actions.

L'intégration de Dagger dans un workflow GitHub se fait de manière assez simple via dagger-for-github.

Exemple de workflow GitHub Actions utilisant Dagger :

on: [push]  # déclenché à chaque push


jobs:
   build:
       name: build
       runs-on: ubuntu-latest
       steps:
       - name: Checkout
         uses: actions/checkout@v4  # récupère le code
       - name: Build Jar
         uses: dagger/dagger-for-github@8.2.0  # utilise l'action Dagger
         with:
           version: "latest"
           args: build-mvn --source=. --image-name="ttl.sh/dagger-wescale-app:1h"
           module: github.com/elkouhen/dagger-tasks  # module Dagger

Conclusion

Après avoir exploré Dagger à travers ces exemples pratiques, faisons le point sur ce que cet outil apporte à l'écosystème CI/CD.

Dagger représente une nouvelle manière de concevoir et d’exécuter les pipelines CI/CD. En effet, l’outil offre la possibilité de définir des pipelines portables, indépendants de toute plateforme spécifique, tout en restant intégrable avec les solutions existantes.

Dagger n'est pas une plateforme CI/CD complète, mais un moteur d'exécution de pipelines.

Cet outil se révèle particulièrement intéressant pour :

  • Les organisations qui souhaitent réduire leur dépendance à une plateforme CI/CD donnée ;
  • Les équipes qui cherchent à améliorer la qualité de leurs applications en testant leurs pipelines localement, sans avoir à dupliquer la logique CI/CD.

Retour d'expérience

Cet article est le résultat d'une expérimentation menée, lors d'un hackathon d'une journée chez WeScale, avec mes deux binômes du jour Vincent Ringuedé et Rémi Calizzano.

Dagger est un outil agréable et intéressant à utiliser, mais reste un peu jeune : j'ai eu à gérer quelques bugs (corrigés rapidement par l'équipe de développement) ainsi que l'évolution de l'API.

Le principal point faible de Dagger reste le référentiel de modules communautaires : le daggerverse. On y trouve de nombreux modules souvent redondants. Plus préoccupant encore, plusieurs modules testés se sont révélés non fonctionnels, ce qui limite pour l’instant la fiabilité de ce référentiel.

Pour tout projet nécessitant une CI/CD complexe, Dagger constitue aujourd'hui une option à considérer.