🟡 Intermediate

Dagger — Programmable CI/CD Pipelines

Dagger je open-source nástroj na písanie portable CI/CD pipelines v programovacom jazyku namiesto YAML súborov. Pipelines bežia lokálne aj v CI úplne rovnako, pretože sú zabalené v kontajneroch a riadené Dagger Enginom (postaveným nad BuildKitom).

Dagger vznikol v roku 2022 v tíme pôvodných autorov Dockeru (Solomon Hykes, Sam Alba) a rieši typický problém modernej dobe: YAML konfigurácie CI sa neškálujú, nedajú sa debugovať lokálne a každý vendor (GitHub, GitLab, CircleCI, Jenkins) má vlastný diallekt.


Prečo Dagger?

Klasický CI pipeline v YAML má známe bolesti:

  1. "Works on my machine" naopak — pipeline funguje v CI, ale na lokále ho neviete reprodukovať. Každá zmena si vyžaduje push a čakanie na runner.
  2. Vendor lock-in — presun z GitHub Actions na GitLab CI znamená prepísanie všetkých workflow súborov.
  3. Bez typovej kontroly — YAML chyby sa odhalia až po spustení.
  4. Duplicita — každý tím kopíruje tie isté build/test kroky do svojho workflow.

Dagger tieto problémy rieši:

  • Pipeline je kód — Go, Python, TypeScript alebo Java SDK s plným type-checkingom, IDE autocomplete a debugovaním.
  • Lokálne = CIdagger call build robí to isté na notebooku aj na runneri.
  • Portabilita — ten istý Dagger kód beží v GitHub Actions, GitLab CI, Jenkinse, CircleCI alebo priamo zo shellu.
  • Reproducibility cache — BuildKit cache vrstvy sa zdieľajú medzi behmi, aj medzi vývojármi (cez Dagger Cloud).

Ako Dagger funguje (architektúra)

┌───────────────────────────┐
│  Váš pipeline kód         │  Go / Python / TS / Java SDK
│  (Dagger Functions)       │
└──────────────┬────────────┘
               │ GraphQL API
               ▼
┌───────────────────────────┐
│  Dagger Engine (daemon)   │  container: dagger/engine
│  (BuildKit pod kapotou)   │
└──────────────┬────────────┘
               │ OCI runtime
               ▼
┌───────────────────────────┐
│  Containerd / Docker      │
└───────────────────────────┘
  • Dagger Functions — jednotky pipeline logiky (build, test, publish). Píšete ich v SDK.
  • Dagger Engine — daemon bežiaci ako container, ktorý vykonáva operácie cez BuildKit. Každá funkcia dostane prístup k images, volume mountom, envs, secrets.
  • Dagger Cloud (voliteľné) — SaaS, ktorý pridáva traces, zdieľanú cache a tímový dashboard.

Inštalácia

# macOS/Linux
curl -fsSL https://dl.dagger.io/dagger/install.sh | sh
sudo mv bin/dagger /usr/local/bin/

# Overenie
dagger version

Pre engine nepotrebujete nič navyše — Dagger si ho stiahne a spustí pri prvom volaní.


Praktický príklad 1 — Python projekt

Vytvoríme pipeline, ktorý zbuilduje Python aplikáciu, spustí testy a vytvorí Docker image.

Najprv inicializácia modulu:

cd my-python-app
dagger init --sdk=python --name=ci

Toto vytvorí adresár ci/ s Python SDK skeletonom. Hlavný súbor ci/src/ci/main.py:

import dagger
from dagger import dag, function, object_type

@object_type
class Ci:
    @function
    async def test(self, source: dagger.Directory) -> str:
        """Spusti pytest v Python 3.12 kontajneri."""
        return await (
            dag.container()
            .from_("python:3.12-slim")
            .with_directory("/app", source)
            .with_workdir("/app")
            .with_exec(["pip", "install", "-r", "requirements.txt"])
            .with_exec(["pytest", "-v"])
            .stdout()
        )

    @function
    async def build_image(self, source: dagger.Directory) -> dagger.Container:
        """Postav production image."""
        return (
            dag.container()
            .from_("python:3.12-slim")
            .with_directory("/app", source)
            .with_workdir("/app")
            .with_exec(["pip", "install", "-r", "requirements.txt"])
            .with_entrypoint(["python", "main.py"])
        )

    @function
    async def publish(self, source: dagger.Directory, tag: str) -> str:
        """Build + push do registry."""
        image = await self.build_image(source)
        return await image.publish(f"ttl.sh/my-app:{tag}")

