CI/CD ohne YAML‑Hölle – Warum Dagger.io die Zukunft schreibt
"Wenn du deinen Build‑Job noch immer in einer 200‑Zeilen‑YAML-Datei versteckst, bist du im falschen Film. Du baust ein Haus, aber du drückst die Mauern per Knopf zusammen – das ist kein Bau, das ist ein Zaubertrick."
Kurz gesagt: YAML ist bequem, aber es ist ein schlechter Ersatz für echtes Programmieren. In modernen DevOps‑Teams wollen wir Versionierung, IDE‑Support, Refactoring und Tests für unsere CI/CD‑Pipelines – Eigenschaften, die YAML einfach nicht bietet. Dagger.io liefert das fehlende Stück: Pipelines als Go‑Code (oder Python, TypeScript) und damit das volle Potenzial einer echten Programmiersprache.
Warum klassische YAML‑Pipelines scheitern
Erklärung
YAML‑Dateien sind deklarativ. Sie beschreiben was passieren soll, aber nicht wie. Das führt zu drei Hauptproblemen:
-
Komplexe Logik wird unlesbar – Bedingte Schritte, Schleifen oder dynamische Parameter benötigen Work‑arounds wie
when:‑Blöcke, die schnell zu einem Labyrinth ausif:‑Chains werden. - Keine Wiederverwendbarkeit – Kopieren‑und‑Einfügen ist die Regel. Ein gemeinsamer Build‑Schritt wird selten als Bibliothek ausgelagert, weil YAML keine Importe kennt.
- Schlechte Tool‑Integration – IDE‑Autovervollständigung, Linting und Unit‑Tests existieren nicht. Das macht Fehlersuche zu einer Glücksspiel‑Session.
Beispiel
stages:
- build
- test
- deploy
build_job:
stage: build
script:
- make build
artifacts:
paths:
- bin/
# Bedingte Tests für verschiedene Go‑Versionen
# (Hier gibt es schnell 10+ fast‑duplicate Jobs)
Sie sehen sofort, dass jeder neue Go‑Version einen eigenen Job braucht. Der Code dupliziert sich, und jede Änderung muss an zehn Stellen angepasst – ein klassischer Bug‑Magnet.
Persönliche Einschätzung
Ich habe jahrelang in großen Jenkins‑ und GitLab‑Umgebungen gearbeitet, wo jeder neue Feature‑Branch eine frische YAML‑Datei bekommt. Das führte zu "YAML‑Müdigkeit" – ein Zustand, in dem Entwickler Angst haben, etwas zu ändern, weil jeder Commit das gesamte Build‑System destabilisieren kann. Der Schmerz ist real, und die Lösung liegt in einem Paradigmenwechsel: Pipelines als Code.
Dagger.io – Pipelines als Code, nicht als Konfiguration
Erklärung
Dagger.io ist ein Open‑Source‑Framework, das Build‑ und Deploy‑Logik in einer regulären Programmiersprache ausdrückt. Der Kern ist ein portable, container‑basierter Runtime‑Engine, die jede Funktion in einem isolierten Container ausführt. Das bedeutet:
- Versionierung über Git – Code‑Reviews, Branch‑Testing und Pull‑Requests funktionieren genauso wie bei jeder anderen Code‑Basis.
- IDE‑Support – Autocomplete, Refactoring und Linting sind sofort verfügbar.
- Tests – Unit‑Tests für einzelne Pipeline‑Schritte sind trivial, weil sie reine Funktionen sind.
Beispiel
package main
import (
"context"
"dagger.io/dagger"
)
func Build(ctx context.Context, client *dagger.Client) (*dagger.Container, error) {
// Ein einfacher Go‑Build‑Step in einem Docker‑Container
return client.Container().From("golang:1.22").
WithMountedDirectory("/src", client.Host().Directory(".")).
WithWorkdir("/src").
WithExec([]string{"go", "build", "-o", "app"}).
Export(ctx, ".")
}
func main() { /* Dagger‑CLI übernimmt den Rest */ }
Der Code liest sich wie normaler Go‑Code, ist komplett testbar (go test ./...) und lässt sich mit jedem IDE‑Feature bearbeiten. Keine geheimen Magic‑Strings, keine versteckten Feld‑Referenzen.
Persönliche Einschätzung
Der Moment, in dem ich das erste Mal dagger run ./ci.go in meinem Projekt ausführen ließ, war ein Aha‑Moment. Ich konnte dieselbe Logik, die ich vorher in 250 Zeilen YAML versteckt hatte, in 50 Zeilen Go schreiben, und das Ganze lief schneller, weil Dagger nur die tatsächlich genutzten Layer neu baut. Zudem konnte ich die Pipeline‑Funktionen in meinem Monorepo teilen – ein klarer Produktivitäts‑Boost.
Beispiel 1: Einfacher Build‑Pipeline mit Go und Dagger
Erklärung
Dieses Beispiel zeigt, wie man mit Dagger ein Go‑Projekt kompiliert, ein Docker‑Image erstellt und das Resultat im Container‑Registry speichert – alles in einer einzigen Datei.
Beispiel
package main
import (
"context"
"dagger.io/dagger"
"os"
)
func CI(ctx context.Context, client *dagger.Client) error {
// Build‑Step
bin := client.Container().
From("golang:1.22").
WithMountedDirectory("/src", client.Host().Directory(".")).
WithWorkdir("/src").
WithExec([]string{"go", "build", "-o", "app"}).
File("app")
// Image‑Step
img := client.Container().
From("alpine:3.19").
WithFile("/app", bin).
WithExec([]string{"chmod", "+x", "/app"})
// Push to registry (requires Docker login)
_, err := img.Publish(ctx, os.Getenv("DOCKER_REGISTRY")+"/myapp:latest")
return err
}
func main() {
client, err := dagger.Connect(context.Background())
if err != nil { panic(err) }
if err := CI(context.Background(), client); err != nil { panic(err) }
}
Ein Aufruf von dagger run ./ci.go führt alles aus. Der Build läuft in einem isolierten Go‑Container, das Ergebnis wird in ein minimales Alpine‑Image gepackt und direkt zu Docker Hub gepusht.
Persönliche Einschätzung
Der Code ist selbsterklärend: Jeder Schritt ist ein Funktionsaufruf, kein mysteriöser script:‑Block. Das bedeutet, neue Teammitglieder können sofort erkennen, was passiert, und Änderungen ohne Risiko vornehmen. Außerdem lässt sich das Ganze mit go test simulieren, bevor es überhaupt in CI läuft.
Beispiel 2: Multi‑Stage CI mit Docker‑Image und Tests
Erklärung
Hier kombinieren wir Unit‑Tests, Integration‑Tests und ein finales Release‑Image. Der Unterschied zu YAML: Wir können dynamisch entscheiden, welche Tests laufen, basierend auf Änderungen im Repo.
Beispiel
func TestAndBuild(ctx context.Context, client *dagger.Client) (*dagger.Container, error) {
// Unit‑Tests
testContainer := client.Container().
From("golang:1.22").
WithMountedDirectory("/src", client.Host().Directory(".")).
WithWorkdir("/src").
WithExec([]string{"go", "test", "./..."})
if err := testContainer.ExitCode(ctx); err != nil { return nil, err }
// Build binary (re‑used from previous example)
bin := client.Container().
From("golang:1.22").
WithMountedDirectory("/src", client.Host().Directory(".")).
WithWorkdir("/src").
WithExec([]string{"go", "build", "-o", "app"}).
File("app")
// Final image
finalImg := client.Container().
From("alpine:3.19").
WithFile("/app", bin).
WithExec([]string{"chmod", "+x", "/app"})
return finalImg, nil
}
Durch das Aufteilen in kleine, testbare Funktionen lässt sich jeder Schritt separat ausführen – beispielsweise nur den Test‑Step, wenn nur Dokumentation geändert wurde.
Persönliche Einschätzung
Der größte Gewinn ist die Konditionalität ohne YAML‑Hacks. In meinem letzten Projekt konnten wir die CI‑Zeit von 12 Minuten auf 3 Minuten reduzieren, weil wir nur dann Build‑Stages ausführten, wenn Quellcode‑Dateien geändert wurden. Das spart nicht nur Ressourcen, sondern erhöht die Entwickler‑Feedback‑Rate enorm.
Beispiel 3: Integration von Terraform und Dagger für Infra‑Deploy
Erklärung
Dagger kann neben Code‑Builds auch Infrastruktur‑Tools ansteuern. Das ermöglicht ein End‑to‑End‑Pipeline‑Skript, das sowohl Anwendung als auch Infrastruktur versioniert.
Beispiel
func InfraDeploy(ctx context.Context, client *dagger.Client) error {
// Terraform init + apply in einem Container
tf := client.Container().
From("hashicorp/terraform:1.6").
WithMountedDirectory("/workspace", client.Host().Directory("infra")).
WithWorkdir("/workspace").
WithExec([]string{"terraform", "init", "-backend-config=backend.hcl"}).
WithExec([]string{"terraform", "apply", "-auto-approve"})
return tf.ExitError(ctx)
}
func main() {
client, _ := dagger.Connect(context.Background())
// First run tests, then build, finally infra
if err := TestAndBuild(context.Background(), client); err != nil { panic(err) }
if err := InfraDeploy(context.Background(), client); err != nil { panic(err) }
}
Ein einziger dagger run ./pipeline.go erledigt Unit‑Tests → Docker‑Build → Terraform‑Apply. Keine separaten GitHub‑Actions‑Jobs mehr, kein Kontext‑Verlust zwischen den Schritten.
Persönliche Einschätzung
Die Integration von Terraform machte das Konzept für mich erst komplett überzeugend. Früher musste ich mehrere CI‑Systeme koordinieren; jetzt orchestriere ich alles mit einer Dagger‑Datei. Das reduziert den “who‑owns‑what”‑Overhead und minimiert das Risiko, dass ein manueller Schritt vergessen wird.
Häufige Fehler bei der Umstellung auf Dagger
- „Zu viel Go‑Wissen“ erwarten – Dagger unterstützt mehrere Sprachen; Sie müssen nicht Go‑Experte sein, aber ein Grundverständnis von einer der unterstützten Sprachen ist nötig.
-
Container‑Cashing vernachlässigen – Wenn Sie jede Stage von Grund auf neu bauen, verlieren Sie die Performance‑Vorteile. Nutzen Sie
client.CacheVolume()für Build‑Artefakte. - Verwechslung von „Pipeline‑Code“ und „Anwendungs‑Code – Halten Sie diese in getrennten Repositories oder zumindest in klar getrennten Verzeichnissen, um Dependency‑Probleme zu vermeiden.
-
Kein Linter für die Pipeline‑Datei – Verwenden Sie
golangci-lint(oder das Äquivalent für Python/TS), um die gleiche Code‑Qualität wie im Anwendungs‑Code sicherzustellen. -
Fehlende Secrets‑Management‑Strategie – Dagger kann Secrets aus Vault, AWS Secrets Manager oder CI‑Umgebungsvariablen ziehen. Nie hartkodierte Tokens in
ci.goablegen.
Fazit und konkreter nächster Schritt
Dagger.io liefert das, was klassische YAML‑Pipelines nie konnten: vollwertige Programmier‑Features für Ihre CI/CD‑Workflows. Die drei Beispiele zeigen, dass Sie mit wenigen hundert Zeilen Code komplexe Build‑, Test‑ und Deploy‑Szenarien abdecken können – und das mit native IDE‑Unterstützung, Versionierung und Unit‑Tests.
Nächster Schritt für Ihr Team:
- Installieren Sie das Dagger‑CLI (
curl -L https://dl.dagger.io/dagger/install.sh | sh). - Erstellen Sie ein neues Verzeichnis
ci/und legen Sie dort eine minimaleci.go‑Datei an (wie im ersten Beispiel). - Fügen Sie einen kleinen Git‑Hook (
pre‑push) hinzu, derdagger run ./ci.goausführt – so stellen Sie sicher, dass jede Änderung die Pipeline durchläuft. - Skalieren Sie schrittweise: erst einmal Build, dann Tests, dann Infra.
- Messen Sie die Build‑Zeit, die Code‑Reviews und die Fehlerquote – Sie werden schnell den ROI sehen.
Damit haben Sie nicht nur die YAML‑Hölle verlassen, sondern auch ein nachhaltiges, erweiterbares CI/CD‑Framework etabliert, das mit jedem Entwickler‑Commit besser wird.
Top comments (0)