Aller au contenu
CI/CD & Automatisation medium

Dagger : connexion, cycle async et gestion des erreurs

17 min de lecture

En Python, les pipelines Dagger utilisent l’async/await pour gérer les opérations asynchrones. La particularité de Dagger est sa lazy evaluation : le code que vous écrivez ne s’exécute pas immédiatement — il construit un graphe d’opérations qui sera exécuté quand vous le demanderez explicitement.

Prérequis : Avoir installé Dagger et créé un module. Si vous n’êtes pas familier avec async/await en Python, consultez la documentation officielle asyncio.

Quand vous appelez des méthodes comme dag.container().from_(), vous ne lancez pas de conteneurs — vous décrivez ce que vous voulez faire. L’exécution réelle se produit seulement quand vous appelez une méthode “terminale” comme stdout(), sync() ou export().

La lazy evaluation offre plusieurs avantages :

  • Optimisation automatique : Dagger peut réorganiser et paralléliser les opérations
  • Cache intelligent : les étapes identiques sont exécutées une seule fois
  • Composition : vous pouvez construire des pipelines complexes puis les exécuter

L’exécution se déclenche quand vous appelez une méthode qui demande un résultat :

MéthodeCe qu’elle faitRetour
stdout()Récupère la sortie standardstr
stderr()Récupère la sortie d’erreurstr
sync()Force l’exécutionl’objet lui-même
export()Exporte un fichier/répertoirechemin
contents()Lit le contenu d’un fichierstr

Le SDK Python Dagger vous permet d’écrire deux types de fonctions.

Une fonction synchrone retourne directement une valeur ou un objet Dagger sans attendre d’exécution :

@function
def hello(self, name: str = "World") -> str:
"""Retourne un message — pas d'interaction avec Dagger."""
return f"Bonjour, {name} !"
Fenêtre de terminal
dagger call hello --name="DevOps"
# Bonjour, DevOps !

Vous pouvez aussi retourner un Container sans l’exécuter :

@function
def build_container(self) -> dagger.Container:
"""Retourne un conteneur configuré (lazy - pas encore exécuté)."""
return (
dag.container()
.from_("python:3.12-slim")
.with_exec(["python", "--version"])
)

Ce code ne lance aucun conteneur. Il ne fait que décrire le pipeline.

Une fonction asynchrone attend le résultat d’une opération qui force l’exécution :

@function
async def get_python_version(self) -> str:
"""Récupère la version de Python — force l'exécution."""
result = await (
dag.container()
.from_("python:3.12-slim")
.with_exec(["python", "--version"])
.stdout() # <-- Force l'exécution
)
return result.strip()
Fenêtre de terminal
dagger call get-python-version
# Python 3.12.12

Dagger sépare proprement la sortie standard et la sortie d’erreur.

@function
async def get_output(self) -> str:
"""Récupère la sortie standard d'une commande."""
return await (
dag.container()
.from_("alpine:latest")
.with_exec(["echo", "Hello from stdout"])
.stdout()
)
@function
async def get_error(self) -> str:
"""Récupère la sortie d'erreur d'une commande."""
return await (
dag.container()
.from_("alpine:latest")
.with_exec(["sh", "-c", "echo 'Ceci va sur stderr' >&2"])
.stderr()
)

Pour récupérer les deux sorties, stockez le conteneur dans une variable :

@function
async def show_outputs(self) -> str:
"""Récupère stdout ET stderr séparément."""
container = (
dag.container()
.from_("alpine:latest")
.with_exec(["sh", "-c", "echo stdout && echo stderr >&2"])
)
stdout_content = await container.stdout()
stderr_content = await container.stderr()
return f"STDOUT: {stdout_content.strip()} | STDERR: {stderr_content.strip()}"
Fenêtre de terminal
dagger call show-outputs
# STDOUT: stdout | STDERR: stderr

sync() : forcer l’exécution sans récupérer la sortie

Section intitulée « sync() : forcer l’exécution sans récupérer la sortie »

