배포 후 모니터링: SLO 연동, 자동 롤백 트리거, 배포 이력
데이터 플랫폼 배포가 특별히 위험한 이유
일반 소프트웨어 배포가 실패하면 프로세스가 죽거나, HTTP 500이 터지거나, 익셉션 로그가 쌓인다. 신호가 크고 즉각적이다. 데이터 플랫폼 배포 실패는 다르다. Airflow DAG이 성공 상태로 완료되는 동안, 테이블에는 잘못된 수치가 조용히 적재될 수 있다. dbt 모델이 run과 test를 모두 통과했는데 24시간 후 Finance 팀이 매출 대시보드의 숫자가 23% 낮다고 보고하는 경우가 그 예다.
Airflow가 추적하는 것은 "Python 태스크가 예외를 던졌는가" 뿐이다. 빈 파티션, 중복 행, LEFT JOIN을 INNER JOIN으로 바꿔서 생긴 데이터 누락은 태스크 실패를 일으키지 않는다. 그리고 문제는 lineage를 타고 내려간다. staging 모델 하나가 잘못되면 그것을 의존하는 모든 mart와 대시보드가 함께 오염된다.
배포 후 모니터링은 이 조용한 실패를 자동으로 잡는 시스템이다.
데이터 파이프라인 SLO: 세 가지 유형
SLO(Service Level Objective)는 "무엇이 정상인지"를 수치로 정의한다. 데이터 플랫폼에서는 세 가지 유형이 핵심이다.
예: 매시간 파이프라인 → 60분 이내
예: 오늘 6,400행 vs 기준 8,200행 → 이상
도메인 값 범위 내
신선도 SLO: dbt source freshness
# models/sources.yml
sources:
- name: raw_events
database: raw
schema: analytics
tables:
- name: orders
freshness:
warn_after: {count: 6, period: hour}
error_after: {count: 24, period: hour}
loaded_at_field: _etl_loaded_at# CI에서 또는 배포 전 실행
dbt source freshness --output json --output-path target/sources.json출력된 sources.json을 파싱해서 status: error 소스가 있으면 배포를 차단한다.
볼륨 SLO: Elementary anomaly detection
# models/schema.yml
models:
- name: fct_orders
config:
elementary:
timestamp_column: created_at
tests:
- elementary.volume_anomalies:
time_bucket:
period: day
count: 1
training_period:
period: day
count: 14 # 14일 기준선 학습
anomaly_sensitivity: 3 # 3-sigma 이상이면 이상치품질 SLO: Soda Core SodaCL
# checks/post_deploy_orders.yml
checks for fct_orders:
- row_count between 50000 and 5000000
- missing_percent(customer_id) < 1%
- duplicate_count(order_id) = 0
- freshness(created_at) < 4h
- invalid_percent(status) < 0.1%:
valid values: [placed, processing, shipped, delivered, cancelled]soda scan -d my_warehouse -c config/soda_config.yml checks/post_deploy_orders.yml
# 실패 시 non-zero exit → CI 스텝 실패 → 배포 차단배포 헬스 체크: 구체적인 검증 기준
배포 직후 "이 파이프라인이 정상인가"를 확인하는 체크리스트다.
Check 1 — 행 수 조정(Reconciliation):
-- 스테이징 vs 프로덕션 행 수 비교
WITH prod AS (
SELECT COUNT(*) AS cnt FROM analytics_prod.fct_orders
),
staging AS (
SELECT COUNT(*) AS cnt FROM analytics_staging.fct_orders
)
SELECT
ABS(staging.cnt - prod.cnt) * 1.0 / prod.cnt AS pct_diff,
CASE WHEN ABS(staging.cnt - prod.cnt) * 1.0 / prod.cnt > 0.1
THEN 'FAIL' ELSE 'PASS' END AS status
FROM prod, staging;Check 2 — NULL 비율:
SELECT
column_name,
SUM(CASE WHEN column_value IS NULL THEN 1 ELSE 0 END) * 100.0 / COUNT(*) AS null_rate
FROM fct_orders
GROUP BY column_name
HAVING null_rate > 1.0; -- 1% 초과 시 이상Check 3 — dbt 아티팩트 파싱:
import json, sys
with open("target/run_results.json") as f:
results = json.load(f)
failures = [
r for r in results["results"]
if r["status"] in ("error", "fail")
]
if failures:
print(f"품질 게이트 실패: {len(failures)}건")
for f in failures:
print(f" - {f['unique_id']}: {f.get('message', '')}")
sys.exit(1)Blue-Green 배포: 데이터 계층의 카나리 전략
애플리케이션 카나리(트래픽 %를 나눠 보내기)는 데이터 플랫폼에 직접 적용하기 어렵다. 대신 Blue-Green 스키마 배포를 쓴다.
fct_orders
8,200행 ✓
dbt run 실행
dbt test 실행
↓
SWAP WITH
(원자적 전환)
(이전 Blue)
즉시 swap 복원 가능
Snowflake 구현:
# 1. 스테이징에 배포
dbt run --target staging
dbt test --target staging
# 2. 헬스 체크 (행 수 조정, NULL 검사)
python scripts/post_deploy_check.py --compare-schemas
# 3. 검증 통과 → 원자적 전환
dbt run-operation deploy_promote
# 내부적으로: ALTER DATABASE analytics_staging SWAP WITH analytics_prod;
# 롤백 (즉시)
dbt run-operation deploy_rollback
# 내부적으로: ALTER DATABASE analytics_prod SWAP WITH analytics_staging;SWAP WITH는 Snowflake의 원자적 연산이다. 전환하는 동안 대시보드에 다운타임이 없다. 이전 Blue 스키마가 그대로 남아 있으므로 롤백도 동일한 swap 한 번이다.
CREATE OR REPLACE TABLE로 만든 dbt 모델은 Time Travel 창이 매 실행마다 초기화된다. Snowflake에서 dbt 롤백에 Time Travel을 쓰면 안 되는 이유다. Blue-Green swap이 올바른 메커니즘이다.
BigQuery 구현 (Time Travel 활용 가능):
-- 배포 전 타임스탬프를 기록해두고, 배포 후 문제 발생 시:
CREATE OR REPLACE TABLE `project.dataset.fct_orders`
AS SELECT * FROM `project.dataset.fct_orders`
FOR SYSTEM_TIME AS OF '2026-07-08T13:45:00Z';
-- BigQuery는 7일간 이력 유지Delta Lake / Databricks:
-- 배포 전 버전 확인
DESCRIBE HISTORY orders;
-- 롤백
RESTORE TABLE orders TO VERSION AS OF 5;
-- 또는
RESTORE TABLE orders TO TIMESTAMP AS OF '2026-07-08T13:45:00.000Z';Delta Lake의 RESTORE는 데이터를 복사하지 않고 트랜잭션 로그를 재생하는 메타데이터 연산이다. 단, VACUUM 이후에는 이전 버전의 데이터 파일이 삭제돼 복원 불가능해진다.
자동 롤백 트리거 설계
어떤 신호가 롤백을 트리거해야 하는가?
| 신호 | 수집 도구 | 임계값 예시 |
|---|---|---|
| 신선도 SLO 위반 | dbt source freshness | 매시간 파이프라인 → 2시간 초과 |
| 행 수 급락 | Elementary volume_anomalies | 14일 기준선 대비 20% 이상 감소 |
| NULL 비율 급증 | Elementary null_count_anomalies | 주요 컬럼 null_percent > 5% |
| dbt 테스트 실패 | run_results.json | 상태 fail/error 1건 이상 |
| DAG 실패율 급등 | Airflow Prometheus: airflow_ti_failures | 1시간 내 3회 이상 실패 |
| 스키마 드리프트 | Elementary schema_changes | 예기치 않은 컬럼 추가/삭제/타입 변경 |
| 분포 이상 | Monte Carlo ML 모니터 | anomaly score > threshold |
핵심 운영 원칙: "먼저 롤백하고, 나중에 디버깅한다(Rollback first, debug later)." 원인을 파악하면서 잘못된 데이터를 계속 제공하는 것은 잘못된 데이터를 전달하는 것보다 MTTR(평균 복구 시간)을 늘린다.
GitHub Actions 배포 파이프라인: 헬스 게이트 포함
# .github/workflows/dbt-deploy.yml
name: dbt Deploy + Health Gate
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: dbt 설치
run: pip install dbt-snowflake
- name: 소스 신선도 확인
run: dbt source freshness --output json --output-path target/sources.json
- name: 신선도 검증
run: |
python -c "
import json, sys
with open('target/sources.json') as f:
sources = json.load(f)
stale = [s for s in sources['results']
if s['status'] in ('error', 'runtime error')]
if stale:
print('신선도 이상 소스:', [s['unique_id'] for s in stale])
sys.exit(1)
"
- name: 스테이징 배포 (Blue-Green)
run: dbt run --target staging --select state:modified+
env:
DBT_TARGET: staging
- name: 스테이징 테스트
run: dbt test --target staging --select state:modified+
id: staging_tests
- name: 행 수 조정 검증
run: |
python scripts/reconcile.py \
--prod-schema analytics_prod \
--staging-schema analytics_staging \
--threshold 0.05 # 5% 이상 차이 시 실패
- name: 프로덕션으로 스왑
if: success()
run: dbt run-operation deploy_promote --target prod
- name: 실패 시 롤백
if: failure()
run: |
echo "배포 실패. 롤백 시작."
dbt run-operation deploy_rollback --target prod
- name: dbt 아티팩트 보관 (감사 이력)
if: always()
uses: actions/upload-artifact@v4
with:
name: dbt-artifacts-${{ github.sha }}
path: |
target/manifest.json
target/run_results.json
target/sources.jsonif: failure()로 헬스 게이트 실패 시 자동으로 롤백 매크로가 실행된다. 아티팩트는 github.sha를 포함한 이름으로 보관하여 커밋과 배포 결과를 연결한다.
배포 이력과 감사 추적
데이터 사고는 종종 "이 숫자가 바뀐 게 언제부터야?"라는 질문으로 시작한다. 배포 이력이 제대로 쌓여 있으면 이 질문에 5분 안에 답할 수 있다.
dbt 아티팩트가 만드는 감사 추적
| 파일 | 내용 | 용도 |
|---|---|---|
manifest.json | 전체 모델 그래프, 컴파일된 SQL, lineage | 어떤 코드가 배포됐는가 |
run_results.json | 모델별 실행 상태, 소요 시간, 오류 메시지 | 무엇이 통과/실패했는가 |
sources.json | 소스별 신선도 상태 | 데이터가 최신이었는가 |
manifest.json의 metadata.generated_at와 metadata.env.git_sha를 함께 저장하면 "언제, 어떤 코드로" 배포됐는지 나중에 정확히 재구성할 수 있다.
Elementary 테이블: 웨어하우스 내 감사 이력
Elementary는 dbt 패키지로 설치하면 on-run-end 훅을 통해 모든 실행 결과를 웨어하우스 테이블에 자동으로 기록한다.
# packages.yml
packages:
- package: elementary-data/elementary
version: [">=0.14.0", "<1.0.0"]dbt deps && dbt run --select elementary이후 모든 dbt run, dbt test 결과가 아래 테이블에 쌓인다.
elementary_test_results: 테스트 실행 결과 이력dbt_run_results: 모델 실행 이력 (행 수 포함)dbt_models: 전체 모델 메타데이터dbt_sources: 소스 신선도 이력
사고 발생 시 조회 예시:
-- 최근 24시간 내 fct_revenue에 어떤 변화가 있었나?
SELECT
generated_at,
rows_affected,
status,
invocation_id
FROM dbt_run_results
WHERE model_unique_id = 'model.my_project.fct_revenue'
AND generated_at >= DATEADD(hour, -24, CURRENT_TIMESTAMP())
ORDER BY generated_at DESC;Airflow 3.0 DAG 버전 추적
Airflow 3.0부터 GitDagBundle로 DAG 코드 버전이 각 DAG 실행에 기록된다.
# 어떤 DAG 실행이 어떤 git commit으로 실행됐는지 확인
airflow dags list-runs --dag-id orders_daily_pipeline
# Run ID | State | Git Bundle SHA | Start Date과거 실행을 동일한 코드 버전으로 재실행하는 옵션도 UI에서 제공된다.
구조화된 배포 로그
{
"deploy_id": "deploy-2026-07-08-abc123",
"git_sha": "a3f8c9d",
"git_actor": "[email protected]",
"dbt_run_id": "abc123-xyz",
"deploy_timestamp": "2026-07-08T14:00:00Z",
"models_affected": ["fct_orders", "dim_customers", "mart_revenue"],
"tests_passed": 47,
"tests_failed": 0,
"freshness_status": "pass",
"row_count_delta_pct": 0.3,
"deploy_outcome": "success"
}이런 구조화된 로그를 S3나 BigQuery에 쌓으면 "7월 8일 14시 이후 첫 번째 배포는 누가 무엇을 바꿨나"를 SQL로 바로 조회할 수 있다.
실전 사고 예시: 잘못된 JOIN이 대시보드를 망가뜨릴 때
사고 경위
엔지니어가 fct_revenue.sql을 리팩터링하며 LEFT JOIN을 INNER JOIN으로 변경했다. SQL 문법은 맞다. dbt의 not_null, unique 테스트도 모두 통과했다. 배포가 성공으로 기록됐다. 35분 후, Finance 팀이 주간 매출 대시보드에서 23% 매출 하락을 발견했다.
탐지 경로 (도구가 있는 경우)
Elementary volume_anomalies 테스트가 배포 후 15분 만에 fct_revenue 행 수가 8,200행 → 6,400행(22% 감소)으로 떨어졌음을 감지하고 Slack 알림을 보냈다. Finance 팀이 보고하기 20분 전에 이미 온콜에게 알림이 전달됐다.
탐지 경로 (도구가 없는 경우)
- Finance 보고 → 온콜 수신 (T+38분)
dbt_run_results쿼리 → 오늘fct_revenue행 수: 6,400 (어제: 8,200)git log --oneline -5→ 오늘 배포 커밋 발견git diff HEAD~1 HEAD -- models/marts/fct_revenue.sql→ JOIN 변경 확인- 원인: INNER JOIN이
campaign_id가 NULL인 정상 주문 1,800건을 제거
롤백 실행 (Snowflake Blue-Green)
# 스테이징에 이전 버전이 남아 있으므로 swap 한 번으로 복원
dbt run-operation deploy_rollback
# analytics_prod ↔ analytics_staging 원자적 교환
# 즉시 대시보드 정상화사후 대응
fct_revenue에volume_anomaliesElementary 테스트 추가- 배포 파이프라인에 스테이징 vs 프로덕션 행 수 조정 스텝 추가 (±10% 초과 시 차단)
- singular dbt 테스트 추가: 전일 대비 행 수 10% 이상 감소 시 실패
-- tests/fct_revenue_row_count_gate.sql
-- 0행 반환 = 통과, 1행 이상 = 실패
SELECT 1
FROM (
SELECT COUNT(*) AS today_cnt FROM {{ ref('fct_revenue') }}
WHERE DATE(created_at) = CURRENT_DATE
) today,
(
SELECT COUNT(*) AS yesterday_cnt FROM {{ ref('fct_revenue') }}
WHERE DATE(created_at) = CURRENT_DATE - 1
) yesterday
WHERE (today_cnt * 1.0 - yesterday_cnt) / NULLIF(yesterday_cnt, 0) < -0.10도구 선택 기준
| 상황 | 도구 선택 |
|---|---|
| dbt 프로젝트에서 이상 탐지 추가 비용 없이 | Elementary (dbt 패키지) |
| 명시적인 check 선언 + 다양한 DW 지원 | Soda Core |
| 코드 없이 ML 기반 자동 베이스라인 | Monte Carlo |
| Snowflake Blue-Green 롤백 | dbt run-operation (SWAP WITH) |
| BigQuery 롤백 | FOR SYSTEM_TIME AS OF |
| Delta Lake / Databricks 롤백 | RESTORE TABLE TO VERSION AS OF |
| 복잡한 카나리 검증 | 행 수 조정 쿼리 + 스키마 비교 |
References
- dbt Source Freshness: https://docs.getdbt.com/docs/deploy/source-freshness
- dbt Artifacts — manifest.json, run_results.json: https://docs.getdbt.com/reference/artifacts/dbt-artifacts
- elementary-data/dbt-data-reliability (GitHub): https://github.com/elementary-data/dbt-data-reliability
- Elementary Docs — Volume Anomalies: https://docs.elementary-data.com/data-tests/anomaly-detection-tests/volume-anomalies
- Soda Core SodaCL Metrics and Checks: https://docs.soda.io/sodacl-reference/metrics-and-checks
- Soda Core GitHub: https://github.com/sodadata/soda-core
- Monte Carlo + dbt Integration: https://docs.getmontecarlo.com/docs/dbt-integration
- Blue-Green dbt + Snowflake (Montreal Analytics): https://blog.montrealanalytics.com/blue-green-deployment-with-dbt-and-snowflake-922f1c658011
- dbt Production Rollbacks (community): https://discourse.getdbt.com/t/production-rollbacks/2513
- Rolling back dbt results on test failure: https://anddata.substack.com/p/rolling-back-dbt-results-on-test
- Snowflake Time Travel Documentation: https://docs.snowflake.com/en/user-guide/data-time-travel
- BigQuery Time Travel Documentation: https://cloud.google.com/bigquery/docs/time-travel
- Delta Lake RESTORE Documentation: https://delta.io/blog/2022-10-03-rollback-delta-lake-restore/
- Airflow 3.0 DAG Versioning (Astronomer): https://www.astronomer.io/docs/learn/airflow-dag-versioning/
- Airflow Metrics (StatsD/Prometheus): https://airflow.apache.org/docs/apache-airflow/stable/administration-and-deployment/logging-monitoring/metrics.html
- Datadog Monitors as GitHub Deployment Gates: https://docs.datadoghq.com/monitors/guide/github_gating/
- How to Create Automatic Rollback Triggers: https://oneuptime.com/blog/post/2026-01-30-automatic-rollback-triggers/view
- dbt Blue-Green on Snowflake (Walid Djebali): https://www.walid-djebali.fr/2025/08/blue-green-deployments-in-dbt-are-and-how-using-temporary-schemas-with-atomic-swaps/