Produkty Poradenství O nás Blog Kontakt English
arrow_back Zpět na blog

Dagger: Slibný CI jako kód (pro backendy, ne pro mobil)

Dagger: Slibný CI jako kód (pro backendy, ne pro mobil)

Letos jsem po delší pauze vyrazil na Devoxx s kolegy — naposledy jsem byl na Devoxx Krakow před pár lety. Kromě skvělé (a hodně vtipné) přednášky od Joshe Longa na mě silně zapůsobila jedna další: Paul Dragoonis o Daggeru. Pro mobilní vývoj se to nehodí, ale pro multi-platformní buildy — myslím tím Intel a ARM architektury — by to mohlo věci výrazně zjednodušit.

Problém s maticí

Každá CI pipelina začíná jednoduše. Pak jí naroste matice.

YAML
strategy:
  matrix:
    os: [macos-latest, windows-latest]
    version: [12, 14, 16]
    environment: [staging, production]
    exclude:
      - os: macos-latest
        version: 12
      # ... rostoucí seznam speciálních případů

Tohle je slide, který Paul Dragoonis na Devoxx promítl, a ten okamžik, kdy se YAML přetvářka rozsype. Co se tu reálně děje, je kartézský součin (2 × 3 × 2 = 12 jobů), filtrovaný seznamem speciálních případů, plus implicitní kontrakt o tom, jak se každá osa propaguje do build kroků níže. Funguje to — dokud nepotřebujete cokoli z tohoto:

  • Parametr (“spusť tuhle matici, ale tenhle týden s extra debug flagy”)
  • Sdílenou logiku napříč buňkami (“každá buňka potřebuje stejný setup, kromě buňky X”)
  • Deduplikaci (stejná matice se opakuje ve třech workflow souborech)
  • Podmíněné chování bohatší než exclude: pravidla

YAML vám žádnou z těchto věcí nedá bez copy-paste nebo nějakého templating preprocesoru, který nikdo v týmu nemá rád. Je to konfigurační soubor, který se vydává za programovací jazyk, a švy se ukážou v momentě, kdy pipelina přeroste triviální velikost.

V Daggeru je matice jen seznam a build je jen smyčka. Tady je multi-platformní Go build přímo z přednášky:

Python
# platforms to build for and push in a multi-platform image
platforms = [
    dagger.Platform("linux/amd64"),  # a.k.a. x86_64
    dagger.Platform("linux/arm64"),  # a.k.a. aarch64
    dagger.Platform("linux/s390x"),  # a.k.a. IBM S/390
]

for platform in platforms:
    my_container = (
        dag.container(platform=platform)
        .from_("golang:1.20-alpine")
        .with_directory("/src", src)
        .with_directory("/output", dag.directory())
        .with_env_variable("CGO_ENABLED", "0")
        .with_workdir("/src")
        .with_exec(["go", "build", "-o", "/output/hello"])
    )

Matice je Python list. Build je for smyčka. dag.container(platform=platform) říká Daggeru, ať stáhne Go image pro danou architekturu — Buildkit pod tím zařídí cross-architecture práci. Přidejte další cíl, smyčka ho pokryje. Přeskočte jeden podmíněně, je to prostě if. Vytáhněte tělo do helperu, napište mu unit testy, dejte si tam breakpoint — je to prostě Python.

Stejný model v TypeScriptu vypadá tak, jak byste čekali:

TYPESCRIPT
// define build matrix
const gooses = ["linux", "darwin"]
const goarches = ["amd64", "arm64"]

let outputs = dag.directory()

const golang = dag
  .container()
  .from("golang:latest")
  .withDirectory("/src", src)
  .withWorkdir("/src")

// ... vnořená smyčka přes gooses × goarches, nastavující GOOS/GOARCH pro každý build

Vyberte si SDK, které sedí vašemu týmu — Python, TypeScript, Java, Go, .NET, PHP. Tvar pipeliny zůstává stejný.

Pointa: pipelina už není skrytý program, který existuje jen ve chvíli, kdy se rozjede runner — je to reálný program, spouštěný přes dagger call. Stejný call funguje na notebooku i v CI, bit po bitu identicky, protože exekuční prostředí je kontejner, ne runner.

(Jedna hvězdička: osa os: z původního YAML. Dagger zvládá architekturní rozdíly — amd64, arm64, dokonce i s390x — čistě přes multi-arch kontejnery, což je opravdu užitečné pro Intel/ARM matice. Hostitelské OS jako macOS a Windows jsou jiná písnička; o tom dál.)

Co Dagger vlastně je

Dagger má dvě části:

  1. Engine, který vše spouští uvnitř kontejnerů, s content-addressable cache a DAG ve stylu Buildkitu pod kapotou.
  2. SDK pro Go, Python, TypeScript, Javu, PHP a .NET, kterými popíšete pipelinu jako kód.

Vaše pipelina se stává programem. Kompiluje se, můžete importovat knihovny, psát na ni unit testy, nastavovat breakpointy. Když ji spustíte, SDK komunikuje s Dagger enginem, který každý krok plánuje jako operaci v kontejneru.

CI vendor (GitHub Actions, GitLab, atd.) se stává tenkou slupkou: klonuj repo, nainstaluj Dagger, spusť jeden příkaz. Veškerá skutečná logika žije ve vašem repozitáři, ve vašem jazyce.

Killer feature: opakovatelnost

