Skip to main content

10 - dbt in Production: Data Governance, Semantic Layer and Orchestration with Airflow

dbt 19 min dbt in Produzione: Data Governance, Semantic Layer e Orchestrazione con Airflow Architettura dbt enterprise: accesso granulare a modelli con...

What do you think about this article?

At a Glance

dbt 19 min dbt in Produzione: Data Governance, Semantic Layer e Orchestrazione con Airflow Architettura dbt enterprise: accesso granulare a modelli con...

dbt 19 min

dbt in Production: Data Governance, Semantic Layer and Orchestration with Airflow

Enterprise dbt architecture: granular access to models with governance policies, dbt Semantic Layer (MetricFlow) for consistent metrics that any BI tool can query, Integration with Apache Airflow for cross-pipeline dependency orchestration.

dbt production dbt semantic layer airflow dbt data governance dbt MetricFlow

dbt in Production: What Changes Compared to Development

Moving dbt from a development project to a production system used by dozens of analysts and consumer BI requires three additional components that are not necessary in development: one strategy of governance to control who sees and modifies what, a Semantic Layer to ensure everyone uses the same definitions of metrics, and a robust orchestrator to manage dependencies between dbt and the rest of the data platform.

This final module in the series addresses each of the three themes with a practical approach: Real YAML configurations, working Airflow code, and ready-to-use MetricFlow patterns.

Data Governance: Access Control and Policy

Groups and Access to Templates

dbt 1.5 introduced i groups and the system of access control about models: Each model can have an access level that determines who can reference it with ref().

# models/groups.yml
# Definisci i gruppi del progetto

version: 2

groups:
  - name: finance
    owner:
      name: Finance Analytics Team
      email: finance-analytics@company.com

  - name: marketing
    owner:
      name: Marketing Analytics Team
      email: marketing@company.com

  - name: platform
    owner:
      name: Data Platform Team
      email: data-platform@company.com
# models/marts/finance/schema.yml
# Assegna i modelli finance al gruppo finance con accesso protetto

version: 2

models:
  - name: fct_orders
    group: finance
    access: public       # qualsiasi modello del progetto può referenziarlo

  - name: fct_revenue_detailed
    group: finance
    access: protected    # solo modelli dello stesso gruppo possono referenziarlo

  - name: int_order_calculations
    group: finance
    access: private      # solo modelli nella stessa directory possono referenziarlo

  - name: finance_internal_adjustments
    group: finance
    access: private      # dati sensibili: solo il gruppo finance
-- Se il team marketing prova a referenziare un modello privato del team finance:
-- models/marts/marketing/fct_campaigns.sql

SELECT *
FROM {{ ref('finance_internal_adjustments') }}  -- ERRORE in dbt compile!

-- dbt error: Node 'model.my_project.fct_campaigns' attempted to reference
-- 'model.my_project.finance_internal_adjustments', however 'finance_internal_adjustments'
-- is private to the 'finance' group.

-- La soluzione: il team platform espone solo i modelli public come contratto stabile

Model Versioning: Stable Contracts with Consumers

# dbt 1.5+ supporta il versioning dei modelli
# Utile quando modelli pubblici vengono usati da sistemi esterni (API, altri progetti dbt)

# models/marts/v1/schema.yml
version: 2

models:
  - name: fct_orders
    latest_version: 2    # indica quale è la versione corrente

    versions:
      - v: 1
        description: "Versione originale, deprecata. Usa v2."
        config:
          alias: fct_orders_v1
        deprecation_date: 2025-06-01

      - v: 2
        description: "Versione corrente con campo net_revenue e segmentazione cliente."
        columns:
          - include: all
          - name: net_revenue   # colonna aggiunta in v2
          - name: customer_tier # colonna aggiunta in v2
-- Consumer che puntano esplicitamente a una versione:
SELECT * FROM {{ ref('fct_orders', v=1) }}  -- usa la v1 (deprecated)
SELECT * FROM {{ ref('fct_orders', v=2) }}  -- usa la v2 (current)
SELECT * FROM {{ ref('fct_orders') }}        -- usa latest_version (v2)

Contract: Guarantee the Public Model Scheme

