콘텐츠로 이동

Dagger

Go, Python 또는 TypeScript SDK를 사용하여 컨테이너에서 파이프라인을 실행하는 프로그래밍 가능한 CI/CD 엔진.

설치

CLI 설치

명령어설명
curl -fsSL https://dl.dagger.io/dagger/install.sh | shLinux/macOS에 Dagger CLI 설치
brew install dagger/tap/daggerHomebrew로 macOS에 설치
choco install daggerChocolatey로 Windows에 설치
scoop install daggerScoop으로 Windows에 설치
nix profile install nixpkgs#daggerNixOS/Nix로 설치
dagger version설치된 Dagger 버전 확인

모듈 초기화

명령어설명
dagger init새 Dagger 모듈 초기화
dagger init --sdk=pythonPython SDK로 모듈 초기화
dagger init --sdk=typescriptTypeScript SDK로 모듈 초기화
dagger init --sdk=goGo SDK로 모듈 초기화
dagger init --name=my-pipeline사용자 정의 이름으로 모듈 초기화
dagger init --sdk=go --source=./ci하위 디렉터리에서 초기화

CLI 명령어

핵심 작업

명령어설명
dagger call현재 모듈에서 함수 호출
dagger call buildbuild 함수 호출
dagger call test --source=.소스 인자와 함께 test 함수 호출
dagger call build --source=. export --path=./dist빌드 후 로컬로 출력 내보내기
dagger call lint --source=. stdoutlint 실행 후 stdout 출력
dagger call publish --source=. --tag=v1.0태그와 함께 publish 함수 호출
dagger functions모듈에서 사용 가능한 함수 목록 표시
dagger shell대화형 Dagger 셸 열기
dagger query엔진에 대해 원시 GraphQL 쿼리 실행
dagger run cmdDagger 세션 내에서 명령 실행

인증 및 클라우드

명령어설명
dagger loginDagger Cloud에 인증
dagger logout인증 제거
dagger config모듈 구성 표시
dagger cloud tracesDagger Cloud에서 파이프라인 추적 보기
DAGGER_CLOUD_TOKEN=token dagger call buildDagger Cloud 토큰으로 실행

모듈 관리

종속성

명령어설명
dagger install github.com/org/repo종속성 모듈 설치
dagger install github.com/org/repo@v1.0특정 버전 설치
dagger install github.com/org/repo/subpath하위 디렉터리에서 모듈 설치
dagger developSDK 바인딩 생성/업데이트
dagger develop --sdk=python모듈을 다른 SDK로 전환
dagger mod sync모듈 종속성 동기화
dagger mod update모듈 종속성 업데이트
dagger publish ttl.sh/my-module레지스트리에 모듈 게시
dagger publish ghcr.io/org/module:v1.0GitHub Container Registry에 게시

파이프라인 개발 (Go)

컨테이너 작업

명령어설명
func (m *MyModule) Build(ctx context.Context, src *dagger.Directory) *dagger.Containerbuild 함수 정의
dag.Container().From("golang:1.22")이미지에서 컨테이너 생성
container.WithDirectory("/src", src)컨테이너에 디렉터리 마운트
container.WithExec([]string{"go", "build", "."})컨테이너에서 명령 실행
container.WithEnvVariable("KEY", "value")환경 변수 설정
container.File("/app/binary")컨테이너에서 파일 가져오기
container.Directory("/app/dist")컨테이너에서 디렉터리 가져오기
container.WithServiceBinding("db", dbSvc)서비스 종속성 바인딩
container.WithWorkdir("/src")작업 디렉터리 설정
container.WithUser("nonroot")컨테이너 사용자 설정
container.Publish(ctx, "ttl.sh/my-image")컨테이너를 이미지로 게시
container.Export(ctx, "./output.tar")컨테이너를 tarball로 내보내기

Go 파이프라인 예제

package main

import (
	"context"
	"dagger/my-pipeline/internal/dagger"
)

type MyPipeline struct{}

