LLM WikiAccess-protected knowledge portal
← 스터디 홈
7편 · 약 24분

배포 후 모니터링: SLO 연동, 자동 롤백 트리거, 배포 이력

데이터 플랫폼 배포가 특별히 위험한 이유

일반 소프트웨어 배포가 실패하면 프로세스가 죽거나, HTTP 500이 터지거나, 익셉션 로그가 쌓인다. 신호가 크고 즉각적이다. 데이터 플랫폼 배포 실패는 다르다. Airflow DAG이 성공 상태로 완료되는 동안, 테이블에는 잘못된 수치가 조용히 적재될 수 있다. dbt 모델이 runtest를 모두 통과했는데 24시간 후 Finance 팀이 매출 대시보드의 숫자가 23% 낮다고 보고하는 경우가 그 예다.

Airflow가 추적하는 것은 "Python 태스크가 예외를 던졌는가" 뿐이다. 빈 파티션, 중복 행, LEFT JOIN을 INNER JOIN으로 바꿔서 생긴 데이터 누락은 태스크 실패를 일으키지 않는다. 그리고 문제는 lineage를 타고 내려간다. staging 모델 하나가 잘못되면 그것을 의존하는 모든 mart와 대시보드가 함께 오염된다.

배포 후 모니터링은 이 조용한 실패를 자동으로 잡는 시스템이다.


데이터 파이프라인 SLO: 세 가지 유형

SLO(Service Level Objective)는 "무엇이 정상인지"를 수치로 정의한다. 데이터 플랫폼에서는 세 가지 유형이 핵심이다.

① 신선도 SLO (Freshness)
최신 레코드 타임스탬프 ≤ 현재 - N분
예: 매시간 파이프라인 → 60분 이내
SLI: MAX(loaded_at) vs NOW()
② 볼륨 SLO (Row Count)
행 수가 14일 기준선 ±20% 이내
예: 오늘 6,400행 vs 기준 8,200행 → 이상
SLI: |today - baseline| / baseline
③ 품질 SLO (Quality)
NULL 비율 <1%, 중복 0건,
도메인 값 범위 내
SLI: dbt test / Soda checks
SLA = 비즈니스 계약 | SLO = 운영 목표 | SLI = 실제 측정값 (지표)
데이터 파이프라인 SLO 계층

신선도 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 스키마 배포를 쓴다.

현재 PROD (Blue)
analytics_prod
fct_orders
8,200행 ✓
대시보드가 읽는 중
스테이징 (Green)
analytics_staging
dbt run 실행
dbt test 실행
행 수 / NULL / 신선도 검증
검증 통과

SWAP WITH
(원자적 전환)
전환 즉시 대시보드 반영
롤백 가능 상태
old_prod
(이전 Blue)
즉시 swap 복원 가능
Blue-Green 데이터 배포 흐름

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_anomalies14일 기준선 대비 20% 이상 감소
NULL 비율 급증Elementary null_count_anomalies주요 컬럼 null_percent > 5%
dbt 테스트 실패run_results.json상태 fail/error 1건 이상
DAG 실패율 급등Airflow Prometheus: airflow_ti_failures1시간 내 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.json

if: failure()로 헬스 게이트 실패 시 자동으로 롤백 매크로가 실행된다. 아티팩트는 github.sha를 포함한 이름으로 보관하여 커밋과 배포 결과를 연결한다.


배포 이력과 감사 추적

데이터 사고는 종종 "이 숫자가 바뀐 게 언제부터야?"라는 질문으로 시작한다. 배포 이력이 제대로 쌓여 있으면 이 질문에 5분 안에 답할 수 있다.

dbt 아티팩트가 만드는 감사 추적

파일내용용도
manifest.json전체 모델 그래프, 컴파일된 SQL, lineage어떤 코드가 배포됐는가
run_results.json모델별 실행 상태, 소요 시간, 오류 메시지무엇이 통과/실패했는가
sources.json소스별 신선도 상태데이터가 최신이었는가

manifest.jsonmetadata.generated_atmetadata.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 JOININNER JOIN으로 변경했다. SQL 문법은 맞다. dbt의 not_null, unique 테스트도 모두 통과했다. 배포가 성공으로 기록됐다. 35분 후, Finance 팀이 주간 매출 대시보드에서 23% 매출 하락을 발견했다.

탐지 경로 (도구가 있는 경우)

Elementary volume_anomalies 테스트가 배포 후 15분 만에 fct_revenue 행 수가 8,200행 → 6,400행(22% 감소)으로 떨어졌음을 감지하고 Slack 알림을 보냈다. Finance 팀이 보고하기 20분 전에 이미 온콜에게 알림이 전달됐다.

탐지 경로 (도구가 없는 경우)

  1. Finance 보고 → 온콜 수신 (T+38분)
  2. dbt_run_results 쿼리 → 오늘 fct_revenue 행 수: 6,400 (어제: 8,200)
  3. git log --oneline -5 → 오늘 배포 커밋 발견
  4. git diff HEAD~1 HEAD -- models/marts/fct_revenue.sql → JOIN 변경 확인
  5. 원인: INNER JOIN이 campaign_id가 NULL인 정상 주문 1,800건을 제거

롤백 실행 (Snowflake Blue-Green)

# 스테이징에 이전 버전이 남아 있으므로 swap 한 번으로 복원
dbt run-operation deploy_rollback
# analytics_prod ↔ analytics_staging 원자적 교환
# 즉시 대시보드 정상화

사후 대응

  1. fct_revenuevolume_anomalies Elementary 테스트 추가
  2. 배포 파이프라인에 스테이징 vs 프로덕션 행 수 조정 스텝 추가 (±10% 초과 시 차단)
  3. 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/