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.
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 :
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 :
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
Nous allons à présent construire l'image Docker d’une application Java SpringBoot de deux manières différentes.
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
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"]
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 :
alpine:latestimport 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])
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.
Exécution locale de container_echo :
dagger -m ../dagger-tasks call container-echo --string_arg helloworld stdout
Exécution via module GitHub :
dagger -m github.com/elkouhen/dagger-tasks call container-echo --string_arg helloworld stdout
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
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"
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
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 :
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.