func (m *MyPipeline) Build(ctx context.Context, src *dagger.Directory) *dagger.Container {
	goCache := dag.CacheVolume("go-mod")
	buildCache := dag.CacheVolume("go-build")

	return dag.Container().
		From("golang:1.22-alpine").
		WithMountedCache("/go/pkg/mod", goCache).
		WithMountedCache("/root/.cache/go-build", buildCache).
		WithDirectory("/src", src).
		WithWorkdir("/src").
		WithExec([]string{"go", "mod", "download"}).
		WithExec([]string{"go", "build", "-o", "/app/server", "./cmd/server"})
}

func (m *MyPipeline) Test(ctx context.Context, src *dagger.Directory) (string, error) {
	return m.Build(ctx, src).
		WithExec([]string{"go", "test", "-v", "./..."}).
		Stdout(ctx)
}

func (m *MyPipeline) Publish(ctx context.Context, src *dagger.Directory, tag string) (string, error) {
	build := m.Build(ctx, src)
	return dag.Container().
		From("cgr.dev/chainguard/static:latest").
		WithFile("/app/server", build.File("/app/server")).
		WithEntrypoint([]string{"/app/server"}).
		Publish(ctx, "ttl.sh/my-app:"+tag)
}

파이프라인 개발 (Python)

컨테이너 작업

명령어설명
@functionDagger에 노출할 함수 데코레이터
async def build(self, src: dagger.Directory) -> dagger.Container비동기 build 함수 정의
dag.container().from_("python:3.12")이미지에서 컨테이너 생성
container.with_directory("/src", src)디렉터리 마운트
container.with_exec(["pip", "install", "-r", "requirements.txt"])pip install 실행
await container.stdout()명령 출력 가져오기
container.with_workdir("/app")작업 디렉터리 설정
container.with_user("nonroot")컨테이너 사용자 설정
await container.publish("ttl.sh/my-app")컨테이너를 이미지로 게시

Python 파이프라인 예제

import dagger
from dagger import dag, function, object_type

@object_type
class MyPipeline:
    @function
    async def build(self, src: dagger.Directory) -> dagger.Container:
        pip_cache = dag.cache_volume("pip")
        return (
            dag.container()
            .from_("python:3.12-slim")
            .with_mounted_cache("/root/.cache/pip", pip_cache)
            .with_directory("/app", src)
            .with_workdir("/app")
            .with_exec(["pip", "install", "--no-cache-dir", "-r", "requirements.txt"])
            .with_exec(["pip", "install", "."])
        )

    @function
    async def test(self, src: dagger.Directory) -> str:
        return await (
            self.build(src)
            .with_exec(["pytest", "-v", "--tb=short"])
            .stdout()
        )

    @function
    async def lint(self, src: dagger.Directory) -> str:
        return await (
            self.build(src)
            .with_exec(["ruff", "check", "."])
            .stdout()
        )

    @function
    async def publish(self, src: dagger.Directory, tag: str) -> str:
        build = self.build(src)
        return await (
            dag.container()
            .from_("cgr.dev/chainguard/python:latest")
            .with_directory("/app", build.directory("/app"))
            .with_entrypoint(["python", "-m", "myapp"])
            .publish(f"ttl.sh/my-python-app:{tag}")
        )

파이프라인 개발 (TypeScript)

TypeScript 파이프라인 예제

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

@object()
class MyPipeline {
  @func()
  build(src: Directory): Container {
    const nodeCache = dag.cacheVolume("node-modules")
    return dag
      .container()
      .from("node:20-slim")
      .withMountedCache("/app/node_modules", nodeCache)
      .withDirectory("/app", src)
      .withWorkdir("/app")
      .withExec(["npm", "ci"])
      .withExec(["npm", "run", "build"])
  }

  @func()
  async test(src: Directory): Promise<string> {
    return this.build(src)
      .withExec(["npm", "test"])
      .stdout()
  }

  @func()
  async lint(src: Directory): Promise<string> {
    return this.build(src)
      .withExec(["npm", "run", "lint"])
      .stdout()
  }

