CI/CD ohne YAML‑Hölle – Warum Dagger.io die Zukunft der Pipelines ist
„Wenn ich noch eine weitere
docker-compose.ymlsehen muss, schmeiße ich meinen Laptop aus dem Fenster.“ – Mein tägliches Mantra, seit ich das erste Mal in einer monolithischen CI‑Pipeline erstickt war.
In den letzten Jahren haben wir die DevOps‑Landschaft mit unzähligen YAML‑Dateien überschwemmt: gitlab-ci.yml, azure-pipelines.yml, circleci/config.yml. Jedes Mal, wenn ein neuer Schritt, ein neues Secret oder ein zusätzlicher Build‑Parameter hinzukommt, schleicht sich ein neues Indent‑Level ein, das das Ganze schnell zu einem Labyrinth aus Einrückungen und kryptischen Shortcuts macht.
Ich habe genug von diesem Rätselraten. In diesem Beitrag zeige ich Ihnen, warum Dagger.io – ein reines Go‑basiertes SDK – das Gegenmittel zur YAML‑Apokalypse ist, und wie Sie konkrete Pipelines als echten Code schreiben können. Dabei gibt es drei handfeste Beispiele, jedes mit echten Befehlen, Konfigurationen und realen Learnings aus meiner täglichen Praxis.
Warum klassische YAML‑CI/CD scheitert
Erklärung
YAML ist dicht gepackt – das ist sein Zweck. Doch bei CI/CD wird sie zu einem Datenformat für Logik, und das ist fatal. Zwei Kernprobleme dominieren:
-
Mangelnde Wiederverwendbarkeit: Ein Block
script:lässt sich kaum modularisieren. Kopieren‑Einfügen ist die Regel, nicht die Ausnahme. - Fehlende Programmierspracheigenschaften: Bedingungen, Schleifen oder Funktionen gibt es nur über Work‑arounds (z. B. Bash‑One‑Liners), die den Code nur noch unlesbarer machen.
Beispiel 1 – Jenkins‑Pipeline in YAML (fiktiv)
stages:
- build
- test
- deploy
build:
script:
- echo "Building …"
- docker build -t myapp:${CI_COMMIT_SHA} .
test:
script:
- echo "Testing …"
- docker run --rm myapp:${CI_COMMIT_SHA} go test ./...
deploy:
script:
- echo "Deploying …"
- kubectl apply -f k8s/deployment.yaml
Persönliche Einschätzung: Für ein einfaches Projekt mag das noch funktionieren, aber sobald Sie mehrere Environments, Feature‑Flags oder dynamische Artefakte benötigen, wird diese Struktur unhandlich. Die Logik sitzt in Bash‑Snippets, die kaum testbar sind.
Beispiel 2 – GitLab CI mit vielen Includes
include:
- local: "/ci/templates/build.yml"
- local: "/ci/templates/test.yml"
- local: "/ci/templates/deploy.yml"
Jeder Include ruft ein weiteres YAML‑File auf – ein typisches Spaghetti‑YAML. Die Wartung wird zur täglichen Tortur, besonders wenn ein Teammitglied die Datei test.yml ändert und plötzlich die Build‑Stufe bricht.
Persönliche Einschätzung: Das ist das Symptom einer zu tief gekapselten CI‑Konfiguration. Jeder kleine Change hat unerwartete Seiteneffekte.
Dagger.io – Pipelines als Code
Erklärung
Dagger.io ist kein YAML‑Wrapper, sondern ein Go‑SDK (mit Bindings für Python, TypeScript, Bash). Sie schreiben Ihre Pipeline als reguläres Programm, welches die folgenden Vorteile garantiert:
- Typ‑Sicherheit: Der Compiler prüft, dass alle Schritte korrekt verkettet sind.
- Modularität: Funktionen können als wiederverwendbare Bausteine exportiert werden.
- Testbarkeit: Unit‑Tests für einzelne Schritte funktionieren wie jeder andere Go‑Test.
- Caching & Parallelisierung: Dagger kümmert sich intern um deduplizierte Schichten – kein manuelles Cache‑Management nötig.
Beispiel – Minimaler Daggerfile (Go)
package main
import (
"context"
"dagger.io/dagger"
"log"
)
func main() {
ctx := context.Background()
client, err := dagger.Connect(ctx)
if err != nil { log.Fatal(err) }
defer client.Close()
// Build‑Stage
build := client.Container().From("golang:1.22").
WithMountedDirectory("/src", client.Host().Directory(".")).
WithWorkdir("/src").
WithExec([]string{"go", "build", "-o", "app"})
// Test‑Stage
test := build.WithExec([]string{"go", "test", "./..."})
// Publish‑Stage (Docker Image)
image := client.Container().From("alpine").
WithFile("/app", build.File("/src/app")).
WithEntrypoint([]string{"/app"})
// Export
_, err = image.Publish(ctx, "ttl.sh/myapp:$(date +%s)")
if err != nil { log.Fatal(err) }
}
Persönliche Einschätzung: Mit vier Zeilen Code haben wir Build, Test und Publish definiert. Der Compiler verhindert sofortige Typ‑Fehler, und wir können die einzelnen Schritte in eigenen Functions auslagern.
Praktisches Beispiel 1 – Build & Test einer Go‑App
Erklärung
Hier zerlegen wir die Pipeline in wiederverwendbare Go‑Funktionen. Das Ergebnis ist ein modulares, testbares CI‑Script, das jede Änderung sofort validiert.
Code‑Beispiel
func BuildApp(ctx context.Context, client *dagger.Client) *dagger.Container {
return client.Container().
From("golang:1.22").
WithMountedDirectory("/src", client.Host().Directory(".")).
WithWorkdir("/src").
WithExec([]string{"go", "build", "-o", "myapp"})
}
func TestApp(ctx context.Context, build *dagger.Container) *dagger.Container {
return build.WithExec([]string{"go", "test", "./..."})
}
func PublishImage(ctx context.Context, client *dagger.Client, build *dagger.Container) (string, error) {
img := client.Container().
From("alpine").
WithFile("/myapp", build.File("/src/myapp")).
WithEntrypoint([]string{"/myapp"})
return img.Publish(ctx, "docker.io/yourorg/myapp:latest")
}
Ausführen
# Vorher: go install dagger.io/dagger@latest && dagger up
dagger run ./ci.go
Der Befehl startet das Go‑Programm, baut das Binary, führt die Tests aus und pusht das Image – alles in einem Schritt.
Persönliche Einschätzung: Die klare Trennung in Functions macht Unit‑Tests zum Kinderspiel. Ich habe zum Beispiel go test ./ci_test.go geschrieben, das den Build‑Container isoliert prüft.
Praktisches Beispiel 2 – Multi‑Arch Docker Images mit Dagger
Erklärung
Ein häufiger Pain‑Point ist das Erzeugen von multi‑arch Images (amd64 & arm64). Traditionell muss man docker buildx manuell konfigurieren – das ist fehleranfällig und nicht wiederholbar.
Code‑Beispiel
func BuildMultiArch(ctx context.Context, client *dagger.Client) (string, error) {
// Build für amd64
amd := client.Container().From("golang:1.22").
WithPlatform("linux/amd64").
WithMountedDirectory("/src", client.Host().Directory(".")).
WithWorkdir("/src").
WithExec([]string{"go", "build", "-o", "myapp-amd64"})
// Build für arm64
arm := client.Container().From("golang:1.22").
WithPlatform("linux/arm64").
WithMountedDirectory("/src", client.Host().Directory(".")).
WithWorkdir("/src").
WithExec([]string{"go", "build", "-o", "myapp-arm64"})
// Erstelle finales Image mit beiden Binaries
img := client.Container().From("alpine").
WithFile("/myapp-amd64", amd.File("/src/myapp-amd64")).
WithFile("/myapp-arm64", arm.File("/src/myapp-arm64")).
WithEntrypoint([]string{"/myapp-${TARGETARCH}"})
return img.Publish(ctx, "docker.io/yourorg/myapp:multi-arch")
}
Ausführen
export DAGER_PLATFORM=linux/amd64
dagger run ./ci.go build-multi-arch
Dagger übernimmt das Plattform‑Management – Sie müssen nur die gewünschten Archs angeben. Der resultierende Image‑Manifest enthält beide Varianten, und docker run wählt automatisch die passende Architektur aus.
Persönliche Einschätzung: Der Plattform‑Switch ist in Dagger ein One‑Liner. Im Vergleich zu docker buildx spart das jede Menge Skript‑Boilerplate und vermeidet das häufige "manifest missing"‑Problem.
Praktisches Beispiel 3 – Deploy nach Kubernetes ohne YAML
Erklärung
Statt ein deployment.yaml zu schreiben, erzeugen wir das Kubernetes‑Objekt direkt aus Go‑Code. Das reduziert Duplication zwischen Git‑Ops‑Config und CI‑Pipeline.
Code‑Beispiel
import (
"k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"
)
func DeployToK8s(ctx context.Context, client *dagger.Client, image string) error {
dep := &v1.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "myapp", Namespace: "production"},
Spec: v1.DeploymentSpec{
Replicas: int32Ptr(3),
Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"app": "myapp"}},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"app": "myapp"}},
Spec: corev1.PodSpec{Containers: []corev1.Container{{Name: "myapp", Image: image}}},
},
},
}
yamlData, _ := yaml.Marshal(dep)
// Push YAML to cluster via kubectl
return client.Host().Exec([]string{"kubectl", "apply", "-f", "-"}, dagger.WithStdin(string(yamlData))).Run(ctx)
}
Ausführen
dagger run ./ci.go deploy-to-k8s --image docker.io/yourorg/myapp:latest
Damit entsteht das Deployment on‑the‑fly – kein statisches YAML‑File, das veraltet sein könnte.
Persönliche Einschätzung: Der größte Gewinn ist die Kohärenz zwischen Build‑ und Deploy‑Logik. Änderungen am Image‑Tag werden automatisch übernommen, ohne dass ein separates Manifest aktualisiert werden muss.
Häufige Fehler beim Umstieg auf Dagger.io
- „Ich will nur ein Schnell‑Script“ – Viele starten mit einer kleinen Bash‑Datei und vergessen, das gesamte Projekt in ein Go‑Modul zu legen. Das führt schnell zu Import‑Chaos.
-
„Ich setze alles auf
client.Host()“ – Der Host‑Connector ist praktisch, aber übermäßiger Gebrauch erzeugt unvorhersehbare Side‑Effects auf dem Build‑Host. Stattdessen lieber dedizierte Container‑Steps. - „Ich vergesse Caching‑Strategien“ – Dagger kann Layer‑Caching automatisch nutzen, solange Sie immutable Eingaben (z. B. Git‑Commit‑Hash) als Parameter übergeben. Ohne das entsteht bei jedem Run ein kompletter Rebuild.
-
„Ich missinterpretieren
WithPlatform“ – Wird nur für Container‑Befehle gebraucht; beim Deploy‑Step ist es irrelevant und kann zu verwirrender Fehlermeldungen führen. - „Ich schreibe alles in einer Datei“ – Der Code wird schnell unübersichtlich. Nutzen Sie Packages und interfaces, um die Pipeline in sinnvolle Module zu spalten.
Fazit & konkreter nächster Schritt
Dagger.io verwandelt Ihre CI/CD‑Pipeline von einer statischen YAML‑Ansammlung in ein vollwertiges Programm, das vom Compiler geprüft, unit‑getestet und versioniert wird. Die drei Beispiele zeigen, dass Sie damit Build & Test, Multi‑Arch‑Images und Kubernetes‑Deployments ohne ein einziges YAML‑File umsetzen können.
Ihr nächster Schritt:
- Installieren Sie das Dagger‑CLI (
curl -L https://dl.dagger.io/dagger/install.sh | sh). - Initialisieren Sie ein neues Projekt:
dagger init github.com/yourorg/ci-pipeline. - Kopieren Sie das obige Minimal‑Beispiel in
ci.go, passen Sie die Pfade an und führen Siedagger run ./ci.goaus. - Schreiben Sie Unit‑Tests (
go test ./...) für jede Function – das ist das eigentliche „Sicherheit‑Netz“ Ihrer Pipeline. - Integrieren Sie das Dagger‑Projekt in Ihr Repository und lassen Sie Ihren Git‑Server (
GitHub Actions,GitLab CI) dasdagger runausführen – Sie haben dann eine YAML‑freie, reproduzierbare CI/CD‑Umgebung.
Wenn Sie diesen Prozess durchlaufen, wird das Gefühl von YAML‑Horror schnell von Kontrolle und Klarheit abgelöst. Und das ist der Kernpunkt: Pipelines als Code geben Ihnen die Werkzeuge, um komplexe Build‑ und Deploy‑Workflows zu programmieren, nicht zu beschreiben.
Viel Erfolg beim Umstieg – und denken Sie daran: Wenn Sie das nächste Mal ein script: sehen, fragen Sie sich, ob Sie nicht einfach einen Funktionsaufruf in Go schreiben könnten.
Top comments (0)