Skip to content

필사 모드: Dagger로 CI/CD 파이프라인을 코드로 관리하기: 로컬에서 CI까지 동일한 파이프라인

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

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

"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

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

**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 옵션을 사용합니다.

현재 단락 (1/257)

Dagger는 CI/CD 파이프라인을 프로그래밍 언어(Go, Python, TypeScript 등)로 작성할 수 있게 해주는 오픈소스 도구입니다. 핵심 가치는 **"Write on...

작성 글자: 0원문 글자: 7,384작성 단락: 0/257