Parfois, vous voulez vous assurer qu’une étape est exécutée sans récupérer sa sortie. C’est le rôle de sync() :

@function
async def prepare_environment(self) -> dagger.Container:
"""Prépare l'environnement et s'assure qu'il est construit."""
container = (
dag.container()
.from_("python:3.12-slim")
.with_exec(["pip", "install", "pytest"])
)
# sync() force l'exécution et retourne le même Container
await container.sync()
return container

Quand une commande échoue dans un conteneur, Dagger lève une exception. Vous pouvez la capturer avec try/except.

import dagger
from dagger import dag, function, object_type
@object_type
class MonModule:
@function
async def demo_error_handling(self) -> str:
"""Capture les erreurs d'exécution."""
try:
await (
dag.container()
.from_("alpine:latest")
.with_exec(["cat", "/fichier-inexistant"])
.stdout()
)
return "Succès"
except dagger.ExecError as e:
return f"Erreur capturée : {e}"
Fenêtre de terminal
dagger call demo-error-handling
# Erreur capturée : exit code: 1 [traceparent:...]
ExceptionCause
dagger.ExecErrorUne commande with_exec() a échoué (code de sortie ≠ 0)
dagger.QueryErrorErreur lors de l’appel à l’API Dagger
ExceptionAutres erreurs Python
@function
async def safe_operation(self) -> str:
"""Pattern de gestion d'erreurs robuste."""
try:
result = await (
dag.container()
.from_("alpine:latest")
.with_exec(["commande", "risquee"])
.stdout()
)
return f"Succès : {result}"
except dagger.ExecError as e:
# Erreur de commande (code de sortie ≠ 0)
return f"La commande a échoué : {e}"
except dagger.QueryError as e:
# Erreur d'API Dagger
return f"Erreur Dagger : {e}"
except Exception as e:
# Autres erreurs
return f"Erreur inattendue : {type(e).__name__}: {e}"

Dagger offre plusieurs outils pour debugger vos pipelines.

Vous pouvez ouvrir un terminal dans n’importe quel conteneur :

Fenêtre de terminal
dagger call build-container terminal

Cela ouvre un shell interactif dans le conteneur, où vous pouvez explorer l’état du système de fichiers, exécuter des commandes manuellement, etc.

Si une commande échoue, vous pouvez relancer avec --interactive pour obtenir un terminal au point d’échec :

Fenêtre de terminal
dagger call --interactive ma-fonction

Ajoutez des flags -v pour plus de détails :

Fenêtre de terminal
dagger call -v ma-fonction # Garde les spans visibles
dagger call -vv ma-fonction # Révèle les spans internes
dagger call -vvv ma-fonction # Révèle les spans < 100ms
dagger call --debug ma-fonction # Informations de debug complètes

Cet exemple rassemble tous les concepts vus dans ce guide. Prenez le temps de le lire — chaque ligne applique une notion expliquée précédemment.

import dagger
from dagger import dag, function, object_type
@object_type
class MonPipeline:
"""Pipeline avec gestion async et erreurs."""
@function
async def build_and_test(self, source: dagger.Directory) -> str:
"""Build et teste un projet Python."""
try:
# ① LAZY : on décrit le pipeline, rien ne s'exécute encore
container = (
dag.container()
.from_("python:3.12-slim")
.with_directory("/app", source)
.with_workdir("/app")
.with_exec(["pip", "install", "-e", "."])
)
# ② SYNC : on force l'exécution pour valider le build
# Si pip échoue ici, on saute directement au except
await container.sync()
# ③ STDOUT : on récupère la sortie des tests
# C'est aussi un trigger, donc await obligatoire
test_output = await (
container
.with_exec(["pytest", "-v"])
.stdout()
)
return f"Tests réussis :\n{test_output}"
except dagger.ExecError as e:
# ④ ERREUR : une commande a échoué (pip install ou pytest)
return f"Échec : {e}"
LigneConceptCe qui se passe
dag.container().from_()...LazyOn construit un graphe, aucun conteneur ne démarre
await container.sync()TriggerL’Engine exécute tout jusqu’ici (pull image + pip install)
await ... .stdout()Async + TriggerOn attend le résultat de pytest
except dagger.ExecErrorGestion erreursOn capture les codes de sortie ≠ 0

