Dagger로 CI/CD를 코드로 작성하기: 로컬과 CI에서 같은 파이프라인, 캐시, 모듈
이 글의 핵심
Dagger는 CI/CD 파이프라인을 YAML이 아닌 Go·Python·TypeScript 코드로 작성하게 해주는 컨테이너 기반 실행 엔진입니다. 파이프라인이 컨테이너 안에서 실행되므로 로컬과 GitHub Actions·GitLab·Jenkins에서 같은 단계를 재현하기 쉽고, 단계별 캐시로 바뀌지 않은 작업을 건너뜁니다. 이 글은 설치·첫 파이프라인·Functions·모듈·CI 통합과 함께, 배포 단계가 캐시되어 실행되지 않는 문제처럼 실제로 부딪히는 함정을 다룹니다.
이 글의 핵심
Dagger는 “CI/CD를 YAML 대신 실제 프로그래밍 언어로 작성하자”는 문제의식에서 시작된 오픈소스 실행 엔진입니다. Docker를 만든 Solomon Hykes가 공동 창업한 팀이 개발하고 있으며, 내부적으로는 Docker의 빌드 엔진인 BuildKit 위에서 동작합니다.
핵심 가치:
- 언어: Go / Python / TypeScript SDK — 타입 안전, IDE 자동완성, 테스트 가능
- 이식성: 로컬·GitHub Actions·GitLab·Jenkins·CircleCI 어디서든 동일 실행
- 캐시: BuildKit 기반 단계별 자동 캐싱으로 재실행 속도 극적 단축
- Functions: 재사용 가능한 파이프라인 단위, npm 모듈처럼 공유
- 디버깅: 파이프라인 단계를 로컬에서 그대로 실행 → “CI에서만 터지는” 버그를 로컬에서 재현하기 쉬움
이 글은 설치부터 프로덕션 파이프라인 구축·Functions 공유·CI 통합까지 실전 중심으로 정리합니다.
왜 Dagger인가
YAML의 한계
# 전형적인 GitHub Actions
jobs:
test:
steps:
- run: npm ci
- run: npm test
- run: npm run build
- 로직 분기·반복은 별도의 YAML 문법
- 로컬 재현이 불가능 (GitHub Actions 러너 환경만 정확)
- 재사용 단위는 Action이지만 파라미터·리턴값 제한
- 복잡한 파이프라인은 YAML 수천 줄이 됨
Dagger
func (m *Pkglog) Test(ctx context.Context, source *dagger.Directory) (string, error) {
return dag.Container().
From("node:20").
WithDirectory("/app", source).
WithWorkdir("/app").
WithExec([]string{"npm", "ci"}).
WithExec([]string{"npm", "test"}).
Stdout(ctx)
}
- Go 코드, 타입 체크·테스트·리팩터링 가능
dagger call test --source=.로 로컬에서 CI 잡 실행func반환값(String, Directory, File, Container)이 자동으로 다음 단계 입력으로 연결
이 코드를 읽을 때 알아 두어야 할 점은 WithExec 같은 메서드 체인이 호출하는 순간 실행되지 않는다는 것입니다. 각 메서드는 “이 컨테이너에 이 명령을 적용한 결과”라는 그래프 노드를 만들 뿐이고, Stdout(ctx)나 Export, Publish처럼 실제 값이 필요한 호출이 있을 때 엔진이 그래프 전체를 계산합니다. 그래서 중간 단계를 여러 함수에서 공유해도 같은 입력이면 한 번만 실행되고, 서로 의존하지 않는 가지는 병렬로 돌아갑니다. 반대로 말하면, 체인 중간에 fmt.Println을 넣어도 그 시점에는 명령이 아직 실행되지 않았으므로 로그로 진행 상황을 확인하는 방식이 통하지 않습니다. 디버깅은 뒤에서 다룰 --progress=plain이나 dagger shell, Terminal()로 해야 합니다.
YAML과 비교한 트레이드오프도 분명합니다. YAML 워크플로는 CI 플랫폼이 제공하는 매트릭스, 승인 게이트, 캐시 액션, 마켓플레이스 액션과 바로 연결되지만, Dagger로 옮긴 단계는 그 안에서 컨테이너로 실행되므로 플랫폼 기능 일부(액션의 캐시, 러너에 설치된 도구)를 직접 쓰기 어렵습니다. 또 CI 로그가 “dagger call 한 줄”로 뭉쳐 보이기 때문에, 단계별 로그를 CI 화면에서 바로 보던 팀은 처음에 불편해합니다. 저는 이런 이유로 트리거·권한·승인 같은 오케스트레이션은 YAML에 남기고, 빌드·테스트·패키징 같은 작업 내용만 Dagger 함수로 옮기는 경계를 두는 편이 가장 무리가 없었습니다.
설치
CLI
# macOS / Linux
curl -L https://dl.dagger.io/dagger/install.sh | BIN_DIR=$HOME/.local/bin sh
# 또는 Homebrew
brew install dagger/tap/dagger
# Windows (PowerShell)
iwr -useb https://dl.dagger.io/dagger/install.ps1 | iex
dagger version
Docker Desktop 또는 Docker Engine이 로컬에 설치되어 있어야 합니다(Dagger Engine 실행용).
첫 모듈 초기화
mkdir my-ci && cd my-ci
dagger init --sdk=go --name=pkglog
# 디렉터리 구조
# dagger.json
# .dagger/
# main.go
# go.mod
Python은 --sdk=python, TypeScript는 --sdk=typescript를 씁니다. dagger init은 SDK에 맞는 코드 생성 파일(Go의 경우 internal/dagger 패키지)을 만들어 두는데, 이 생성 코드는 CLI·엔진 버전에 맞춰 만들어집니다. CLI를 업그레이드한 뒤 undefined: dagger.XXX 같은 컴파일 에러가 나거나 IDE 자동완성이 맞지 않는다면 dagger develop으로 생성 코드를 다시 만들면 대부분 해결됩니다. 팀원마다 CLI 버전이 다르면 이런 차이가 반복되므로, dagger.json의 engineVersion과 CI의 CLI 버전을 맞춰 두는 것이 좋습니다.
첫 번째 파이프라인: Node 앱 테스트 + 빌드
Go SDK
// .dagger/main.go
package main
import (
"context"
"dagger/pkglog/internal/dagger"
)
type Pkglog struct{}
// source 디렉터리로 테스트
func (m *Pkglog) Test(
ctx context.Context,
// +defaultPath="."
source *dagger.Directory,
) (string, error) {
return m.nodeBase(source).
WithExec([]string{"npm", "test"}).
Stdout(ctx)
}
// Astro 프로덕션 빌드 → dist 디렉터리 반환
func (m *Pkglog) Build(
ctx context.Context,
// +defaultPath="."
source *dagger.Directory,
) *dagger.Directory {
return m.nodeBase(source).
WithExec([]string{"npm", "run", "build"}).
Directory("/app/dist")
}
// 내부 헬퍼: node 베이스 이미지 + 의존성 설치
func (m *Pkglog) nodeBase(source *dagger.Directory) *dagger.Container {
return dag.Container().
From("node:20-alpine").
WithMountedCache("/root/.npm", dag.CacheVolume("node-npm")).
WithDirectory("/app", source).
WithWorkdir("/app").
WithExec([]string{"npm", "ci"})
}
# 로컬 실행
dagger call test --source=.
dagger call build --source=. export --path=./dist
같은 명령이 CI에서도 동작합니다.
// +defaultPath="." 주석은 Go 문법상으로는 평범한 주석이지만, Dagger가 읽는 프라그마입니다. 이 인자를 생략하면 모듈 루트 디렉터리가 기본값으로 들어가므로 dagger call test만 입력해도 됩니다. 주의할 점은 소스 디렉터리가 엔진으로 업로드되는 입력이라는 것입니다. node_modules나 .git, 빌드 산출물까지 통째로 넘기면 업로드 자체가 느려지고, 그 안의 파일이 조금만 바뀌어도 이후 단계 캐시가 모두 무효가 됩니다. // +ignore=["node_modules", ".git", "dist"] 프라그마로 제외 목록을 지정해 두는 것이 사실상 필수입니다. 또 로컬의 node_modules가 함께 올라가면 컨테이너 안의 npm ci 결과와 섞여, macOS에서 설치된 네이티브 모듈이 Linux 컨테이너에서 invalid ELF header로 실패하는 식의 문제도 생깁니다.
TypeScript SDK
import { dag, Container, Directory, object, func } from "@dagger.io/dagger"
@object()
export class Pkglog {
@func()
async test(source: Directory): Promise<string> {
return this.nodeBase(source)
.withExec(["npm", "test"])
.stdout()
}
@func()
build(source: Directory): Directory {
return this.nodeBase(source)
.withExec(["npm", "run", "build"])
.directory("/app/dist")
}
private nodeBase(source: Directory): Container {
return dag
.container()
.from("node:20-alpine")
.withMountedCache("/root/.npm", dag.cacheVolume("node-npm"))
.withDirectory("/app", source)
.withWorkdir("/app")
.withExec(["npm", "ci"])
}
}
Python SDK
import dagger
from dagger import dag, function, object_type, Directory
@object_type
class Pkglog:
@function
async def test(self, source: Directory) -> str:
return await self._node_base(source).with_exec(["npm", "test"]).stdout()
@function
def build(self, source: Directory) -> Directory:
return self._node_base(source).with_exec(["npm", "run", "build"]).directory("/app/dist")
def _node_base(self, source: Directory) -> dagger.Container:
return (
dag.container()
.from_("node:20-alpine")
.with_mounted_cache("/root/.npm", dag.cache_volume("node-npm"))
.with_directory("/app", source)
.with_workdir("/app")
.with_exec(["npm", "ci"])
)
캐시: 속도의 비결
WithMountedCache("/root/.npm", dag.CacheVolume("node-npm"))
CacheVolume은 여러 실행 간 공유되는 영속 볼륨입니다.
npm install에서 다운로드된 패키지가 다음 실행에 재사용됨- Maven
.m2, Go module cache, Cargo registry, pip cache 등에 적용
또 Dagger는 각 단계의 입력(기반 이미지, 파일 내용, 명령, 환경 변수)을 기준으로 단계별 캐시를 구축합니다. 이전과 같은 입력이면 그 단계는 다시 실행하지 않고 결과를 재사용합니다.
CacheVolume과 단계 캐시는 성격이 다릅니다. 단계 캐시는 “입력이 같으면 결과를 통째로 재사용”하는 것이라 한 글자만 바뀌어도 처음부터 다시 실행하지만, CacheVolume은 입력이 바뀌어 단계가 다시 실행될 때도 안의 내용이 남아 있는 디렉터리라서 npm ci가 패키지를 다시 내려받지 않게 해 줍니다. 대신 캐시 볼륨의 내용은 캐시 키에 포함되지 않으므로, 빌드 결과물처럼 정확성이 중요한 파일을 캐시 볼륨에만 두면 이전 실행의 찌꺼기가 섞일 수 있습니다. 캐시 볼륨에는 “없어도 되지만 있으면 빠른” 다운로드 캐시만 두는 것이 원칙입니다.
실전: 의존성 단계 분리
func (m *Pkglog) nodeDeps(source *dagger.Directory) *dagger.Container {
return dag.Container().
From("node:20-alpine").
WithMountedCache("/root/.npm", dag.CacheVolume("node-npm")).
// package.json만 먼저 복사 → 의존성 설치
WithFile("/app/package.json", source.File("package.json")).
WithFile("/app/package-lock.json", source.File("package-lock.json")).
WithWorkdir("/app").
WithExec([]string{"npm", "ci"})
}
func (m *Pkglog) Test(ctx context.Context, source *dagger.Directory) (string, error) {
return m.nodeDeps(source).
WithDirectory("/app", source). // 소스 코드는 마지막에
WithExec([]string{"npm", "test"}).
Stdout(ctx)
}
package.json 변경 없이 테스트 코드만 수정하면 npm ci 단계의 입력(이미지, 두 파일, 명령)이 그대로이므로 캐시가 적중하고, 설치를 건너뛴 채 테스트만 다시 실행됩니다. Dockerfile에서 COPY package*.json 후 RUN npm ci를 먼저 두는 레이어 캐시 기법과 같은 원리입니다. 다만 뒤의 WithDirectory("/app", source)는 설치된 node_modules 위에 소스를 덮어쓰므로, 소스에 node_modules가 들어 있지 않도록 앞에서 말한 +ignore를 함께 설정해야 이 구조가 의도대로 동작합니다.
Functions: 재사용 가능한 단위
Dagger의 @func() 어노테이션이 붙은 메소드는 CLI에서 직접 호출 가능한 공개 API가 됩니다.
dagger functions # 사용 가능한 함수 목록
dagger call test --help
dagger call build --source=. export --path=./dist
함수는 다른 함수의 결과를 입력으로 받을 수 있으며, Directory·File·Container 타입은 그래프 연산자처럼 연결됩니다.
# build 결과(Directory)를 publish 함수에 전달
dagger call publish \
--source=. \
--registry=ghcr.io \
--username=myteam \
--token=env:GHCR_TOKEN
모듈 의존성: npm처럼 재사용
다른 팀이 만든 Dagger 모듈을 내 파이프라인에서 함수 호출처럼 사용할 수 있습니다.
# Hello 모듈 설치
dagger install github.com/shykes/daggerverse/hello@main
# 설치된 모듈 호출
dagger -m github.com/shykes/daggerverse/hello call hello --greeting="Hi"
공식 Daggerverse(https://daggerverse.dev)에 공개된 모듈 수백 개가 있으며, 내부 GitHub repo의 private 모듈도 동일하게 import 가능합니다.
GitHub Actions 통합
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dagger/dagger-for-github@v6
with:
version: "0.13.7"
verb: call
args: test --source=.
build-and-publish:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dagger/dagger-for-github@v6
with:
verb: call
args: |
publish
--source=.
--registry=ghcr.io
--username=${{ github.actor }}
--token=env:GITHUB_TOKEN
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GitHub Actions 안에서도 CLI 한 줄로 끝납니다. 로컬에서 돌린 것과 완전히 동일한 파이프라인이 실행됩니다.
GitLab CI 통합
.dagger:
image: docker:24
services:
- docker:24-dind
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
before_script:
- apk add curl
- curl -L https://dl.dagger.io/dagger/install.sh | BIN_DIR=$HOME/.local/bin sh
- export PATH=$HOME/.local/bin:$PATH
test:
extends: .dagger
script:
- dagger call test --source=.
실전 파이프라인: 빌드 → 테스트 → Docker 이미지 → Push
func (m *Pkglog) Publish(
ctx context.Context,
source *dagger.Directory,
registry string,
username string,
token *dagger.Secret,
tag string,
) (string, error) {
// 1) 빌드 결과물 디렉터리
dist := m.Build(ctx, source)
// 2) 프로덕션 이미지 조립
prod := dag.Container().
From("nginx:alpine").
WithDirectory("/usr/share/nginx/html", dist)
// 3) registry 로그인 + 이미지 push
return prod.
WithRegistryAuth(registry, username, token).
Publish(ctx, fmt.Sprintf("%s/pkglog-web:%s", registry, tag))
}
# 로컬 테스트
dagger call publish \
--source=. \
--registry=ghcr.io \
--username=myteam \
--token=env:GHCR_TOKEN \
--tag=$(git rev-parse --short HEAD)
빌드→조립→push 가 하나의 함수로 표현되고, 어디서든 동일하게 실행됩니다. (코드에는 fmt import가 필요합니다.)
Publish는 레지스트리에 푸시한 이미지의 다이제스트가 포함된 참조(ghcr.io/...:tag@sha256:...)를 문자열로 돌려줍니다. 배포 단계에서 태그 대신 이 다이제스트를 쓰면, 누군가 같은 태그로 이미지를 다시 푸시해도 배포되는 이미지가 바뀌지 않습니다. 태그로 latest를 쓰지 않고 커밋 SHA를 넘기는 위 예제의 방식도 같은 이유입니다.
비밀(Secret) 관리
func (m *Pkglog) Deploy(
ctx context.Context,
deployKey *dagger.Secret,
) (string, error) {
return dag.Container().
From("alpine:3").
WithSecretVariable("DEPLOY_KEY", deployKey).
WithExec([]string{"sh", "-c", "curl -X POST -H \"Auth: $DEPLOY_KEY\" https://deploy.internal"}).
Stdout(ctx)
}
dagger call deploy --deploy-key=env:DEPLOY_KEY
dagger call deploy --deploy-key=file:/tmp/key
dagger call deploy --deploy-key=cmd:"op read op://vault/key/value"
env:VAR— 환경변수에서file:/path— 파일에서cmd:"..."— 임의 명령 실행 결과
1Password CLI(op), AWS Secrets Manager CLI, Vault 등과 자연스럽게 연동됩니다.
*dagger.Secret 타입으로 받은 값은 엔진 안에서 따로 관리되어, 로그 출력에서 가려지고 캐시 키와 레이어에도 평문으로 남지 않습니다. 일반 string 인자로 토큰을 받으면 이런 보호가 전혀 없고 --progress=plain 로그와 Trace에 그대로 찍히므로, 비밀 값은 반드시 Secret 타입으로 받아야 합니다. WithEnvVariable로 넣은 값도 마찬가지로 보호되지 않습니다.
이 Deploy 예제에는 Dagger에서 가장 흔히 겪는 함정이 숨어 있습니다. WithExec는 입력이 같으면 캐시에서 결과를 꺼내므로, 같은 인자로 두 번째 실행하면 curl이 다시 실행되지 않고 이전 출력만 돌아옵니다. 빌드·테스트에는 바람직한 동작이지만 배포나 알림처럼 “매번 실행되어야 하는” 부수 효과에는 치명적입니다. 에러도 나지 않고 성공한 것처럼 보이기 때문에, 저도 처음에는 “배포 로그는 정상인데 서버가 안 바뀐다”는 증상을 한참 들여다보게 되었습니다. WithEnvVariable("DEPLOY_REV", commitSHA)나 실행 시각처럼 매번 달라지는 값을 명령 앞에 넣어 캐시 키를 바꾸는 것이 일반적인 해결책입니다. 같은 원리로 apt-get update나 curl로 최신 파일을 내려받는 단계도 한 번 캐시되면 계속 옛 결과를 쓰므로, 버전을 명시하거나 주기적으로 캐시 키를 바꿔 줘야 합니다.
다중 서비스 통합 테스트
백엔드 + DB + Redis를 띄우고 E2E 테스트를 돌려야 할 때:
func (m *Pkglog) E2ETest(ctx context.Context, source *dagger.Directory) (string, error) {
postgres := dag.Container().
From("postgres:16").
WithEnvVariable("POSTGRES_PASSWORD", "test").
WithExposedPort(5432).
AsService()
redis := dag.Container().
From("redis:7").
WithExposedPort(6379).
AsService()
return dag.Container().
From("node:20-alpine").
WithServiceBinding("db", postgres).
WithServiceBinding("cache", redis).
WithEnvVariable("DATABASE_URL", "postgres://postgres:test@db:5432/postgres").
WithEnvVariable("REDIS_URL", "redis://cache:6379").
WithDirectory("/app", source).
WithWorkdir("/app").
WithExec([]string{"npm", "ci"}).
WithExec([]string{"npm", "run", "test:e2e"}).
Stdout(ctx)
}
AsService() + WithServiceBinding()이 docker-compose의 서비스 링크와 같은 역할을 해주며 테스트 완료 후 자동 정리됩니다.
서비스 바인딩은 포트가 열리는 것까지는 기다려 주지만, 데이터베이스가 연결을 받을 준비가 됐는지까지 보장하지는 않습니다. PostgreSQL은 포트를 연 뒤에도 초기화 스크립트를 실행하는 동안 잠깐 재시작하므로, 테스트 첫 쿼리에서 ECONNREFUSED나 the database system is starting up 오류가 간헐적으로 날 수 있습니다. 테스트 시작 전에 연결을 재시도하는 대기 로직(pg_isready 반복 등)을 넣어 두면 이런 불안정이 사라집니다. 또 이 함수는 앞 절의 의존성 분리 없이 npm ci를 실행하므로, 실제로는 nodeDeps 헬퍼를 재사용해 설치 단계 캐시를 공유하는 편이 좋습니다.
로컬 DX
# 모든 함수 목록
dagger functions
# 함수 파라미터 설명
dagger call test --help
# 대화형 셸로 그래프 탐색
dagger shell
# 추적(Trace) 확인 (Dagger Cloud 연결 시 UI URL 출력)
dagger call publish ... --progress=plain
dagger shell은 파이프라인을 단계별로 실행·검사할 수 있는 REPL로, 디버깅의 생산성을 크게 높입니다.
Dagger Cloud: 팀 협업
dagger login
브라우저로 계정 인증 후 로컬과 CI의 모든 실행이 Dagger Cloud Trace UI에서 시각화됩니다.
- 각 단계의 실행 시간·상태·로그
- 캐시 히트/미스 분석
- 팀원의 실행까지 공유
- 원격 캐시로 CI 간·팀원 간 캐시 공유
무료 tier로 소규모 팀도 즉시 사용 가능합니다.
문제 해결
dagger init이 느림
로컬 Docker Engine이 이미지 pull 중일 수 있음. docker ps로 확인하고 초기화 완료까지 대기.
캐시가 동작하지 않음
- 컨테이너 입력이 실제로 동일한지 확인(
dagger call ... --progress=plain로그에서 CACHED 표시 확인) .git/처럼 매번 변하는 디렉터리를WithDirectory에 통째로 넣으면 캐시 무효화됨 →.dockerignore활용
CI에서는 느리고 로컬에선 빠름
로컬 Docker의 공유 캐시를 CI 러너가 갖지 못해서 발생. Dagger Cloud 원격 캐시 또는 GitHub Actions 캐시 액션 연동으로 해결.
엔진 연결이 시간 초과로 실패함
처음 실행할 때는 엔진 컨테이너 이미지를 내려받아야 하므로 시간이 오래 걸리고, 네트워크가 느린 CI에서는 연결 단계에서 시간 초과가 날 수 있습니다. docker ps로 dagger-engine 컨테이너가 떠 있는지, docker logs로 엔진이 오류 없이 시작됐는지 먼저 확인합니다. CLI와 이미 실행 중인 엔진의 버전이 다르면 CLI가 새 엔진을 띄우므로, 여러 버전을 오가는 환경에서는 오래된 엔진 컨테이너를 정리하는 것도 도움이 됩니다.
SDK 간 interop
Go·Python·TypeScript 모듈을 같은 프로젝트에서 혼합 사용 가능하지만 단일 SDK 유지가 유지보수에 유리.
전환 가이드: GitHub Actions에서 Dagger로
- 한 잡부터 Dagger로: 가장 오래 걸리고 자주 실행되는 테스트 잡을 먼저 이관
- 로컬 검증:
dagger call test로컬에서 통과 확인 - Actions에 삽입:
dagger/dagger-for-github@v6액션으로 치환 - 캐시 최적화:
CacheVolume·단계 분리로 CI 시간 측정 후 개선 - 점진적 확장: 빌드·배포·품질 체크 순으로 모듈화
언제 쓰지 말아야 하나
- 파이프라인이 단순:
npm test2줄짜리라면 Actions YAML이 충분 - 팀이 Docker 미숙: 컨테이너 디버깅 경험이 없다면 학습 곡선 있음
- 컨테이너로 돌릴 수 없는 작업: macOS 코드 서명, iOS 시뮬레이터, 특정 하드웨어가 필요한 테스트처럼 러너 호스트에서 직접 실행해야 하는 단계
- Docker 데몬을 쓸 수 없는 CI: 권한 있는 컨테이너(privileged)를 허용하지 않는 공유 러너에서는 엔진을 띄우기 어려움
파이프라인이 복잡하고 “CI에서만 실패하는” 문제를 로컬에서 재현하느라 시간을 많이 쓰는 팀일수록 도입 효과가 큽니다. 반대로 YAML 몇십 줄로 충분히 관리되는 파이프라인이라면, 팀이 새 도구를 배우는 비용이 얻는 이점보다 클 수 있습니다.
체크리스트
- 로컬 Docker/Podman +
dagger설치 - 핵심 잡(test/build/publish) 함수로 구현
-
CacheVolume으로 의존성·빌드 캐시 최적화 - 비밀 값은
dagger.Secret타입 + 외부 vault 연동 - GitHub Actions/GitLab CI에
dagger call한 줄로 연결 - Dagger Cloud 로그인으로 Trace·원격 캐시 활성화
- 모듈 공유: 사내
daggerverse/private repo에 공통 함수 퍼블리시
마무리
Dagger는 CI/CD의 작업 내용을 YAML이 아닌 코드로 옮겨, 로컬과 CI에서 같은 단계를 실행하고 단계별 캐시로 바뀌지 않은 작업을 건너뛰게 해 줍니다. 대신 지연 실행되는 그래프 모델, 부수 효과가 있는 단계의 캐시, 소스 디렉터리 업로드 범위처럼 새로 익혀야 할 개념이 있습니다. 가장 오래 걸리는 테스트 잡 하나를 옮겨 보고, 로컬 재현성과 CI 시간이 실제로 나아지는지 확인한 뒤 범위를 넓히는 것을 권합니다.
자주 묻는 질문 (FAQ)
Q. Dagger 파이프라인에서 캐시가 매번 무효화되는 이유는 무엇인가요?
A. Dagger는 각 단계의 입력이 이전 실행과 완전히 같을 때만 캐시를 재사용합니다. 소스 디렉터리를 통째로 WithDirectory에 넣으면 .git/처럼 실행할 때마다 바뀌는 파일 때문에 입력이 달라져 이후 단계가 모두 다시 실행됩니다. 불필요한 디렉터리는 제외해 입력을 줄이고, 의존성 설치 결과는 CacheVolume으로 마운트하며, —progress=plain 로그에서 CACHED 표시를 보고 어느 단계부터 캐시가 깨지는지 확인합니다.