- Published on
Dagger로 CI/CD 파이프라인을 코드로 관리하기: 로컬에서 CI까지 동일한 파이프라인
- Authors
- Name
- Dagger란?
- 설치 및 프로젝트 초기화
- 첫 번째 파이프라인: Go 프로젝트 빌드
- Python SDK로 파이프라인 작성
- 캐싱 전략
- 시크릿 관리
- CI 통합
- 멀티 플랫폼 빌드
- 통합 파이프라인: 전체 CI/CD
- Dagger vs 기존 CI 도구 비교
- 트러블슈팅
Dagger란?
Dagger는 CI/CD 파이프라인을 프로그래밍 언어(Go, Python, TypeScript 등)로 작성할 수 있게 해주는 오픈소스 도구입니다. 핵심 가치는 "Write once, run anywhere" — 로컬 노트북에서 실행한 파이프라인이 GitHub Actions, GitLab CI, Jenkins 어디서든 동일하게 동작합니다.
기존 CI/CD의 문제점
- YAML 지옥: 복잡한 파이프라인을 YAML로 표현하면 가독성과 재사용성이 떨어짐
- 로컬 테스트 불가: CI에서만 실행 가능 → 피드백 루프가 길어짐
- 벤더 종속: GitHub Actions와 GitLab CI의 문법이 완전히 다름
- 디버깅 어려움: CI 환경을 로컬에 재현하기 힘듬
Dagger의 해결책
로컬 개발 ──→ Dagger Engine ──→ 동일한 결과
GitHub Actions ──→ Dagger Engine ──→ 동일한 결과
GitLab CI ──→ Dagger Engine ──→ 동일한 결과
Dagger Engine은 모든 작업을 컨테이너에서 실행하므로 환경 차이가 없습니다.
설치 및 프로젝트 초기화
Dagger CLI 설치
# macOS
brew install dagger/tap/dagger
# Linux
curl -fsSL https://dl.dagger.io/dagger/install.sh | sh
# 버전 확인
dagger version
프로젝트 초기화
# Go SDK 사용
mkdir my-pipeline && cd my-pipeline
dagger init --sdk=go --name=my-pipeline
# 생성된 파일 구조
# ├── dagger.json
# ├── main.go
# └── go.mod
첫 번째 파이프라인: Go 프로젝트 빌드
main.go 작성
package main
import (
"context"
"dagger/my-pipeline/internal/dagger"
)
type MyPipeline struct{}
// Build 함수: Go 프로젝트를 빌드합니다
func (m *MyPipeline) Build(ctx context.Context, source *dagger.Directory) *dagger.Container {
// Go 빌드 환경 설정
builder := dag.Container().
From("golang:1.22-alpine").
WithDirectory("/src", source).
WithWorkdir("/src").
WithEnvVariable("CGO_ENABLED", "0").
WithExec([]string{"go", "mod", "download"}).
WithExec([]string{"go", "build", "-o", "/app", "./cmd/server"})
// 경량 런타임 이미지
return dag.Container().
From("alpine:3.19").
WithFile("/app", builder.File("/app")).
WithEntrypoint([]string{"/app"})
}
// Test 함수: 테스트 실행
func (m *MyPipeline) Test(ctx context.Context, source *dagger.Directory) (string, error) {
return dag.Container().
From("golang:1.22-alpine").
WithDirectory("/src", source).
WithWorkdir("/src").
WithExec([]string{"go", "mod", "download"}).
WithExec([]string{"go", "test", "-v", "-race", "./..."}).
Stdout(ctx)
}
// Lint 함수: golangci-lint 실행
func (m *MyPipeline) Lint(ctx context.Context, source *dagger.Directory) (string, error) {
return dag.Container().
From("golangci/golangci-lint:v1.57").
WithDirectory("/src", source).
WithWorkdir("/src").
WithExec([]string{"golangci-lint", "run", "--timeout", "5m"}).
Stdout(ctx)
}
로컬 실행
# 빌드
dagger call build --source=.
# 테스트
dagger call test --source=.
# 린트
dagger call lint --source=.
# 빌드 결과를 Docker 이미지로 내보내기
dagger call build --source=. export --path=./image.tar
Python SDK로 파이프라인 작성
# dagger/src/main/__init__.py
import dagger
from dagger import dag, function, object_type
@object_type
class MyPipeline:
@function
async def test(self, source: dagger.Directory) -> str:
"""Python 프로젝트 테스트"""
return await (
dag.container()
.from_("python:3.12-slim")
.with_directory("/src", source)
.with_workdir("/src")
.with_exec(["pip", "install", "-r", "requirements.txt"])
.with_exec(["pip", "install", "pytest", "pytest-cov"])
.with_exec(["pytest", "-v", "--cov=app", "--cov-report=term-missing"])
.stdout()
)
@function
async def build(self, source: dagger.Directory) -> dagger.Container:
"""Docker 이미지 빌드"""
# 의존성 캐싱
pip_cache = dag.cache_volume("pip-cache")
return (
dag.container()
.from_("python:3.12-slim")
.with_mounted_cache("/root/.cache/pip", pip_cache)
.with_directory("/app", source)
.with_workdir("/app")
.with_exec(["pip", "install", "--no-cache-dir", "-r", "requirements.txt"])
.with_entrypoint(["python", "main.py"])
)
@function
async def publish(
self,
source: dagger.Directory,
registry: str = "ghcr.io",
image_name: str = "my-app",
tag: str = "latest",
registry_user: dagger.Secret | None = None,
registry_pass: dagger.Secret | None = None,
) -> str:
"""이미지 빌드 및 레지스트리 푸시"""
container = await self.build(source)
if registry_user and registry_pass:
container = container.with_registry_auth(
registry,
await registry_user.plaintext(),
registry_pass,
)
ref = f"{registry}/{image_name}:{tag}"
digest = await container.publish(ref)
return digest
캐싱 전략
Dagger의 캐싱은 빌드 속도를 극적으로 개선합니다:
func (m *MyPipeline) BuildWithCache(ctx context.Context, source *dagger.Directory) *dagger.Container {
// Go 모듈 캐시
goModCache := dag.CacheVolume("go-mod-cache")
goBuildCache := dag.CacheVolume("go-build-cache")
return dag.Container().
From("golang:1.22-alpine").
WithMountedCache("/go/pkg/mod", goModCache).
WithMountedCache("/root/.cache/go-build", goBuildCache).
WithDirectory("/src", source).
WithWorkdir("/src").
WithExec([]string{"go", "build", "-o", "/app", "./cmd/server"})
}
캐싱 효과 비교
첫 번째 빌드: 2분 30초 (의존성 다운로드 포함)
두 번째 빌드: 15초 (캐시 히트)
소스만 변경: 20초 (의존성 캐시 재사용)
시크릿 관리
func (m *MyPipeline) Deploy(
ctx context.Context,
source *dagger.Directory,
kubeconfig *dagger.Secret,
registryToken *dagger.Secret,
) (string, error) {
// 시크릿은 로그에 노출되지 않음
return dag.Container().
From("bitnami/kubectl:latest").
WithMountedSecret("/root/.kube/config", kubeconfig).
WithSecretVariable("REGISTRY_TOKEN", registryToken).
WithDirectory("/manifests", source.Directory("k8s")).
WithExec([]string{"kubectl", "apply", "-f", "/manifests/"}).
Stdout(ctx)
}
# 로컬 실행 시 시크릿 전달
dagger call deploy \
--source=. \
--kubeconfig=file:$HOME/.kube/config \
--registry-token=env:REGISTRY_TOKEN
CI 통합
GitHub Actions
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dagger/dagger-for-github@v6
with:
version: 'latest'
verb: call
args: test --source=.
- uses: dagger/dagger-for-github@v6
with:
version: 'latest'
verb: call
args: lint --source=.
- uses: dagger/dagger-for-github@v6
with:
version: 'latest'
verb: call
args: build --source=.
GitLab CI
# .gitlab-ci.yml
stages:
- test
- build
test:
stage: test
image: registry.dagger.io/engine:latest
services:
- docker:dind
script:
- dagger call test --source=.
build:
stage: build
image: registry.dagger.io/engine:latest
services:
- docker:dind
script:
- dagger call build --source=. export --path=image.tar
artifacts:
paths:
- image.tar
멀티 플랫폼 빌드
func (m *MyPipeline) BuildMultiPlatform(
ctx context.Context,
source *dagger.Directory,
) (string, error) {
platforms := []dagger.Platform{
"linux/amd64",
"linux/arm64",
}
platformVariants := make([]*dagger.Container, len(platforms))
for i, platform := range platforms {
platformVariants[i] = dag.Container(dagger.ContainerOpts{Platform: platform}).
From("golang:1.22-alpine").
WithDirectory("/src", source).
WithWorkdir("/src").
WithEnvVariable("CGO_ENABLED", "0").
WithExec([]string{"go", "build", "-o", "/app", "./cmd/server"})
}
// 멀티 플랫폼 이미지 푸시
digest, err := dag.Container().
Publish(ctx, "ghcr.io/my-org/my-app:latest",
dagger.ContainerPublishOpts{
PlatformVariants: platformVariants,
})
return digest, err
}
통합 파이프라인: 전체 CI/CD
func (m *MyPipeline) CI(ctx context.Context, source *dagger.Directory) error {
// 1. 린트
_, err := m.Lint(ctx, source)
if err != nil {
return fmt.Errorf("lint failed: %w", err)
}
fmt.Println("✅ Lint passed")
// 2. 테스트
_, err = m.Test(ctx, source)
if err != nil {
return fmt.Errorf("test failed: %w", err)
}
fmt.Println("✅ Tests passed")
// 3. 빌드
container := m.Build(ctx, source)
_, err = container.Sync(ctx)
if err != nil {
return fmt.Errorf("build failed: %w", err)
}
fmt.Println("✅ Build succeeded")
return nil
}
Dagger vs 기존 CI 도구 비교
| 특성 | GitHub Actions | GitLab CI | Dagger |
|---|---|---|---|
| 파이프라인 정의 | YAML | YAML | Go/Python/TS |
| 로컬 실행 | act (제한적) | 불가 | 완전 지원 |
| 디버깅 | 어려움 | 어려움 | IDE 디버거 사용 |
| 벤더 종속 | GitHub | GitLab | 없음 |
| 캐싱 | 수동 설정 | 수동 설정 | 자동 (콘텐츠 기반) |
| 재사용성 | 마켓플레이스 | 템플릿 | 패키지/모듈 |
트러블슈팅
자주 발생하는 문제와 해결 방법
# Dagger Engine 상태 확인
docker ps | grep dagger-engine
# 캐시 초기화
dagger query <<< '{ defaultPlatform }'
# 상세 로그 출력
dagger call --debug test --source=.
# 특정 단계에서 쉘 접속 (디버깅)
dagger call build --source=. terminal
📝 확인 퀴즈 (5문제)
Q1. Dagger가 기존 CI/CD 도구 대비 가장 큰 장점은?
로컬과 CI 환경에서 동일한 파이프라인을 실행할 수 있으며, 프로그래밍 언어로 파이프라인을 작성하므로 IDE 지원, 타입 안전성, 디버깅이 가능합니다.
Q2. Dagger에서 캐싱을 구현하는 방법은?
dag.CacheVolume()으로 캐시 볼륨을 생성하고 WithMountedCache()로 컨테이너에 마운트합니다. 콘텐츠 기반으로 자동 캐싱됩니다.
Q3. Dagger에서 시크릿을 안전하게 전달하는 방법은?
함수 파라미터로 dagger.Secret 타입을 받고, WithMountedSecret이나 WithSecretVariable로 컨테이너에 전달합니다. 로그에 노출되지 않습니다.
Q4. GitHub Actions에서 Dagger를 사용하려면 어떤 액션을 사용하나요?
dagger/dagger-for-github@v6 액션을 사용합니다.
Q5. Dagger에서 멀티 플랫폼 빌드를 하려면 어떤 옵션을 사용하나요?
dagger.ContainerOpts의 Platform 필드와 Publish의 PlatformVariants 옵션을 사용합니다.