왜 CI 파이프라인은 특정 플랫폼에 묶이는가

GitHub Actions, GitLab CI, Jenkins는 각자의 YAML 문법과 실행 모델을 가진다. 파이프라인 로직이 이 YAML 안에 흩어져 있으면 몇 가지 문제가 생긴다. 첫째, 로컬에서 재현이 안 된다. push하고 러너 로그를 기다리는 디버깅 루프는 느리다. 둘째, 벤더 종속이 발생한다. GitLab에서 GitHub로 옮기려면 파이프라인을 통째로 다시 쓴다. 셋째, 캐시·병렬화가 플랫폼 기능에 의존해 재현성이 떨어진다.

dagger는 파이프라인을 컨테이너 기반의 코드(Go/Python/TypeScript SDK 또는 셸)로 정의하고, 이를 BuildKit 엔진 위에서 실행한다. CI 플랫폼은 그저 "dagger call"을 호출하는 얇은 래퍼가 된다. 결과적으로 로컬과 CI가 완전히 동일한 로직을 돌린다.

핵심 개념: 함수와 캐시

dagger의 실행 단위는 Container 상태를 변환하는 함수다. 각 연산(파일 추가, 명령 실행)은 콘텐츠 주소 기반으로 캐시되며, 입력이 바뀌지 않으면 재실행되지 않는다. 이 캐시는 BuildKit 레이어와 동일한 모델이라 로컬·원격에서 일관되게 동작한다.

Python SDK로 빌드/테스트 함수 정의

실제 테스트를 수행하는 모듈을 Python으로 작성한다. 이 코드는 CI가 아니라 개발자 노트북에서도 그대로 실행된다.

import dagger
from dagger import dag, function, object_type

@object_type
class Ci:
    @function
    async def test(self, source: dagger.Directory) -> str:
        return await (
            dag.container()
            .from_("python:3.12-slim")
            .with_directory("/src", source)
            .with_workdir("/src")
            .with_exec(["pip", "install", "-e", ".[dev]"])
            .with_exec(["pytest", "-q"])
            .stdout()
        )

핵심은 with_directory로 소스를 주입하고 with_exec로 명령을 쌓는 것이다. 각 단계는 캐시되므로 소스만 바뀌면 pip install 단계는 재사용된다.

로컬에서 먼저 실행하고 CI에 이식

정의한 함수는 CLI로 바로 호출한다. 로컬에서 통과하면 CI에서도 통과한다는 보장이 생긴다.

# 로컬 개발 머신에서 실행
dagger call test --source=.

# 특정 명령을 즉석에서 조합 (셸 문법)
dagger core container \
  from --address=python:3.12-slim \
  with-exec --args="python","--version" \
  stdout

플랫폼별 래퍼는 최소화한다

각 CI 플랫폼의 설정 파일은 dagger를 설치하고 함수를 호출하는 역할만 한다. 로직은 들어가지 않는다.

name: ci
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dagger/dagger-for-github@v6
        with:
          verb: call
          args: test --source=.

GitLab로 옮길 때는 script:에 동일한 dagger call test --source=. 한 줄만 넣으면 된다. 파이프라인 본체는 재작성이 필요 없다.

플랫폼 종속 vs dagger 비교

항목플랫폼 네이티브 YAMLdagger
로컬 재현어려움(러너 필요)동일 명령으로 가능
이식성재작성 필요래퍼만 교체
캐시 모델플랫폼별 상이BuildKit 콘텐츠 주소
디버깅 언어YAML 표현식Go/Python/TS

주의할 점

장밋빛만 있는 것은 아니다. 첫째, 엔진 실행에 Docker(또는 호환 런타임)가 필요하다. 컨테이너를 못 쓰는 러너에서는 부적합하다. 둘째, 최초 실행은 이미지 pull과 엔진 기동 때문에 오히려 느릴 수 있다. 캐시가 채워진 뒤에야 이점이 나온다. 셋째, 원격 캐시를 공유하려면 DAGGER_CLOUD_TOKEN 같은 별도 설정이나 캐시 백엔드를 구성해야 CI 러너 간 캐시가 유지된다. 로컬 캐시만 쓰면 러너가 매번 새로 시작하는 환경에서는 캐시 이점이 사라진다.

넷째, SDK 버전과 엔진 버전을 고정하라. 버전이 어긋나면 재현성이라는 핵심 가치가 무너진다. dagger.json의 엔진 버전과 CI에서 설치하는 CLI 버전을 함께 핀으로 묶어 관리하는 것이 안전하다.