dbt CI/CD 심화: Slim CI, state, 환경 분리, 아티팩트 관리
Slim CI가 해결하는 문제
dbt 프로젝트는 성장한다. 처음에는 모델 50개지만 6개월 후에는 300개, 1년 후에는 500개가 된다. 이 시점에서 "PR마다 전체 dbt 빌드를 돌린다"는 접근은 두 가지 문제를 만든다.
시간: BigQuery나 Snowflake에서 500개 모델을 실행하면 30~60분이 걸릴 수 있다. 리뷰 사이클이 느려지고 개발자 경험이 나빠진다.
비용: 클라우드 DWH는 쿼리 실행량에 따라 비용이 부과된다. PR 10개가 하루에 올라오면 전체 빌드를 10번 돌리는 셈이다. 월 수백 달러의 낭비가 발생한다.
Slim CI는 이 문제의 해결책이다. 변경된 모델과 그 영향을 받는 모델만 선택적으로 빌드하고 테스트한다. 나머지는 이미 프로덕션에서 검증된 결과를 그대로 참조한다.
manifest.json과 state 비교의 원리
Slim CI의 핵심은 두 manifest.json을 비교하는 것이다.
manifest.json이란
dbt 컴파일(또는 빌드) 시 target/manifest.json이 생성된다. 이 파일은 프로젝트의 모든 노드(모델, 테스트, 소스, 스냅샷, 매크로)에 대해 다음을 기록한다.
- 컴파일된 SQL과 그 체크섬(hash)
- 의존성 그래프 (upstream/downstream refs)
- materialization, partition, cluster 등 설정
- 테스트 정의와 소스 메타데이터
두 manifest.json의 해시 값을 비교하면 무엇이 바뀌었는지 정확히 알 수 있다.
state: 선택자 유형
state:modified - SQL, 설정, 의존성 중 무언가 바뀐 모든 노드
state:modified.body - SQL 본문(body)만 바뀐 노드
state:modified.configs - materialization 등 설정만 바뀐 노드
state:modified.macros - 참조하는 매크로가 바뀐 노드
state:new - 프로덕션 manifest에 없는 새 모델
state:modified+ - 변경·신규 모델 + 모든 다운스트림 의존 모델실무에서 state:modified+를 가장 많이 쓰는 이유: 업스트림 모델이 변경되면 다운스트림 모델도 함께 테스트해야 예상치 못한 영향을 잡을 수 있기 때문이다.
--defer: 변경 안 된 모델을 다시 빌드하지 않는 방법
state:modified+로 빌드 범위를 줄였지만, 한 가지 문제가 남는다. 변경된 다운스트림 모델이 변경 안 된 업스트림 모델을 참조(ref())할 때, 그 업스트림 모델의 결과가 CI 환경에는 없다. 다시 빌드해야 할까?
--defer가 이 문제를 해결한다. 빌드 대상이 아닌 업스트림 모델을 다시 빌드하지 않고, 지정된 환경(보통 프로덕션)의 실제 테이블을 직접 참조한다.
--state와 --defer-state 분리 (dbt 1.6+)
# 기본: 하나의 manifest로 비교 + 참조 모두 처리
dbt build \
--select state:modified+ \
--defer \
--state ./prod-manifest
# dbt 1.6+: 비교 기준과 deferral 참조를 분리
dbt build \
--select state:modified+ \
--state ./prod-manifest \ # 무엇이 변경됐는지 비교할 기준
--defer \
--defer-state ./staging-manifest # 변경 안 된 모델은 여기서 참조언제 분리하는가: 스테이징 환경이 잘 유지되고 있어 실제 데이터로 CI 테스트를 하고 싶을 때. 변경 감지는 프로덕션 기준으로, 실제 참조는 스테이징 데이터로 분리할 수 있다.
manifest.json 아티팩트 관리 전략
Slim CI가 제대로 작동하려면 최신 프로덕션 manifest.json이 항상 CI에서 접근 가능해야 한다.
dbt build --target prod
성공 시에만 manifest 저장
또는 GitHub Artifact
target/manifest.json
./prod-manifest/ 에 저장
Slim CI 실행
전략 1: S3/GCS 클라우드 스토리지 (OSS 권장)
# .github/workflows/dbt-prod.yml — 프로덕션 배포 후 manifest 저장
jobs:
deploy:
steps:
- name: Run production dbt
run: dbt build --target prod --profiles-dir .
- name: Upload manifest to S3
if: success() # 성공 시에만 업로드
run: |
aws s3 cp target/manifest.json \
s3://my-dbt-artifacts/prod/manifest.json \
--metadata "git-sha=${{ github.sha }}"
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}# .github/workflows/dbt-ci.yml — PR 시 manifest 다운로드
jobs:
slim-ci:
steps:
- name: Download production manifest
run: |
mkdir -p ./prod-manifest
aws s3 cp s3://my-dbt-artifacts/prod/manifest.json \
./prod-manifest/manifest.json \
|| echo "No prod manifest found, running full build"전략 2: GitHub Actions Artifact (단순한 경우)
# 프로덕션 워크플로우
- name: Save manifest artifact
if: success()
uses: actions/upload-artifact@v4
with:
name: dbt-prod-manifest
path: target/manifest.json
retention-days: 30
# CI 워크플로우
- name: Download manifest artifact
uses: actions/download-artifact@v4
with:
name: dbt-prod-manifest
path: ./prod-manifest주의: GitHub Actions Artifact는 같은 레포지토리 내에서만 공유된다. 여러 레포지토리에서 manifest를 공유해야 한다면 S3/GCS를 사용한다.
manifest 없을 때 폴백 처리
최초 배포나 아티팩트가 없을 때를 대비한 로직이 필요하다.
if [ -f ./prod-manifest/manifest.json ]; then
echo "Slim CI 실행"
dbt build \
--select state:modified+ \
--defer \
--state ./prod-manifest \
--target ci
else
echo "manifest 없음 — 전체 빌드 실행"
dbt build --target ci
fi환경 분리 심화
로컬 빌드, 빠른 반복
소규모 샘플 데이터
dbt run --target dev
Slim CI — 변경분만 빌드
--defer로 prod 참조
PR 닫힐 때 스키마 삭제
프로덕션 규모 데이터
통합 테스트 · QA 확인
--defer-state 활용 가능
성공 후 manifest 저장
배포 후 데이터 검증
롤백 플랜 준비
CI 환경의 격리 설정
PR별로 별도 스키마를 생성하면 여러 PR이 동시에 실행될 때 서로 간섭하지 않는다.
# profiles.yml
my_project:
target: dev
outputs:
dev:
type: bigquery
project: mycompany-dev
dataset: "dbt_{{ env_var('DBT_USER', 'default') }}"
ci:
type: bigquery
project: mycompany-ci
dataset: "ci_pr_{{ env_var('PR_NUMBER', 'local') }}"
staging:
type: bigquery
project: mycompany-staging
dataset: dbt_staging
prod:
type: bigquery
project: mycompany-prod
dataset: dbt_prodCI 스키마 자동 정리: PR이 닫힐 때 사용한 스키마를 삭제하지 않으면 스토리지 비용이 쌓인다.
# .github/workflows/cleanup-ci-schema.yml
on:
pull_request:
types: [closed]
jobs:
cleanup:
runs-on: ubuntu-latest
steps:
- name: Drop CI dataset
run: |
bq rm -r -f mycompany-ci:ci_pr_${{ github.event.pull_request.number }}증분 모델의 CI/CD 처리
Slim CI에서 가장 까다로운 케이스는 Incremental 모델이다. CI 환경(ci_pr_42)에는 증분 모델의 이전 빌드 결과가 없다. 그러므로 is_incremental()이 false를 반환하고 전체 재빌드(full-refresh)가 일어난다. 대용량 테이블이라면 CI가 오래 걸리고 비용이 급증한다.
dbt clone으로 해결 (dbt 1.6+)
# 1. 먼저 변경된 증분 모델을 prod에서 CI 스키마로 복제
dbt clone \
--select state:modified,resource_type:model \
--target ci \
--state ./prod-manifest \
--profiles-dir .
# 2. 그 다음 Slim CI 실행 — 복제된 모델은 is_incremental=true로 동작
dbt build \
--select state:modified+ \
--defer \
--state ./prod-manifest \
--target ci \
--profiles-dir .zero-copy clone: BigQuery와 Snowflake에서 CLONE 문은 실제 데이터를 복사하지 않고 메타데이터만 복사한다. dbt clone은 지원 플랫폼에서 이 기능을 자동으로 활용하므로 비용과 시간이 거의 들지 않는다.
주의: Redshift나 Databricks에서는 실제 데이터 복사가 발생할 수 있다. 플랫폼별 동작을 확인해야 한다.
완성된 GitHub Actions 워크플로우
다음은 프로덕션 배포와 Slim CI를 통합한 실전 워크플로우다.
프로덕션 배포 워크플로우 (main 머지 시):
# .github/workflows/dbt-prod.yml
name: dbt Production Deploy
on:
push:
branches: [main]
paths: ['dbt/**']
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dbt
run: pip install dbt-bigquery==1.8.*
- name: Run production build
run: dbt build --target prod --profiles-dir .
env:
DBT_GOOGLE_BIGQUERY_KEYFILE: ${{ secrets.GCP_SA_KEY }}
- name: Upload manifest to S3
if: success()
run: |
aws s3 cp target/manifest.json \
s3://my-dbt-artifacts/prod/manifest.json \
--metadata "sha=${{ github.sha }},ts=$(date -u +%Y%m%dT%H%M%S)"
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}PR Slim CI 워크플로우:
# .github/workflows/dbt-ci.yml
name: dbt Slim CI
on:
pull_request:
paths: ['dbt/**']
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
jobs:
slim-ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dbt
run: pip install dbt-bigquery==1.8.*
- name: Download production manifest
run: |
mkdir -p ./prod-manifest
aws s3 cp s3://my-dbt-artifacts/prod/manifest.json \
./prod-manifest/manifest.json \
|| echo "No manifest found"
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: dbt compile (문법 검사)
run: dbt compile --target ci --profiles-dir .
env:
DBT_GOOGLE_BIGQUERY_KEYFILE: ${{ secrets.GCP_SA_KEY }}
- name: Clone incremental models from prod
if: hashFiles('prod-manifest/manifest.json') != ''
run: |
dbt clone \
--select state:modified,resource_type:model \
--target ci \
--state ./prod-manifest \
--profiles-dir .
env:
DBT_GOOGLE_BIGQUERY_KEYFILE: ${{ secrets.GCP_SA_KEY }}
- name: Slim CI build and test
run: |
if [ -f ./prod-manifest/manifest.json ]; then
dbt build \
--select state:modified+ \
--defer \
--state ./prod-manifest \
--target ci \
--profiles-dir .
else
dbt build --target ci --profiles-dir .
fi
env:
DBT_GOOGLE_BIGQUERY_KEYFILE: ${{ secrets.GCP_SA_KEY }}
cleanup:
runs-on: ubuntu-latest
if: github.event.action == 'closed'
steps:
- name: Drop CI dataset
run: |
bq rm -r -f mycompany-ci:ci_pr_${{ github.event.pull_request.number }}
env:
GOOGLE_APPLICATION_CREDENTIALS: ${{ secrets.GCP_SA_KEY }}흔한 실수와 해결책
실수 1: 실패한 빌드의 manifest 업로드
if: success() 없이 무조건 manifest를 업로드하면, 실패한 빌드의 상태가 prod manifest로 기록된다. 다음 PR의 state 비교가 틀린 baseline을 사용하게 된다.
해결: 업로드 스텝에 항상 if: success() 추가.
실수 2: state:modified (+ 없이) 사용
state:modified는 변경된 모델만 빌드한다. 그 모델의 다운스트림이 영향을 받아도 테스트하지 않는다. state:modified+가 대부분의 경우 올바른 선택이다.
단, 프로젝트가 매우 크면 +가 선택하는 모델이 너무 많아질 수 있다. 이 경우 state:modified,1+state:modified (1단계 다운스트림만)으로 범위를 좁힐 수 있다.
실수 3: 소스 변경 미감지
state:modified는 모델과 매크로 변경만 감지한다. 소스 데이터베이스의 스키마가 바뀌어도 CI에서 자동으로 잡히지 않는다. 소스 freshness 테스트를 별도로 추가해야 한다.
# schema.yml
sources:
- name: raw_orders
freshness:
warn_after: {count: 6, period: hour}
error_after: {count: 24, period: hour}
loaded_at_field: _loaded_at실수 4: PR 스키마 미정리
PR이 닫혀도 CI 스키마가 남아 비용이 쌓인다. pull_request 이벤트의 types: [closed]를 활용해 정리 워크플로우를 추가한다.
Open Questions
- 대규모 프로젝트(1,000+ 모델)에서
state:modified+의 선택 범위가 과도하게 넓어지는 문제를 다루는 표준 전략이 아직 완전히 정립되지 않았다. dbt clone은 BigQuery, Snowflake에서 zero-copy로 동작하지만, 다른 플랫폼에서의 동작 보장이 일관적이지 않다. 플랫폼별 동작 확인이 필요하다.
References
- Continuous integration jobs in dbt — dbt Developer Hub
- Defer — dbt Developer Hub
- Manifest JSON file — dbt Developer Hub
- CI/CD With dbt Slim CI: Optimize Using dbt 1.8 --empty Flag — Datacoves
- Two Approaches to dbt Slim CI — Snowpack Data
- Best Practices for dbt Workflows, Part 2: Slim CI/CD Builds — select.dev
- dbt Defer: Speed Up CI/CD Pipelines and Slash Compute Costs — pmunhoz.com
- How to Use dbt Defer to Optimize Your Data Workflow — Airbyte
- Get started with Continuous Integration tests — dbt Developer Hub
- dbt CI/CD Best Practices for Data Teams — Paradime