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-уровня. Что дальше:
1. **Production experience.** Поработайте на real-world проекте с этими паттернами. Capstone — это симулятор; production — это где встречаются edge cases (warehouse-специфичные ограничения, business chaos, organizational politics).
2. **dbt III (Senior).** Если планируете lead/architect role — следующий курс. Углубление в Mesh, custom adapters, performance-tuning, Fusion engine internals, dbt в context'е big data (>10B rows).
3. **Adjacent tooling.** Airflow / Dagster для оркестрации больших pipelines. Elementary для data observability. Atlan/Alation для catalogs. Каждое — отдельный курс / book.
4. **Community participation.** Открытые dbt-packages, PR в dbt-utils / dbt-expectations, выступления на dbt Coalesce. Это accelerates карьерный рост.
5. **Specialization.** Analytics engineering branches'ит в несколько directions: data platform (infra-fокус), BI engineering (consumer-fокус), data science engineering (ML pipelines). Выберите своё.
---
## Что middle-инженер должен унести
После capstone вы должны:
1. Уметь спроектировать и реализовать production-grade dbt-проект от sources до exposures.
2. Знать ключевые ограничения DuckDB-adapter'а vs Snowflake / BigQuery и workarounds.
3. Настроить полный CI/CD pipeline на GitHub Actions с Slim CI.
4. Использовать Semantic Layer для определения metrics уровня компании.
5. Применять contracts и versions для безопасного развития production-моделей.
6. Документировать проект на уровне, достаточном для onboarding нового dev'а за день.
Этот capstone — **отметка перехода middle -> senior**. С ним в портфолио вы готовы вести production-проект самостоятельно.
---
## Попробуй сам (финал)
1. **Полный run capstone'а** — реализуйте все 4 урока 16-го модуля по-шагам. Делайте commit'ы по логическим блокам.
2. **Создайте PR на GitHub** в свой репо. Дождитесь, пока CI отработает. Зелёный CI — capstone сдан.
3. **Запустите `dbt docs serve`** — посмотрите финальный lineage. Найдите exposures, semantic_models. Видно ли всё, что декларировали?
4. **Откройте проект через 2 недели.** Поймёте ли структуру? Если нет — добавьте README и комментарии. Это критерий "maintainable code".
5. **Покажите проект кому-то** — коллеге, ментору, в data-сообществе. Получите feedback. Итерации улучшают проект.
Капстон — это начало. Хороший engineering — продолжение.
---
<KnowledgeCheck
question={`Команда внедряет Slim CI на GitHub Actions для dbt-проекта. После первого PR CI работает 25 минут — медленнее, чем ожидалось. dbt-build выполняется быстро (5 минут), но 'Setup Python + install dbt + dbt deps + download prod state' занимает 15 минут на каждом run. Какие оптимизации применить?`}
answer={`Проблема — отсутствие кеширования. Каждый CI run заново устанавливает Python, dbt, packages, скачивает state. Оптимизации: (1) Cache pip dependencies через actions/setup-python с cache: 'pip' — экономит ~2-3 минуты на pip install. (2) Cache dbt packages directory: actions/cache@v4 с key: \${{ hashFiles('packages.yml') }} и path: ./dbt_packages. После первого install packages кешируются. Экономия 1-2 минуты на dbt deps. (3) Use composite/reusable actions если несколько workflows используют тот же setup — снижает duplicates. (4) Prefer dbt-binary docker image если pip install устойчиво медленный (хотя обычно для dbt-duckdb pip нормален). (5) Parallelize шаги где можно: source freshness отдельный job (continue-on-error), build отдельный — но coupled через manifest, поэтому осторожно. (6) Для DuckDB workaround "seed + run staging" — рассмотреть кеширование .duckdb файла (но это hacky). (7) Use --select для тестов отдельным шагом с --fail-fast — если упал build, не запускать тесты. (8) Timeout: установить timeout-minutes на job — 20 для CI разумно, иначе stale jobs висят. После оптимизаций target — CI меньше 8 минут (5 build + 3 setup/checkout). Если still slow — посмотреть, не делает ли actions/checkout fetch-depth: 0 (full history), что для большого репо медленно — установить fetch-depth: 1. На больших проектах Snowflake / BigQuery CI ещё нужно делать "warehouse-side": результаты CI идут в отдельную PR-схему, которая cleanup'ится через post-cleanup workflow. Это отдельная тема.`}
/>