  @func()
  async publish(src: Directory, tag: string): Promise<string> {
    return dag
      .container()
      .from("cgr.dev/chainguard/node:latest")
      .withDirectory("/app", this.build(src).directory("/app/dist"))
      .withEntrypoint(["node", "/app/index.js"])
      .publish(`ttl.sh/my-node-app:${tag}`)
  }
}

캐싱

캐시 볼륨

명령어설명
container.WithMountedCache("/go/pkg", dag.CacheVolume("gomod"))영구 캐시 볼륨 마운트 (Go)
container.with_mounted_cache("/root/.cache/pip", dag.cache_volume("pip"))pip 캐시 마운트 (Python)
dag.CacheVolume("node-modules")명명된 캐시 볼륨 생성
container.WithMountedCache("/root/.cache/go-build", dag.CacheVolume("gobuild"))Go 빌드 아티팩트 캐시
container.WithMountedCache("/root/.npm", dag.CacheVolume("npm"))npm 패키지 캐시
container.WithMountedCache("/root/.cargo/registry", dag.CacheVolume("cargo"))Rust 종속성 캐시

캐시 무효화

명령어설명
container.WithEnvVariable("CACHE_BUSTER", time.Now().String())필요시 캐시 무효화
dagger call --focus=false build자동 포커스/캐싱 없이 실행
container.WithoutEnvVariable("CACHE_BUSTER")캐시 무효화 변수 제거

시크릿 관리

명령어설명
dag.SetSecret("token", os.Getenv("API_TOKEN"))환경 변수에서 시크릿 생성
container.WithSecretVariable("API_KEY", secret)시크릿에서 환경 변수 설정
container.WithMountedSecret("/run/secrets/key", secret)시크릿을 파일로 마운트
dagger call deploy --token=env:API_TOKENCLI를 통해 환경에서 시크릿 전달
dagger call deploy --token=file:./token.txtCLI를 통해 파일에서 시크릿 전달
dagger call deploy --token=cmd:"vault read -field=token secret/app"명령에서 시크릿 전달

서비스

서비스 바인딩

명령어설명
container.WithServiceBinding("db", dbSvc)컨테이너에 서비스 바인딩
container.WithExposedPort(8080)컨테이너에서 포트 노출
container.AsService()컨테이너를 서비스로 변환
dag.Container().From("postgres:16").WithExposedPort(5432).AsService()데이터베이스 서비스 생성
service.Start(ctx)서비스 명시적 시작
service.Stop(ctx)실행 중인 서비스 중지

서비스 바인딩 예제

func (m *MyPipeline) IntegrationTest(ctx context.Context, src *dagger.Directory) (string, error) {
	postgres := dag.Container().
		From("postgres:16-alpine").
		WithEnvVariable("POSTGRES_PASSWORD", "test").
		WithEnvVariable("POSTGRES_DB", "testdb").
		WithExposedPort(5432).
		AsService()

	redis := dag.Container().
		From("redis:7-alpine").
		WithExposedPort(6379).
		AsService()

	return m.Build(ctx, src).
		WithServiceBinding("db", postgres).
		WithServiceBinding("cache", redis).
		WithEnvVariable("DATABASE_URL", "postgres://postgres:test@db:5432/testdb").
		WithEnvVariable("REDIS_URL", "redis://cache:6379").
		WithExec([]string{"go", "test", "-v", "-tags=integration", "./..."}).
		Stdout(ctx)
}

멀티 스테이지 빌드 패턴

func (m *MyPipeline) BuildProduction(ctx context.Context, src *dagger.Directory) *dagger.Container {
	// 1단계: 종속성 빌드
	deps := dag.Container().
		From("golang:1.22-alpine").
		WithDirectory("/src", src, dagger.ContainerWithDirectoryOpts{
			Include: []string{"go.mod", "go.sum"},
		}).
		WithWorkdir("/src").
		WithMountedCache("/go/pkg/mod", dag.CacheVolume("gomod")).
		WithExec([]string{"go", "mod", "download"})

	// 2단계: 바이너리 빌드
	build := deps.
		WithDirectory("/src", src).
		WithExec([]string{"go", "build", "-ldflags=-s -w", "-o", "/app/server", "./cmd/server"})

	// 3단계: 최소 런타임
	return dag.Container().
		From("cgr.dev/chainguard/static:latest").
		WithFile("/app/server", build.File("/app/server")).
		WithExposedPort(8080).
		WithEntrypoint([]string{"/app/server"})
}

