Capstone: CI/CD, Semantic Layer, финал
Финальный урок capstone’а. Связываем последние production-компоненты: CI/CD на GitHub Actions со Slim CI, Semantic Layer для 3 ключевых метрик, документация с persist_docs, exposures, и финальный чек-лист сдачи.
После этого урока у вас на руках — полный production-grade dbt-проект, готовый показывать на интервью.
dbt-iii: MetricFlow Internals — под капотом Semantic LayerЧасть 1: GitHub Actions Slim CI
Цель
На каждый PR валидировать только изменённые модели и их downstream. Без CI каждое изменение требует, чтобы reviewer вручную проверял, что ничего не сломалось. Slim CI делает это автоматически.
Структура workflow
# .github/workflows/dbt-ci.yml
name: dbt CI
on:
pull_request:
branches: [main]
permissions:
contents: read
pull-requests: write
env:
DBT_PROFILES_DIR: ./.dbt
PYTHON_VERSION: '3.11'
DBT_DUCKDB_VERSION: '1.10.0'
jobs:
slim-ci:
name: dbt Slim CI
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
- name: Install dbt-duckdb
run: |
pip install dbt-duckdb==${{ env.DBT_DUCKDB_VERSION }}
pip install sqlfluff sqlfluff-templater-dbt
- name: Configure dbt profile
run: |
mkdir -p ~/.dbt
cat > ~/.dbt/profiles.yml << 'EOF'
dbt_capstone_ecommerce:
outputs:
ci:
type: duckdb
path: 'capstone_ci.duckdb'
threads: 4
prod:
type: duckdb
path: 'capstone_prod.duckdb'
threads: 4
target: ci
EOF
- name: dbt deps
run: dbt deps
- name: Download prod state
uses: actions/download-artifact@v4
with:
name: prod-manifest
path: ./prod_state
continue-on-error: true # первый run без artifact допустим
- name: Source freshness check
run: dbt source freshness
continue-on-error: true # freshness — warning, не должен ломать PR
- name: dbt build Slim CI
run: |
if [ -f "./prod_state/manifest.json" ]; then
echo "Running Slim CI with state:modified+"
dbt build \
--select state:modified+ \
--defer \
--state ./prod_state \
--fail-fast
else
echo "No prod state, running full build"
dbt build --fail-fast
fi
- name: SQLFluff lint
run: |
sqlfluff lint models/ --dialect duckdb --templater dbt
- name: Upload CI artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: ci-target-${{ github.event.pull_request.number }}
path: target/
retention-days: 7
Разбор ключевых частей
Triggers: on: pull_request: branches: [main] — workflow срабатывает на каждый PR к main.
Setup: установка Python 3.11, dbt-duckdb 1.10, sqlfluff.
profiles.yml генерируется inline — credentials и пути не должны быть в репо. Для DuckDB достаточно path. Для Snowflake / BigQuery в этом месте использовались бы GitHub Secrets:
account: ${{ secrets.SNOWFLAKE_ACCOUNT }}
user: ${{ secrets.SNOWFLAKE_USER }}
password: ${{ secrets.SNOWFLAKE_PASSWORD }}
Download prod state: скачиваем manifest.json от последнего успешного main-build. Это база для state:modified+. На первом run artifact’а ещё нет — continue-on-error: true.
Source freshness check: проверяем актуальность sources. На fail — warning, не блокируем PR (sources могут быть legitimately stale в момент CI).
Slim CI core:
dbt build \
--select state:modified+ \
--defer \
--state ./prod_state \
--fail-fast
state:modified+— модели, которые изменились в PR относительно prod state, ПЛЮС их downstream--defer— ref’ы upstream’ов резолвятся в prod-схему (мы не пересобираем upstream локально)--state ./prod_state— где лежит prod manifest--fail-fast— упасть сразу при первой ошибке, не продолжать
SQLFluff lint: проверяем стиль SQL. Отдельный quality gate.
Upload CI artifacts: target/ folder с manifest.json + run_results.json — для debugging если CI fail.
Production deploy workflow
Параллельно — workflow для main-merge, который обновляет prod state:
# .github/workflows/dbt-prod-deploy.yml
name: dbt Prod Deploy
on:
push:
branches: [main]
schedule:
- cron: '0 6 * * *' # Daily at 06:00 UTC
jobs:
prod-deploy:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install dbt-duckdb==1.10.0
- name: Configure profiles
run: |
mkdir -p ~/.dbt
cat > ~/.dbt/profiles.yml << 'EOF'
dbt_capstone_ecommerce:
outputs:
prod:
type: duckdb
path: 'capstone_prod.duckdb'
threads: 4
target: prod
EOF
- run: dbt deps
- name: dbt build prod
run: dbt build --target prod --fail-fast
- name: Generate docs
run: dbt docs generate
- name: Upload prod manifest artifact
uses: actions/upload-artifact@v4
with:
name: prod-manifest
path: |
target/manifest.json
target/run_results.json
retention-days: 90
Этот workflow:
- Запускается на push в main и на schedule.
- Полный
dbt build(не Slim — это prod). - Загружает
manifest.jsonкак artifact, который CI workflow на PR может скачать.
Slim CI на DuckDB caveat
DuckDB файлы — это локальные .duckdb файлы. На каждый CI run они создаются с нуля. Это значит:
- Defer работает (manifest.json от prod есть), но физически данных upstream нет
- Когда модель ref’ит upstream — пытается прочитать таблицу, которой нет
Workaround для DuckDB CI: добавить шаг “seed all + run staging” перед Slim CI:
- name: Seed and run staging (DuckDB workaround)
run: |
dbt seed
dbt run --select tag:staging
- name: dbt build Slim CI
run: |
dbt build --select state:modified+ --defer --state ./prod_state --fail-fast
В production на Snowflake / BigQuery это не нужно — там данные физически в warehouse, defer резолвит ref’ы напрямую к prod tables.
Часть 2: Pre-commit hooks
Local quality gate перед commit’ом. Catch’ает обычные проблемы (отсутствующая yml на модель, нарушения стиля SQL) до push’а.
# .pre-commit-config.yaml
repos:
- repo: https://github.com/dbt-checkpoint/dbt-checkpoint
rev: v2.0.4
hooks:
- id: check-model-has-description
- id: check-model-has-tests
args: [--tests, "not_null", "unique", "--"]
- id: check-model-has-properties-file
- id: check-model-columns-have-desc
- repo: https://github.com/sqlfluff/sqlfluff
rev: 3.0.0
hooks:
- id: sqlfluff-lint
args: [--dialect, duckdb, --templater, dbt]
files: ^models/.*\.sql$
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-merge-conflict
Setup для разработчиков
pip install pre-commit
pre-commit install
После этого каждый git commit запустит hooks. Если SQL не lint’ится — commit блокируется до фикса.
В README.md упоминаем это явно — иначе разработчики недоумевают, почему commit “тихо” пропускается.
Часть 3: Semantic Layer
Идея
Semantic Layer (через MetricFlow) — это формальное определение метрик на уровне dbt YAML, которые BI-инструменты (Tableau, Looker, Hex, Mode) могут запрашивать через единый API.
Без Semantic Layer: каждый dashboard переопределяет revenue по-своему. Один считает “до возвратов”, другой “после”, третий с фильтром по country — три разных числа. Semantic Layer решает это: одно определение revenue, все consumers получают то же.
3 semantic_models для capstone
Simple metric: total_revenue
# semantic_models/revenue.yml
semantic_models:
- name: revenue
description: "Revenue measures from mart_orders"
model: ref('mart_orders')
defaults:
agg_time_dimension: order_created_at
entities:
- name: order
type: primary
expr: order_id
- name: user
type: foreign
expr: user_id
dimensions:
- name: order_created_at
type: time
type_params:
time_granularity: day
- name: shipping_address_country
type: categorical
- name: order_status
type: categorical
measures:
- name: total_revenue_usd
description: "Sum of order_total_usd, USD"
agg: sum
expr: order_total_usd
- name: order_count
agg: count_distinct
expr: order_id
- name: avg_order_value_usd
agg: average
expr: order_total_usd
metrics:
- name: total_revenue
description: "Total revenue from successful orders"
type: simple
label: "Total Revenue"
type_params:
measure:
name: total_revenue_usd
filter: "{{ Dimension('order__order_status') }} IN ('paid', 'shipped', 'delivered')"
Ratio metric: conversion_rate
# semantic_models/conversion_rate.yml
semantic_models:
- name: sessions
description: "Session-level measures from int_events_session"
model: ref('int_events_session')
defaults:
agg_time_dimension: session_start_at
entities:
- name: session
type: primary
expr: session_id
- name: user
type: foreign
expr: user_id
dimensions:
- name: session_start_at
type: time
type_params:
time_granularity: day
measures:
- name: session_count
agg: count_distinct
expr: session_id
metrics:
- name: orders_per_session
description: "Order count / session count — proxy for conversion"
type: ratio
label: "Orders per Session"
type_params:
numerator:
name: order_count
denominator:
name: session_count
Cumulative metric: cumulative_revenue
# semantic_models/cumulative_revenue.yml
metrics:
- name: cumulative_revenue
description: "Running total revenue from start of year"
type: cumulative
label: "Cumulative Revenue (YTD)"
type_params:
measure:
name: total_revenue_usd
filter: "{{ Dimension('order__order_status') }} IN ('paid', 'shipped', 'delivered')"
cumulative_type_params:
period_agg: last
grain_to_date: year
Тестирование MetricFlow
В dbt 1.10+ есть dbt mf query:
dbt mf query --metrics total_revenue --group-by shipping_address_country --order metric_time__day
Это даёт SQL и таблицу результатов. Полезно для проверки, что метрика работает корректно.
Semantic Layer на DuckDB. MetricFlow в dbt 1.10 поддерживает DuckDB как warehouse для теста. Production semantic layer API (GraphQL endpoint для BI) — Cloud Enterprise фича. Для capstone достаточно YAML-определений + dbt mf query для верификации.
Saved queries
# semantic_models/saved_queries.yml
saved_queries:
- name: weekly_revenue_by_country
description: "Weekly revenue breakdown by top 10 countries"
query_params:
metrics:
- total_revenue
group_by:
- "Dimension('order__shipping_address_country')"
- "TimeDimension('metric_time', 'week')"
where:
- "{{ Dimension('order__shipping_address_country') }} IN ('US', 'GB', 'DE', 'FR', 'JP', 'CA', 'AU', 'BR', 'IN', 'NL')"
exports:
- name: weekly_revenue_export
config:
export_as: table
Saved queries — это готовые reports для BI. Можно сохранить как table/view в warehouse через export.
Часть 4: Документация и exposures
persist_docs для синхронизации в warehouse
# dbt_project.yml
models:
dbt_capstone_ecommerce:
+persist_docs:
relation: true
columns: true
После dbt run, описания из YAML попадают в COMMENT’ы таблиц и колонок в warehouse. Это значит:
- BI-инструменты, читающие schema, видят descriptions
SHOW TABLES,DESCRIBE TABLEпоказывают comments- Data discovery tools (Atlan, Alation) подхватывают metadata автоматически
Doc blocks для переиспользования
-- models/marts/docs.md
{% docs revenue_usd %}
Revenue в USD с двумя десятичными. Конвертация по daily-rate из таблицы exchange_rates.
Включает только заказы со статусами: paid, shipped, delivered. Cancelled и fraudulent — исключены.
{% enddocs %}
{% docs order_status_enum %}
Возможные значения order_status:
- pending: заказ создан, оплата не получена
- paid: оплата получена
- shipped: товар отправлен
- delivered: товар доставлен
- cancelled: заказ отменён до отправки
- fraudulent: подозрение на мошенничество, заблокировано
{% enddocs %}
Ссылка из YAML:
columns:
- name: total_revenue_usd
description: "{{ doc('revenue_usd') }}"
- name: order_status
description: "{{ doc('order_status_enum') }}"
Это делает доки DRY: одно определение reuse в десятке моделей.
Exposures: 5 downstream систем
# models/_exposures.yml
exposures:
- name: executive_dashboard
type: dashboard
description: "CEO/CFO weekly review dashboard в Tableau"
url: 'https://tableau.company.com/views/executive-dashboard'
owner:
name: Data Team Lead
email: [email protected]
depends_on:
- ref('mart_revenue_daily')
- ref('mart_users_360')
- metric('total_revenue')
- metric('cumulative_revenue')
- name: marketing_attribution_report
type: dashboard
description: "Campaign performance в Looker"
url: 'https://looker.company.com/dashboards/marketing-attribution'
owner:
name: Marketing Analytics Lead
email: [email protected]
depends_on:
- ref('mart_campaigns_attribution')
- name: ml_churn_model
type: ml
description: "Churn prediction model, trained weekly from mart_users_360"
owner:
name: ML Engineer
email: [email protected]
depends_on:
- ref('mart_users_360')
- name: customer_support_lookup
type: application
description: "Internal app for support team — order lookup"
url: 'https://internal-tools.company.com/order-lookup'
owner:
name: Customer Support Engineering
email: [email protected]
depends_on:
- ref('mart_orders')
- name: finance_close_pack
type: report
description: "Monthly financial close report — sent to CFO"
owner:
name: Finance Director
email: [email protected]
depends_on:
- ref('mart_revenue_daily')
- ref('mart_payment_summary')
Exposures дают:
- Lineage до downstream (видно в
dbt docs serve) - Ownership downstream систем (кто отвечает за ML model)
- Breaking change checklist (хочешь удалить колонку в mart_orders? Проверь exposures, депендают ли)
Часть 5: README.md и финальная сдача
README.md — обязательная часть проекта
# dbt-capstone-ecommerce
Production-grade dbt-проект e-commerce analytics для GrandShop.
## Архитектура
- 30+ моделей в medallion-структуре: staging -> intermediate -> marts
- Incremental materializations для fact-таблиц (mart_orders, mart_revenue_daily)
- Snapshots с SCD2 для customers, products, prices
- 3 marts с enforced contracts
- Semantic Layer: 3 metrics (revenue, conversion_rate, cumulative_revenue)
- 5 exposures для downstream систем
## Tech stack
- dbt-core 1.10.21
- dbt-duckdb 1.10.0 (warehouse — DuckDB)
- packages: dbt_utils 1.3.0, dbt_expectations 0.10.4
- CI/CD: GitHub Actions со Slim CI
- Local quality gate: pre-commit + sqlfluff
## Run locally
```bash
python -m venv venv
source venv/bin/activate
pip install dbt-duckdb==1.10.0
dbt deps
dbt seed
dbt build
```
## CI/CD
PR triggers `dbt-ci.yml`:
- Slim CI: `state:modified+ --defer`
- SQLFluff lint
- Source freshness check
Main merge triggers `dbt-prod-deploy.yml`:
- Full `dbt build`
- Generates docs
- Uploads manifest as artifact (for next PR's defer state)
## Conventions
- Naming: `stg_<source>__<entity>`, `int_<domain>__<purpose>`, `<grain>_<entity>` for marts
- Tests: not_null + unique on PK, accepted_values on enums, dbt-expectations for ranges
- Documentation: doc blocks for reusable descriptions, persist_docs to warehouse
## Owners
См. ownership в `models/*/
_models.yml`.
Чек-лист сдачи проекта
Финальная самопроверка перед “сдачей”:
Архитектура
- 30+ моделей в проекте
- Структура staging/intermediate/marts соблюдена
- Naming conventions consistent
-
_sources.ymlдля всех sources -
_models.ymlв каждой папке
Materializations
- mart_orders — incremental (merge с predicates)
- mart_revenue_daily — incremental (delete+insert)
- int_events_session — microbatch
- 3 snapshots работают (customers, products, prices)
Tests
- not_null/unique на всех PK
- accepted_values на enums (order_status, country)
- 5+ dbt-expectations tests
- 1+ custom generic test
- 3+ unit tests с given/expect
- severity grades распределены (error vs warn)
- store_failures настроен на 3+ critical tests
Contracts и versions
- 3 marts с enforced contract
- 1 mart с versions (v1 + v2)
Documentation
- Descriptions на всех marts
- Column descriptions на public marts
- doc blocks для reusable terms
- persist_docs configured
-
dbt docs generateбез warnings
Semantic Layer
- 3 metrics: total_revenue, orders_per_session, cumulative_revenue
- Saved query: weekly_revenue_by_country
Exposures
- 5 exposures: executive_dashboard, marketing_attribution_report, ml_churn_model, customer_support_lookup, finance_close_pack
CI/CD
-
.github/workflows/dbt-ci.ymlс Slim CI -
.github/workflows/dbt-prod-deploy.ymlдля main -
.pre-commit-config.yamlс sqlfluff + dbt-checkpoint - CI зелёный на test PR
Финальные проверки
-
dbt build --full-refreshзелёный -
dbt testвсе тесты pass (warnings ОК) -
sqlfluff lint models/пройден - README.md описывает проект
- Git history чистый, осмысленные commit messages
Постфинал: что дальше
После capstone у вас есть proof of work для intermediate-уровня. Что дальше:
-
Production experience. Поработайте на real-world проекте с этими паттернами. Capstone — это симулятор; production — это где встречаются edge cases (warehouse-специфичные ограничения, business chaos, organizational politics).
-
dbt III (Senior). Если планируете lead/architect role — следующий курс. Углубление в Mesh, custom adapters, performance-tuning, Fusion engine internals, dbt в context’е big data (>10B rows).
-
Adjacent tooling. Airflow / Dagster для оркестрации больших pipelines. Elementary для data observability. Atlan/Alation для catalogs. Каждое — отдельный курс / book.
-
Community participation. Открытые dbt-packages, PR в dbt-utils / dbt-expectations, выступления на dbt Coalesce. Это accelerates карьерный рост.
-
Specialization. Analytics engineering branches’ит в несколько directions: data platform (infra-fокус), BI engineering (consumer-fокус), data science engineering (ML pipelines). Выберите своё.
Что middle-инженер должен унести
После capstone вы должны:
- Уметь спроектировать и реализовать production-grade dbt-проект от sources до exposures.
- Знать ключевые ограничения DuckDB-adapter’а vs Snowflake / BigQuery и workarounds.
- Настроить полный CI/CD pipeline на GitHub Actions с Slim CI.
- Использовать Semantic Layer для определения metrics уровня компании.
- Применять contracts и versions для безопасного развития production-моделей.
- Документировать проект на уровне, достаточном для onboarding нового dev’а за день.
Этот capstone — отметка перехода middle -> senior. С ним в портфолио вы готовы вести production-проект самостоятельно.
Попробуй сам (финал)
-
Полный run capstone’а — реализуйте все 4 урока 16-го модуля по-шагам. Делайте commit’ы по логическим блокам.
-
Создайте PR на GitHub в свой репо. Дождитесь, пока CI отработает. Зелёный CI — capstone сдан.
-
Запустите
dbt docs serve— посмотрите финальный lineage. Найдите exposures, semantic_models. Видно ли всё, что декларировали? -
Откройте проект через 2 недели. Поймёте ли структуру? Если нет — добавьте README и комментарии. Это критерий “maintainable code”.
-
Покажите проект кому-то — коллеге, ментору, в data-сообществе. Получите feedback. Итерации улучшают проект.
Капстон — это начало. Хороший engineering — продолжение.