# Per modelli pubblici critici, abilita il contract enforcement:
# dbt non può fare dbt run se lo schema non corrisponde esattamente

models:
  - name: fct_orders
    access: public
    group: finance
    config:
      contract:
        enforced: true   # dbt fallisce se le colonne non corrispondono al schema YAML

    columns:
      - name: order_id
        data_type: varchar
        constraints:
          - type: not_null
          - type: unique

      - name: customer_id
        data_type: varchar
        constraints:
          - type: not_null

      - name: total_amount
        data_type: numeric
        constraints:
          - type: not_null

      - name: order_date
        data_type: date
        constraints:
          - type: not_null

dbt Semantic Layer: Consistent Metrics for All BI Tools

Il Semantic Layer of dbt (based on MetricFlow) solves one of the problems most frustrating in data teams: metrics calculated differently by different analysts. “Revenue” in Tableau calculates the gross, in PowerBI it calculates the net, in the notebook Python includes refunds. Who is right?

With the Semantic Layer, you define metrics once in YAML. Every BI tool that you connects to the Semantic Layer receives the same definition, the same logic, the same numbers.

Semantic Models: Defining Entities and Measurements

# models/semantic_models/sem_orders.yml
# Un semantic model è come un "cubo OLAP" dichiarativo

semantic_models:
  - name: orders
    description: "Modello semantico degli ordini completati."
    model: ref('fct_orders')

    # Entità: chiavi per i join semantici
    entities:
      - name: order
        type: primary
        expr: order_id

      - name: customer
        type: foreign
        expr: customer_id

    # Dimensioni: attributi per il raggruppamento
    dimensions:
      - name: order_date
        type: time
        type_params:
          time_granularity: day   # granularità minima: giorno

      - name: status
        type: categorical

      - name: acquisition_channel
        type: categorical

      - name: customer_country
        type: categorical

    # Misure: aggregazioni pre-definite
    measures:
      - name: order_count
        description: "Numero totale di ordini."
        agg: count
        expr: order_id

      - name: gross_revenue
        description: "Ricavo lordo in EUR (prima di sconti)."
        agg: sum
        expr: gross_revenue

      - name: net_revenue
        description: "Ricavo netto in EUR (dopo sconti, prima di rimborsi)."
        agg: sum
        expr: net_revenue

      - name: unique_customers
        description: "Clienti unici con almeno un ordine."
        agg: count_distinct
        expr: customer_id

      - name: avg_order_value
        description: "Valore medio degli ordini."
        agg: average
        expr: total_amount

Metrics: KPIs from Semantic Models

# models/metrics/metrics.yml
# Le metriche referenziano le misure dei semantic models

metrics:
  # Metrica semplice: mapping diretto su una misura
  - name: revenue
    label: "Ricavo Netto"
    description: "Ricavo netto totale in EUR."
    type: simple
    type_params:
      measure: net_revenue

  - name: orders
    label: "Numero Ordini"
    type: simple
    type_params:
      measure: order_count

  # Metrica derivata: operazione su altre metriche
  - name: average_order_value
    label: "Valore Medio Ordine"
    description: "Ricavo netto medio per ordine."
    type: derived
    type_params:
      expr: revenue / orders
      metrics:
        - name: revenue
        - name: orders

  # Metrica ratio: calcolo di proporzione
  - name: organic_revenue_share
    label: "Quota Ricavo Organico"
    description: "Percentuale di ricavo proveniente da canale organico."
    type: ratio
    type_params:
      numerator:
        name: net_revenue
        filter: "{{ Dimension('orders__acquisition_channel') }} = 'organic'"
      denominator:
        name: net_revenue

  # Metrica cumulativa: running total
  - name: cumulative_revenue_ytd
    label: "Ricavo Cumulativo YTD"
    type: cumulative
    type_params:
      measure: net_revenue
      window: unbounded           # dall'inizio dell'anno (con grain=year)
      grain_to_date: year

Query the Semantic Layer

-- Via CLI dbt (dbt Cloud o dbt Core con server semantics):
dbt sl query \
  --metrics revenue,orders,average_order_value \
  --group-by order_date__month,customer_country \
  --where "order_date__month >= '2024-01-01'" \
  --order revenue