CI/CD 통합

GitHub Actions 통합

name: CI with Dagger
on:
  push:
    branches: [main]
  pull_request:

jobs:
  build:
    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 pipeline
        env:
          DAGGER_CLOUD_TOKEN: ${{ secrets.DAGGER_CLOUD_TOKEN }}
        run: |
          dagger call build --source=.
          dagger call test --source=.
          dagger call lint --source=.
      - name: Publish
        if: github.ref == 'refs/heads/main'
        run: |
          dagger call publish --source=. --tag=${{ github.sha }}

GitLab CI 통합

stages:
  - build
  - test

variables:
  DAGGER_CLOUD_TOKEN: $DAGGER_CLOUD_TOKEN

build:
  stage: build
  image: alpine:latest
  before_script:
    - apk add curl
    - curl -fsSL https://dl.dagger.io/dagger/install.sh | sh
    - export PATH=$PWD/bin:$PATH
  script:
    - dagger call build --source=.
    - dagger call test --source=.

디버깅

명령어설명
dagger call --debug build디버그 로깅으로 실행
dagger call build --interactive실패 시 대화형 셸 진입
container.WithExec(["sh", "-c", "cat /etc/os-release"])컨테이너 내용 디버그
container.Terminal()컨테이너에서 터미널 열기 (Go)
export DAGGER_LOG_LEVEL=debug상세 로깅 설정
dagger query '{ container { from(address:"alpine") { exec(args:["ls"]) { stdout } } } }'원시 GraphQL로 디버그
dagger call build 2>&1 | tee dagger-output.log출력을 로그 파일에 저장
container.WithExec(["ls", "-la", "/app"])디버깅용 파일 목록 표시
container.WithExec(["env"])환경 변수 출력

모범 사례

  1. 캐시 볼륨을 적극 활용하세요 — 패키지 관리자(pip, npm, cargo, Go 모듈)용 캐시를 마운트하면 빌드 간 시간을 획기적으로 줄일 수 있습니다.

  2. 모듈을 작고 조합 가능하게 유지하세요 — 대규모 파이프라인을 각각 하나의 작업을 잘 수행하는 재사용 가능한 함수로 분리한 후 조합하세요.

  3. 환경 변수 대신 시크릿을 사용하세요 — 민감한 값을 일반 문자열로 전달하지 말고 항상 dag.SetSecret() 또는 CLI 시크릿 참조를 사용하세요.

  4. 베이스 이미지 버전을 고정하세요 — 재현 가능한 빌드를 위해 latest 대신 golang:1.22와 같은 특정 태그를 사용하세요.

  5. 멀티 스테이지 패턴을 활용하세요 — 빌드 종속성과 런타임을 분리하여 최소한의 최종 컨테이너를 생성하세요.

  6. 통합 테스트에 서비스 바인딩을 사용하세요 — 데이터베이스, 캐시 및 기타 종속성을 모킹 대신 서비스로 실행하세요.

  7. 아티팩트를 명시적으로 내보내세요.Export() 또는 .Publish()를 사용하여 Dagger 엔진 외부에서 출력을 사용할 수 있게 하세요.

  8. Dagger Cloud를 활성화하세요 — Dagger Cloud에 연결하여 파이프라인 캐싱, 추적 및 협업 디버깅을 활용하세요.

  9. 디버깅에 --interactive를 사용하세요 — 단계가 실패하면 --interactive가 실패 지점의 컨테이너로 진입합니다.

  10. CI 전에 로컬에서 테스트하세요 — Dagger 파이프라인은 노트북과 CI에서 동일하게 실행되므로 모든 것을 먼저 로컬에서 검증하세요.