배포와 CI/CD
dbt에서 배포가 어려운 이유
일반 애플리케이션 배포는 새 바이너리나 컨테이너를 올리고, 문제가 생기면 이전 버전으로 되돌리는 그림으로 설명할 수 있다. dbt 배포는 조금 다르다. 배포 결과가 코드만이 아니라 웨어하우스 안의 테이블, 뷰, 스냅샷, 테스트 결과, 문서, 메타데이터 아티팩트로 남는다. 잘못된 모델 하나가 배포되면 대시보드가 깨지는 데서 끝나지 않고, 그 모델을 참조하는 하위 마트와 리포트까지 연쇄적으로 영향을 받는다.
그래서 dbt의 CI/CD는 "PR이 열리면 SQL 문법을 검사한다" 수준으로는 부족하다. 좋은 파이프라인은 세 가지 질문에 답해야 한다.
- 이 변경이 컴파일되는가?
- 이 변경이 영향을 주는 모델과 테스트가 실제로 통과하는가?
- 통과한 코드가 운영 환경에 반영될 때 어떤 순서와 격리 수준으로 실행되는가?
핵심은 PR 검증과 운영 배포를 분리하는 것이다. PR에서는 변경 범위를 작게 잡아 빠르게 피드백하고, 운영에서는 합쳐진 main 브랜치를 기준으로 재현 가능한 명령을 실행한다.
환경 설계: dev, CI, staging, prod
dbt 환경은 보통 네 층으로 나눈다.
| 환경 | 목적 | 주의점 |
|---|---|---|
| dev | 개인 개발과 로컬 검증 | 사용자별 schema를 분리한다. |
| CI | PR 검증 | PR별 임시 schema를 만들고, 운영과 충돌하지 않게 한다. |
| staging | 릴리스 후보 검증 | 팀 규모가 크거나 배포 묶음이 필요할 때 둔다. |
| prod | 운영 데이터 생성 | 최종 source of truth 환경으로 관리한다. |
작은 팀이라면 dev → CI → prod만으로도 충분하다. 그러나 변경이 많고 데이터 소비자가 많다면 staging을 추가하는 편이 안전하다. 예를 들어 여러 PR을 하루에 한 번 묶어 배포하는 팀은 staging 브랜치에 먼저 머지한 뒤, 일정 시간 동안 대시보드와 주요 테스트를 확인하고 main으로 승격할 수 있다.
중요한 것은 이름보다 격리다. CI가 운영 schema에 직접 쓰면 안 된다. dbt Cloud의 CI 환경은 PR별 임시 schema를 자동으로 만들 수 있고, dbt Core를 GitHub Actions나 GitLab CI에서 돌리는 경우에도 schema: ci_pr_{{ env_var('PR_NUMBER') }} 같은 방식으로 PR별 schema를 분리해야 한다.
PR 검증: Slim CI
대형 dbt 프로젝트에서 모든 PR마다 dbt build 전체를 실행하면 피드백이 느리고 비용도 커진다. 이때 쓰는 패턴이 Slim CI다.
dbt build --select state:modified+ --defer --state path/to/prod-artifacts각 옵션의 의미는 다음과 같다.
| 옵션 | 의미 |
|---|---|
state:modified+ | 이전 manifest와 비교해 변경된 노드와 그 하위 의존 노드를 선택한다. |
--state | 비교 기준이 되는 과거 dbt 아티팩트 디렉터리를 지정한다. 보통 운영 manifest.json을 둔다. |
--defer | 선택되지 않은 상위 모델은 CI schema에 새로 만들지 않고, 운영 manifest가 가리키는 relation을 참조할 수 있게 한다. |
이 조합 덕분에 PR에서 바꾼 모델과 그 영향 범위만 빌드한다. 예를 들어 stg_orders를 고치면 stg_orders와 그 하위 int_orders, fct_orders 테스트는 돌지만, 전혀 관련 없는 마케팅 마트는 건드리지 않는다.
manifest.json은 dbt 프로젝트의 리소스, 설정, 의존 관계를 담은 상태 파일이다. dbt 명령 대부분은 실행 후 target/manifest.json을 만든다. CI에서 Slim CI를 쓰려면 운영 배포가 성공할 때마다 이 파일을 저장해 두어야 한다. dbt Cloud는 환경과 job의 아티팩트를 관리해 주고, dbt Core 기반 CI에서는 S3, GCS, Azure Blob, artifact branch 같은 외부 저장소에 업로드하는 방식이 흔하다.
GitHub Actions로 dbt Core 배포하기
dbt Cloud를 쓰면 UI에서 CI job과 deploy job을 만들 수 있지만, dbt Core만으로도 GitHub Actions 같은 CI 도구에서 충분히 운영할 수 있다. 기본 구조는 다음과 같다.
name: dbt CI
on:
pull_request:
branches: [ main ]
jobs:
dbt-ci:
runs-on: ubuntu-latest
env:
DBT_PROFILES_DIR: .github/dbt
DBT_TARGET: ci
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dbt
run: |
python -m pip install --upgrade pip
pip install dbt-core dbt-postgres
- name: Write profiles.yml
run: |
mkdir -p .github/dbt
printf '%s' "${{ secrets.DBT_PROFILES_YML }}" > .github/dbt/profiles.yml
- name: Download production artifacts
run: |
mkdir -p target-prod
aws s3 cp s3://my-dbt-artifacts/prod/manifest.json target-prod/manifest.json
- name: Validate changed dbt graph
run: |
dbt deps
dbt build --target ci --select state:modified+ --defer --state target-prod이 예시는 개념을 보여주기 위한 것이다. 실제 운영에서는 어댑터(dbt-snowflake, dbt-bigquery, dbt-databricks 등), 클라우드 인증 방식, 네트워크 접근 제어를 팀 환경에 맞게 바꿔야 한다.
운영 배포 workflow는 PR이 아니라 main push나 스케줄을 기준으로 실행한다.
name: dbt Deploy
on:
push:
branches: [ main ]
schedule:
- cron: '0 18 * * *' # UTC 기준. KST 03:00
jobs:
deploy:
runs-on: ubuntu-latest
env:
DBT_PROFILES_DIR: .github/dbt
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dbt
run: pip install dbt-core dbt-postgres
- name: Write profiles.yml
run: |
mkdir -p .github/dbt
printf '%s' "${{ secrets.DBT_PROFILES_YML }}" > .github/dbt/profiles.yml
- name: Build production assets
run: |
dbt deps
dbt source freshness --target prod
dbt build --target prod
dbt docs generate --target prod
- name: Upload artifacts
run: |
aws s3 cp target/manifest.json s3://my-dbt-artifacts/prod/manifest.json
aws s3 cp target/run_results.json s3://my-dbt-artifacts/prod/run_results.json주의할 점은 GitHub Actions의 cron이 UTC 기준이라는 것이다. KST 새벽 3시에 돌리고 싶다면 전날 18:00 UTC로 설정해야 한다.
배포 명령은 dbt run보다 dbt build가 기본값
운영 배포에서 dbt run 뒤에 dbt test를 따로 실행하는 방식도 가능하지만, 요즘 기본값은 dbt build다. dbt build는 모델, 테스트, 스냅샷, seed를 DAG 순서대로 처리하고, 선행 노드가 실패하면 하위 노드를 스킵한다. 운영 파이프라인에서는 이 동작이 중요하다. 잘못된 upstream 모델을 만든 뒤 downstream 모델까지 계속 실행하는 일을 줄여 주기 때문이다.
운영 job의 흔한 명령 순서는 다음과 같다.
dbt deps
dbt source freshness
dbt build
dbt docs generatesource freshness는 원천 데이터가 기대한 시간 안에 들어왔는지 확인한다. 예를 들어 주문 원천이 2시간 넘게 갱신되지 않았는데 매출 마트를 정상으로 빌드하면, 사용자는 "데이터가 정상적으로 최신"이라고 오해할 수 있다. freshness 실패를 배포 실패로 볼지, 경고로만 볼지는 데이터 제품의 SLA에 맞춰 정해야 한다.
secrets와 profiles.yml 관리
CI/CD에서 가장 자주 나는 사고는 profiles.yml이나 키 파일이 저장소에 커밋되는 것이다. dbt 프로젝트 저장소에는 연결 정보의 구조만 두고, 실제 비밀번호와 키는 CI secret store에 둔다.
좋은 패턴은 다음과 같다.
profiles.yml자체를 secret으로 저장하거나, template 파일에서env_var()로 값을 읽게 한다.- 운영 credential과 CI credential을 분리한다.
- CI credential은 임시 schema 생성과 테스트에 필요한 최소 권한만 준다.
- production credential은 승인된 deploy job에서만 쓰게 한다.
target/,logs/, 로컬 key 파일은.gitignore에 넣는다.
예를 들어 profiles.yml은 다음처럼 환경 변수를 읽게 만들 수 있다.
jaffle_shop:
target: "{{ env_var('DBT_TARGET', 'dev') }}"
outputs:
ci:
type: postgres
host: "{{ env_var('DBT_HOST') }}"
user: "{{ env_var('DBT_CI_USER') }}"
password: "{{ env_var('DBT_CI_PASSWORD') }}"
dbname: analytics
schema: "ci_pr_{{ env_var('PR_NUMBER') }}"
threads: 4
prod:
type: postgres
host: "{{ env_var('DBT_HOST') }}"
user: "{{ env_var('DBT_PROD_USER') }}"
password: "{{ env_var('DBT_PROD_PASSWORD') }}"
dbname: analytics
schema: marts
threads: 8이렇게 하면 같은 dbt 프로젝트라도 CI와 prod가 서로 다른 권한과 schema를 사용한다.
문서와 아티팩트도 배포 산출물이다
dbt 배포의 산출물은 테이블만이 아니다. manifest.json, run_results.json, catalog.json, dbt docs HTML도 운영에 필요한 정보다.
| 산출물 | 용도 |
|---|---|
manifest.json | DAG, 모델 설정, 테스트, 매크로 등 프로젝트 상태. Slim CI와 문서 생성의 핵심 입력. |
run_results.json | 각 노드의 실행 결과와 시간. 실패 분석과 운영 리포팅에 유용. |
catalog.json | 웨어하우스 컬럼, 타입, 통계 등 catalog 정보. docs site에 사용. |
| docs site | 데이터 소비자와 분석가가 모델 설명, lineage, 테스트 정보를 확인하는 UI. |
운영 job이 성공할 때마다 아티팩트를 저장하면 다음 PR의 Slim CI 기준점이 안정된다. 반대로 운영 아티팩트가 오래되면 CI가 필요 이상으로 많은 모델을 선택하거나, 실제 운영과 다른 기준으로 검증할 수 있다.
배포 전략: 즉시 배포와 릴리스 트레인
dbt 배포 전략은 팀의 변경량과 데이터 소비자 수에 따라 달라진다.
즉시 배포
PR이 main에 merge되면 바로 production job이 실행된다. 작은 팀이나 모델 수가 적은 프로젝트에 적합하다.
장점은 빠르다는 것이다. 단점은 PR이 자주 merge될수록 운영 job이 자주 돌고, 실패가 발생했을 때 어떤 변경이 원인인지 빠르게 추적해야 한다는 점이다.
릴리스 트레인
여러 변경을 staging이나 release branch에 모은 뒤 정해진 시간에 production으로 승격한다. 데이터 소비자가 많고, 주요 대시보드 검증 시간이 필요한 조직에 적합하다.
장점은 운영 변경 창을 통제할 수 있다는 것이다. 단점은 릴리스 관리자가 필요하고, staging manifest가 자주 바뀌면 CI의 state comparison 기준이 흔들릴 수 있다는 점이다.
실패 대응과 롤백
dbt에서 롤백은 애플리케이션 롤백보다 까다롭다. 이전 SQL로 되돌려도 이미 만들어진 테이블 데이터가 자동으로 과거 상태가 되지는 않는다. 따라서 롤백 전략은 모델 유형별로 나눠야 한다.
| 모델 유형 | 실패 대응 |
|---|---|
| view | 이전 코드로 되돌려 재배포하면 비교적 쉽게 복구된다. |
| table | 이전 코드로 재생성해야 한다. 큰 테이블은 시간이 오래 걸릴 수 있다. |
| incremental | 잘못 들어간 행을 삭제하거나 --full-refresh가 필요할 수 있다. |
| snapshot | 과거 이력 자체가 오염될 수 있어 수동 보정 절차가 필요하다. |
운영적으로는 다음 세 가지가 중요하다.
- 배포마다 commit SHA와 dbt run ID를 남긴다.
- 실패한 노드와 하위 영향 범위를 즉시 확인할 수 있게
run_results.json을 보관한다. - 대용량 incremental 모델은 무작정
--full-refresh하지 말고, 재처리 파티션과 비용을 먼저 계산한다.
실무 체크리스트
| 영역 | 체크할 것 |
|---|---|
| 브랜치 정책 | main 직접 push 금지, PR 필수, CI 통과 후 merge |
| CI 범위 | dbt build --select state:modified+로 변경 영향 범위 검증 |
| 상태 기준 | 운영 manifest.json을 안정적으로 저장하고 CI에서 사용 |
| 환경 격리 | PR별 schema, 운영 credential 분리 |
| 운영 명령 | dbt deps, source freshness, dbt build, docs generate |
| 아티팩트 | manifest.json, run_results.json, catalog.json 저장 |
| 알림 | source freshness 실패, build 실패, 테스트 실패를 채널에 알림 |
| 비용 관리 | 전체 build와 Slim CI 실행 시간을 따로 관측 |
| 롤백 | view/table/incremental/snapshot별 복구 절차 문서화 |
한 줄 정리
dbt CI/CD의 핵심은 PR에서는 state:modified+와 --defer로 변경 영향 범위만 빠르게 검증하고, 운영에서는 격리된 credential로 dbt build를 재현 가능하게 실행하며, 다음 CI의 기준이 될 아티팩트를 안정적으로 남기는 것이다.
References
- https://docs.getdbt.com/docs/deploy/ci-jobs
- https://docs.getdbt.com/guides/set-up-ci
- https://docs.getdbt.com/docs/deploy/deploy-jobs
- https://docs.getdbt.com/docs/deploy/deploy-environments
- https://docs.getdbt.com/reference/node-selection/state-selection
- https://docs.getdbt.com/reference/node-selection/defer
- https://docs.getdbt.com/reference/artifacts/manifest-json
- https://docs.getdbt.com/docs/build/sources