Spustenie:

# Lokálne
dagger call test --source=.

# Build image
dagger call build-image --source=. export --path=./app.tar

# Publish
dagger call publish --source=. --tag=24h

To isté volanie dagger call test --source=. beží identicky na vašom notebooku aj v CI.


Praktický príklad 2 — TypeScript SDK

import { dag, Container, Directory, object, func } from "@dagger.io/dagger"

@object()
class Ci {
  @func()
  async test(source: Directory): Promise<string> {
    return await dag
      .container()
      .from("node:22-alpine")
      .withDirectory("/app", source)
      .withWorkdir("/app")
      .withExec(["npm", "ci"])
      .withExec(["npm", "test"])
      .stdout()
  }

  @func()
  build(source: Directory): Container {
    return dag
      .container()
      .from("node:22-alpine")
      .withDirectory("/src", source)
      .withWorkdir("/src")
      .withExec(["npm", "ci"])
      .withExec(["npm", "run", "build"])
  }
}
dagger call test --source=.
dagger call build --source=. directory --path=dist export --path=./dist

Integrácia s GitHub Actions

Dagger v CI nepotrebuje špeciálny runner — beží v klasickom ubuntu-latest runneri:

name: CI

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Dagger
        run: |
          curl -fsSL https://dl.dagger.io/dagger/install.sh | sh
          sudo mv bin/dagger /usr/local/bin/

      - name: Run tests
        run: dagger call test --source=.
        env:
          DAGGER_CLOUD_TOKEN: ${{ secrets.DAGGER_CLOUD_TOKEN }}

YAML je triviálny — celú zložitosť drží Python/TS kód v ci/. Prechod z GitHub Actions na GitLab CI je záležitosť skopírovania jedného dagger call riadku.


Caching a performance

Dagger využíva BuildKit cache layer-by-layer. V praxi to znamená:

  • Keď zmeníte jeden test súbor, rebuild pip install sa nespustí — vrstva je cache-hit.
  • Pri opakovanom dagger call cache ostáva v engine kontajneri.
  • Dagger Cloud pridáva zdieľanú cache medzi runnermi a lokálnymi vývojármi → prvý push v CI často trvá sekundy namiesto minút.

Secrets a env premenné

token = dag.set_secret("gh_token", os.environ["GITHUB_TOKEN"])

result = (
    dag.container()
    .from_("alpine:3.20")
    .with_secret_variable("GITHUB_TOKEN", token)
    .with_exec(["sh", "-c", "curl -H \"Authorization: token $GITHUB_TOKEN\" ..."])
)

Secrets sa nedajú vypísať cez stdout() ani export() — Dagger engine ich filtruje.


Kedy Dagger použiť a kedy nie

Dobrý fit:

  • Monorepá s viacerými službami, kde YAML duplikácia je bolesť.
  • Tímy, ktoré chcú lokálne debugovať CI (veľká úspora času).
  • Platformové tímy, ktoré pripravujú "zlaté pipelines" pre viacero projektov.
  • Projekty migrujúce medzi CI vendormi.

Horší fit:

  • Minimálne single-service repá, kde 20-riadkový YAML stačí.
  • Tímy bez skúseností s písaním kódu v CI — krivka učenia je reálna.
  • Prostredia bez možnosti spustiť container daemon (niektoré shared runneri).

Ekosystém — Daggerverse

Dagger má Daggerverse — register zdieľaných modulov, ktoré si môžete importovať do vlastného pipeline:

# Pridaj helm modul
dagger install github.com/dagger/dagger/sdk/helm

# Použitie v kode
helm_chart = dag.helm().package(source=source)

Týmto spôsobom vznikajú re-použiteľné building blocky (helm, trivy, snyk, go-releaser, ...) bez copy-paste.


Zhrnutie

Dagger prináša do CI/CD typ premyslenia, ktoré softvérový svet dávno aplikuje na aplikačný kód — verziovanie, testovanie, modularita, lokálne debugovanie. Ak váš tím bojuje s YAML peklom, má viacero projektov so zdieľanými build krokmi, alebo potrebuje prenositeľnosť medzi CI vendormi, Dagger je dnes jedna z najvyspelejších možností.

Pre malý projekt je overkill — ale pre platformový tím, ktorý udržiava CI pre 20 mikroslužieb, je to zásadná zmena.

Odkazy