Tady je scénář, pro který je Dagger postavený. Vaše testovací sada potřebuje:

  • Postgres (s konkrétně nainstalovaným rozšířením)
  • Redis
  • Migrační krok, který musí proběhnout před testy
  • Tři testovací skupiny, které mohou běžet paralelně

V YAML světě napíšete services: blok, doufáte, že síťovka CI providera odpovídá vašemu lokálnímu Docker Compose, a divergenci objevíte, když se něco rozbije ve dvě ráno.

V Daggeru tohle napíšete jednou. Tady je tvar, který padl během Devoxx demo:

Python
import dagger
from dagger import dag, function, object_type, up

@object_type
class Book:

    @function
    @up
    def serve(self) -> dagger.Service:
        """Serves the application"""
        postgresdb = (
            dag.container()
            .from_("postgres:alpine")
            .with_env_variable("POSTGRES_DB", "app")
            .with_env_variable("POSTGRES_PASSWORD", "app_secret")
            .with_exposed_port(5432)
            .as_service(args=[], use_entrypoint=True)
        )
        # ... napojte postgresdb na app kontejner a vraťte službu

Dva dekorátory tu odvádí spoustu práce:

  • @function udělá serve volatelný z Dagger CLI. CI spustí dagger call serve a dostane službu.
  • @up udělá tu samou funkci spustitelnou jako lokální dev prostředí. Váš notebook spustí dagger call serve up a dostane Postgres plus aplikaci, s forwardovanými porty, připravené otevřít v prohlížeči.

Pointa, kterou přednášející uzavřel: konec docker-compose.yml. Stejný Python soubor, který definuje váš CI integration-test setup, definuje i váš lokální dev stack. Jeden zdroj pravdy, jeden exekuční model, jedna kontejnerová topologie — ať mačkáte cmd+R na notebooku, nebo sledujete runner v CI.

Tohle je opakovatelnost, kterou YAML pipeliny nikdy nedají, protože YAML pipeliny nejsou věc, která existuje mimo runner od CI providera.

Co se mi líbilo

Kromě opakovatelnosti, kterou už jsem zmínil výše, mě po přednášce uchytily dvě věci.

Cache. Dagger cache je content-addressable — každý krok má hash odvozený od svých vstupů (stav rodičovského kontejneru, příkaz, namountované soubory), takže nezměněné kroky se kompletně přeskočí místo opětovného spuštění. S Dagger Cloud je cache sdílená napříč stroji, takže běh navazuje na vrstvu, kde ten poslední — váš nebo kolegův, lokální nebo CI — skončil. To je jiný model než per-job runner cache v GitHub Actions nebo GitLabu, a na netriviální pipelině se to rychle sčítá.

Dagger Cloud UI je taky upřímně pěkné: vizuální DAG pipeliny, časování jednotlivých kroků, cache hit/miss indikátory na první pohled. Free tier je dostatečně štědrý, aby měl smysl pro malý tým nebo side project. Enterprise plán něco stojí — ale uděláte spoustu věcí, než narazíte na strop.

Daggerverse

Daggerverse je Dagger registr modulů — znovupoužitelné pipeline moduly publikované komunitou. Potřebujete publikovat Maven artefakt? Existuje modul. Potřebujete spustit Trivy nad kontejnerem? Modul. Potřebujete deploynout na Fly.io? Modul.

Importujete je, jak byste importovali knihovnu:

Bash
dagger install github.com/some-org/some-module

Tohle je opravdu nové. GitHub Actions má marketplace actions, ale jsou to bash skripty slepené YAMLem bez typové bezpečnosti. Dagger moduly jsou typované, kompozovatelné a dají se číst jako kód.

Co Dagger neřeší

V poslední době vyvíjím slušnou hromadu iOS a Android aplikací a tam Dagger se svým container-first modelem naráží na tvrdý limit. iOS buildy potřebují Xcode, Xcode potřebuje macOS a macOS nepoběží uvnitř Linuxového kontejneru. Dokud nebude smysluplný iOS-runner příběh pro Dagger (a moc tomu nevěřím, vzhledem k tomu, jak Apple drží svůj toolchain), užitečnost v mobilním světě je omezená. Android je o trochu vstřícnější — SDK běží na Linuxu — ale toolchain stále tahá za sebou emulátory a podpisové flow, které se v efemérních kontejnerech ne vždy cítí jako doma.

Pro server-side aplikace s Vue (nebo jakýmkoli web) frontendem nad nimi ovšem padne přesně. Build, test, integrační test proti reálným službám, zabalení do kontejneru, deploy — každý krok je Linuxový kontejner, který Dagger umí zorchestrovat. Tam bych pro to sáhl jako první.

Závěr

Dagger je první CI nástroj po letech, který mě donutil přehodnotit, jak píšu pipeliny. YAML běžecký pás je daň, kterou všichni platí a nikdo ji nezpochybňuje — pipeliny by měly být kód, a Dagger to dělá prakticky proveditelným, aniž byste museli opouštět svého stávajícího CI providera.

Není to pro každý workload. Pokud je váš build gradle build && deploy, nepotřebujete programovatelný engine — potřebujete dvacetiřádkový workflow soubor. Ale v momentě, kdy vaší pipelině narostou služby, paralelní testovací skupiny, podmíněné kroky nebo sdílená logika napříč repozitáři, Dagger se začíná vyplácet.

Pro svůj další backendový projekt s netriviálním integračním test setupem začínám tam.


Titulní foto: frank mckenna na Unsplash.

Další z blogu