-- Output: tabella con colonne:
-- order_date__month | customer_country | revenue | orders | average_order_value
# Via Python con dbt Semantic Layer API (dbt Cloud Enterprise)
from dbt_sl_sdk import SemanticLayerClient

client = SemanticLayerClient(
    environment_id=12345,
    auth_token="my_token"
)

result = client.query(
    metrics=["revenue", "orders", "average_order_value"],
    group_by=["order_date__month", "customer_country"],
    where=[{"filter": "order_date__month >= '2024-01-01'"}],
    order_by=[{"metric": "revenue", "descending": True}]
)

df = result.as_dataframe()

Orchestration with Apache Airflow

Airflow is the most used orchestrator in enterprise environments. Integration with dbt use the provider apache-airflow-providers-dbt-cloud (for dbt Cloud) or the Cosmos package (for dbt Core, which translates the dbt DAG into Airflow tasks).

Pattern 1: dbt with BashOperator (Simple)

# dags/dbt_daily_pipeline.py
# Il pattern più semplice: BashOperator che chiama la CLI dbt

from airflow.decorators import dag, task_group
from airflow.operators.bash import BashOperator
from airflow.utils.dates import days_ago
from datetime import timedelta

DBT_PROJECT_DIR = '/opt/airflow/dbt'
DBT_PROFILES_DIR = '/opt/airflow/dbt'

default_args = {
    'owner': 'data-platform',
    'depends_on_past': False,
    'retries': 1,
    'retry_delay': timedelta(minutes=10),
    'on_failure_callback': lambda ctx: alert_slack(ctx)
}

@dag(
    dag_id='dbt_daily_production',
    schedule_interval='0 6 * * *',   # 06:00 UTC ogni giorno
    start_date=days_ago(1),
    default_args=default_args,
    catchup=False,
    tags=['dbt', 'production']
)
def dbt_daily():

    source_freshness = BashOperator(
        task_id='dbt_source_freshness',
        bash_command=f'cd {DBT_PROJECT_DIR} && dbt source freshness --target prod',
        env={'DBT_PROFILES_DIR': DBT_PROFILES_DIR}
    )

    run_staging = BashOperator(
        task_id='dbt_run_staging',
        bash_command=f'cd {DBT_PROJECT_DIR} && dbt run --select tag:staging --target prod',
        env={'DBT_PROFILES_DIR': DBT_PROFILES_DIR}
    )

    test_staging = BashOperator(
        task_id='dbt_test_staging',
        bash_command=f'cd {DBT_PROJECT_DIR} && dbt test --select tag:staging --target prod',
        env={'DBT_PROFILES_DIR': DBT_PROFILES_DIR}
    )

    run_marts = BashOperator(
        task_id='dbt_run_marts',
        bash_command=f'cd {DBT_PROJECT_DIR} && dbt run --select tag:daily --exclude tag:staging --target prod',
        env={'DBT_PROFILES_DIR': DBT_PROFILES_DIR}
    )

    test_marts = BashOperator(
        task_id='dbt_test_marts',
        bash_command=f'cd {DBT_PROJECT_DIR} && dbt test --select tag:daily --exclude tag:staging --target prod',
        env={'DBT_PROFILES_DIR': DBT_PROFILES_DIR}
    )

    run_snapshots = BashOperator(
        task_id='dbt_snapshots',
        bash_command=f'cd {DBT_PROJECT_DIR} && dbt snapshot --target prod',
        env={'DBT_PROFILES_DIR': DBT_PROFILES_DIR}
    )

    # Definizione delle dipendenze
    source_freshness >> run_staging >> test_staging >> run_marts >> test_marts
    run_staging >> run_snapshots  # snapshots in parallelo ai marts

dag = dbt_daily()

Pattern 2: Astronomer Cosmos (DAG dbt → Task Airflow)

Cosmos is Astronomer's open-source library that translates every dbt model in a separate Airflow task, making the Airflow DAG a perfect mirror of the DAG dbt. Each model appears as a task, with dependencies displayed correctly in the Airflow UI.

# dags/dbt_cosmos_dag.py
# Cosmos: ogni modello dbt diventa un task Airflow separato

