Learning Platform
Глоссарий Troubleshooting
Урок 17.04 · 35 мин
Средний
capstonegithub-actionsslim-cisemantic-layermetricflowexposuresdocumentation

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:

  1. Запускается на push в main и на schedule.
  2. Полный dbt build (не Slim — это prod).
  3. Загружает 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 и таблицу результатов. Полезно для проверки, что метрика работает корректно.

NOTE

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. Это отдельная тема.`}
/>

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 6. В GitHub Actions Slim CI workflow для capstone есть шаг 'Download prod state' с 'continue-on-error: true'. Зачем продолжать при отсутствии artifact и какая логика в основном build step?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4