콘텐츠로 이동

Dagger

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

명령어설명
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하위 디렉터리에서 초기화
명령어설명
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에 게시
명령어설명
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로 내보내기
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)
}
명령어설명
@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")컨테이너를 이미지로 게시
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}")
        )
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"})
}
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 }}
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에서 동일하게 실행되므로 모든 것을 먼저 로컬에서 검증하세요.