from airflow.decorators import dag
from airflow.utils.dates import days_ago
from cosmos import DbtDag, ProjectConfig, ProfileConfig, ExecutionConfig
from cosmos.profiles import SnowflakeUserPasswordProfileMapping

profile_config = ProfileConfig(
    profile_name="my_project",
    target_name="prod",
    profile_mapping=SnowflakeUserPasswordProfileMapping(
        conn_id="snowflake_prod",
        profile_args={
            "database": "analytics",
            "schema": "marts",
            "warehouse": "compute_wh_m"
        }
    )
)

@dag(
    dag_id='dbt_cosmos_pipeline',
    schedule_interval='0 6 * * *',
    start_date=days_ago(1),
    catchup=False
)
def cosmos_dbt_dag():
    dbt_project = DbtDag(
        project_config=ProjectConfig(
            dbt_project_path='/opt/airflow/dbt',
        ),
        profile_config=profile_config,
        execution_config=ExecutionConfig(
            dbt_executable_path='/usr/local/bin/dbt',
        ),
        # Seleziona solo i modelli taggati 'daily'
        select=['tag:daily'],
        schedule_interval=None,   # il DAG padre gestisce lo schedule
        dag_id='dbt_models_daily'
    )
    return dbt_project

dag = cosmos_dbt_dag()

Pattern 3: Cross-System Pipeline with Sensors

# dags/full_data_platform_pipeline.py
# Pattern reale: dbt aspetta il completamento dell'ingestion (Fivetran/Airbyte)

from airflow.decorators import dag, task
from airflow.operators.bash import BashOperator
from airflow.providers.http.sensors.http import HttpSensor
from airflow.sensors.external_task import ExternalTaskSensor
from airflow.utils.dates import days_ago
from datetime import timedelta

@dag(
    dag_id='full_data_platform',
    schedule_interval='0 5 * * *',
    start_date=days_ago(1),
    catchup=False
)
def full_pipeline():

    # 1. Aspetta il completamento della sync Fivetran (webhook o polling)
    wait_for_fivetran = HttpSensor(
        task_id='wait_fivetran_sync',
        http_conn_id='fivetran_api',
        endpoint='/v1/connectors/abc123/last-sync',
        response_check=lambda response: response.json()['data']['status']['sync_state'] == 'ready',
        poke_interval=300,    # controlla ogni 5 minuti
        timeout=7200,         # timeout dopo 2 ore
        mode='reschedule'     # libera il worker tra i check
    )

    # 2. Aspetta il DAG di ingestion Kafka se esiste
    wait_for_kafka_ingestion = ExternalTaskSensor(
        task_id='wait_kafka_ingestion',
        external_dag_id='kafka_events_ingestion',
        external_task_id='load_to_warehouse',
        timeout=3600,
        mode='reschedule'
    )

    # 3. dbt source freshness (verifica che i dati siano davvero arrivati)
    source_freshness = BashOperator(
        task_id='dbt_source_freshness',
        bash_command='cd /opt/airflow/dbt && dbt source freshness --target prod'
    )

    # 4. dbt build completo
    dbt_build = BashOperator(
        task_id='dbt_build',
        bash_command='cd /opt/airflow/dbt && dbt build --target prod --select tag:daily'
    )

    # 5. Notifica downstream: BI refresh, ML retraining, ecc.
    @task
    def notify_downstream():
        import requests
        # Trigger refresh Tableau
        requests.post('https://tableau.company.com/api/refresh',
                     headers={'X-Tableau-Auth': '{{ var.value.tableau_token }}'})
        # Notifica il team su Slack
        requests.post('{{ var.value.slack_webhook }}',
                     json={'text': 'Pipeline giornaliera completata - dati aggiornati'})

    [wait_for_fivetran, wait_for_kafka_ingestion] >> source_freshness >> dbt_build >> notify_downstream()

dag = full_pipeline()

Alerting and Monitoring in Production

# utils/dbt_alerts.py
# Funzione di alerting riutilizzabile per i DAG Airflow

import requests
from airflow.models import Variable