Pour tester ce pipeline sur un projet Python local :

Fenêtre de terminal
# Depuis un dossier contenant pyproject.toml et des tests
dagger call build-and-test --source=.

Sortie en cas de succès :

Tests réussis :
============================= test session starts ==============================
collected 3 items
tests/test_main.py ... [100%]
============================== 3 passed in 0.42s ===============================

Sortie en cas d’échec :

Échec : exit code: 1 [traceparent:00-abc123...]

Scénario 1 : pip install échoue

Fenêtre de terminal
# Lancer en mode interactif pour inspecter l'état
dagger call --interactive build-and-test --source=.

Quand l’erreur se produit, un terminal s’ouvre dans le conteneur. Vous pouvez :

Fenêtre de terminal
# Vérifier les fichiers présents
ls -la /app
# Tester pip manuellement
pip install -e . 2>&1
# Inspecter le pyproject.toml
cat /app/pyproject.toml

Scénario 2 : pytest échoue

Fenêtre de terminal
# Augmenter la verbosité pour voir les détails
dagger call -vv build-and-test --source=.

Ou ouvrir un terminal sur le conteneur après le build :

Fenêtre de terminal
# D'abord, créer une fonction qui retourne le conteneur
dagger call build-container terminal

Puis dans le terminal :

Fenêtre de terminal
# Lancer pytest manuellement pour voir les erreurs
cd /app && pytest -v --tb=long

Scénario 3 : comprendre ce qui est caché

Fenêtre de terminal
# Voir TOUT ce que Dagger fait en interne
dagger call --debug build-and-test --source=.

Dans l’exemple, on appelle sync() après pip install, puis stdout() après pytest. Voici pourquoi :

Sans sync()Avec sync()
Si pip échoue, l’erreur est mélangée avec pytestL’erreur pip est isolée, on sait exactement où ça casse
Impossible de savoir quelle étape a plantéDebug ciblé : build OK → tests KO
Moins d’overhead (1 seul trigger)Légèrement plus lent (2 triggers)

Règle : utilisez sync() entre les étapes logiques pour un debug précis.

Pour avoir plus de détails sur l’erreur :

@function
async def build_and_test_verbose(self, source: dagger.Directory) -> str:
"""Version avec diagnostic détaillé."""
container = (
dag.container()
.from_("python:3.12-slim")
.with_directory("/app", source)
.with_workdir("/app")
.with_exec(["pip", "install", "-e", "."])
)
try:
await container.sync()
except dagger.ExecError:
# Récupérer stderr pour comprendre l'échec pip
err = await container.stderr()
return f"pip install a échoué :\n{err}"
test_container = container.with_exec(["pytest", "-v"])
try:
output = await test_container.stdout()
return f"Tests OK :\n{output}"
except dagger.ExecError:
err = await test_container.stderr()
return f"pytest a échoué :\n{err}"
  • Lazy evaluation : dag.container()... ne s’exécute pas immédiatement
  • Triggers : stdout(), stderr(), sync(), export(), contents() forcent l’exécution
  • Async : toute fonction qui appelle un trigger doit être async def avec await
  • Erreurs : utilisez try/except dagger.ExecError pour capturer les échecs
  • Debug : dagger call --interactive ou terminal pour explorer les conteneurs
  • Verbosité : -v, -vv, -vvv, --debug pour plus d’informations

Maintenant que vous maîtrisez le cycle async et la gestion des erreurs :

  • Module 4 : Container API — Maîtriser from_, with_exec, variables d’environnement (à venir)
  • Module 5 : Fichiers & workspace — Monter des répertoires, gérer les sorties (à venir)

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.