def alert_slack_on_failure(context):
    """
    Invia notifica Slack quando un task dbt fallisce.
    Da usare come on_failure_callback nei DAG Airflow.
    """
    dag_id = context['dag'].dag_id
    task_id = context['task_instance'].task_id
    run_id = context['run_id']
    log_url = context['task_instance'].log_url

    message = {
        "blocks": [
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": f":red_circle: *dbt Task Fallito*\n"
                            f"*DAG:* `{dag_id}`\n"
                            f"*Task:* `{task_id}`\n"
                            f"*Run ID:* `{run_id}`\n"
                            f"*Log:* <{log_url}|Visualizza log>"
                }
            }
        ]
    }

    slack_webhook = Variable.get('SLACK_WEBHOOK_DATA_ALERTS')
    requests.post(slack_webhook, json=message)


# Utilizzo nel DAG:
# default_args = {
#     'on_failure_callback': alert_slack_on_failure
# }

Enterprise Governance Framework: All Together

The complete vision of the dbt enterprise architecture combines all the elements seen in this series:

# Struttura del progetto dbt enterprise completo
#
# my_dbt_project/
# ├── models/
# │   ├── staging/               # view, test generici, freshness check
# │   ├── intermediate/          # ephemeral, logica di trasformazione
# │   ├── marts/
# │   │   ├── finance/           # gruppo: finance, access: public/protected
# │   │   ├── marketing/         # gruppo: marketing, access: public
# │   │   └── platform/          # gruppo: platform (modelli shared)
# │   └── semantic_models/       # MetricFlow semantic models
# ├── metrics/                   # definizioni metriche
# ├── snapshots/                 # SCD-2 snapshots
# ├── macros/                    # macro riutilizzabili
# ├── tests/                     # test singolari e generici custom
# ├── analyses/                  # query ad-hoc versionata
# ├── seeds/                     # dati statici di riferimento
# ├── packages.yml               # dbt-utils, dbt-expectations
# ├── dbt_project.yml            # configurazione progetto + materializzazioni
# ├── profiles.yml               # connessioni warehouse (escluso da git con .gitignore per prod)
# ├── requirements.txt           # versioni pinned
# └── .github/workflows/
#     ├── dbt-ci.yml             # Slim CI su PR
#     └── dbt-prod.yml           # Deploy + artifact upload

# Airflow DAGs (repo separato o sottocartella):
# ├── dags/
# │   ├── dbt_daily_pipeline.py
# │   └── full_data_platform.py
# └── utils/
#     └── dbt_alerts.py

Maturity Checklist for dbt in Production

  • Governance: groups defined by team, access control on public/protected/private models, enforced contracts on critical models
  • Testing: generic tests on each primary key, singular tests for critical business rules, dbt-expectations for distribution and freshness
  • CI/CD: Slim CI on every PR with GitHub Actions, branch protection, automatic CI schema cleanup
  • Documentation: YAML descriptions on each template and column, doc blocks for reusable content, exposures for consumers
  • Monitoring: Slack alerting on failure, scheduled source freshness, health check on incremental models
  • Semantic Layer: core metrics defined once, BI tools connected via MetricFlow API
  • Orchestration: Airflow or Cosmos for cross-pipeline dependencies, sensors for upstream sources

Conclusions: End of the dbt Series

This series has covered the complete cycle of dbt: from the installation and setup of the first one project (module 01), the structure of models and sources (module 02), Jinja and macros (module 03), materializations (module 04), testing (module 05), documentation (module 06), Core vs Cloud comparison (module 07), CI/CD with Slim CI (module 08), advanced incrementals with microbatch and CDC (module 09), up to complete enterprise architecture with governance, Semantic Layer and Airflow.

dbt isn't just a SQL transformation tool - it's a framework that brings software engineering best practices - testing, versioning, documentation, CI/CD - in the world of data. Its strength is making these practices accessible to those who know SQL, without requiring deep software development skills.

The natural next step after this series is to delve deeper into dbt integration with the rest of the modern data stack: Apache Kafka for real-time ingestion, Snowflake/BigQuery for warehouse-specific optimizations, and BI tools like Metabase, Superset or Tableau for visualizing transformed data.

Try These Tools

Free developer tools related to this topic.

Commenti

Caricamento commenti...

Sign in to comment

Discuss this article in the community

Join the Telegram group to discuss with other developers, ask questions and share your experiences.

Did you like this article?

Explore other content on the blog or discover my projects