Salta al contenuto principale

10 - dbt in Produzione: Data Governance, Semantic Layer e Orchestrazione con Airflow

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

Cosa ne pensi di questo articolo?

In Breve

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 Produzione: Data Governance, Semantic Layer e Orchestrazione con Airflow

Architettura dbt enterprise: accesso granulare a modelli con policy di governance, dbt Semantic Layer (MetricFlow) per metriche consistenti che qualunque tool BI può interrogare, integrazione con Apache Airflow per orchestrazione dipendenze cross-pipeline.

dbt produzione dbt semantic layer airflow dbt data governance dbt MetricFlow

dbt in Produzione: Cosa Cambia Rispetto allo Sviluppo

Passare dbt da un progetto di sviluppo a un sistema in produzione usato da decine di analisti e consumer BI richiede tre componenti aggiuntivi che in sviluppo non sono necessari: una strategia di governance per controllare chi vede e modifica cosa, un Semantic Layer per garantire che tutti usino le stesse definizioni di metriche, e un orchestratore robusto per gestire le dipendenze tra dbt e il resto della data platform.

Questo modulo finale della serie affronta ciascuno dei tre temi con approccio pratico: configurazioni YAML reali, codice Airflow funzionante, e pattern MetricFlow pronti all'uso.

Data Governance: Controllo Accessi e Policy

Gruppi e Accesso ai Modelli

dbt 1.5 ha introdotto i gruppi e il sistema di access control sui modelli: ogni modello può avere un livello di accesso che determina chi può referenziarlo con 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

Versioning dei Modelli: Contratti Stabili con Consumer

# 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: Garantire lo Schema dei Modelli Pubblici

# 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: Metriche Consistenti per Tutti i Tool BI

Il Semantic Layer di dbt (basato su MetricFlow) risolve uno dei problemi più frustranti nei data team: metriche calcolate in modo diverso da analisti diversi. “Revenue” in Tableau calcola il lordo, in PowerBI calcola il netto, nel notebook Python include i rimborsi. Chi ha ragione?

Con il Semantic Layer, definisci le metriche una volta in YAML. Ogni tool BI che si connette al Semantic Layer riceve la stessa definizione, la stessa logica, gli stessi numeri.

Semantic Models: Definire Entità e Misure

# 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

Metriche: KPI da 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

Interrogare il 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()

Orchestrazione con Apache Airflow

Airflow è l'orchestratore più usato in ambienti enterprise. L'integrazione con dbt usa il provider apache-airflow-providers-dbt-cloud (per dbt Cloud) oppure il Cosmos package (per dbt Core, che traduce il DAG dbt in task Airflow).

Pattern 1: dbt con BashOperator (Semplice)

# 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 è la libreria open-source di Astronomer che traduce ogni modello dbt in un task Airflow separato, rendendo il DAG di Airflow uno specchio perfetto del DAG dbt. Ogni modello appare come task, con dipendenze visualizzate correttamente nell'UI di Airflow.

# 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: Pipeline Cross-Sistema con 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 e Monitoraggio in Produzione

# 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
# }

Schema di Governance Enterprise: Tutto Insieme

La visione completa dell'architettura dbt enterprise combina tutti gli elementi visti in questa serie:

# 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

Checklist di Maturità per dbt in Produzione

  • Governance: gruppi definiti per team, access control su modelli pubblici/protetti/privati, contratti enforced sui modelli critici
  • Testing: test generici su ogni primary key, test singolari per regole di business critiche, dbt-expectations per distribuzione e freshness
  • CI/CD: Slim CI su ogni PR con GitHub Actions, branch protection, cleanup schema CI automatico
  • Documentazione: descrizioni YAML su ogni modello e colonna, doc block per contenuti riutilizzabili, exposures per i consumer
  • Monitoring: alerting Slack su failure, source freshness schedulato, health check sui modelli incrementali
  • Semantic Layer: metriche core definite una volta, tool BI connessi tramite API MetricFlow
  • Orchestrazione: Airflow o Cosmos per le dipendenze cross-pipeline, sensors per le sorgenti upstream

Conclusioni: Fine della Serie dbt

Questa serie ha percorso il ciclo completo di dbt: dall'installazione e setup del primo progetto (modulo 01), alla struttura dei modelli e delle sorgenti (modulo 02), Jinja e macro (modulo 03), materializzazioni (modulo 04), testing (modulo 05), documentazione (modulo 06), confronto Core vs Cloud (modulo 07), CI/CD con Slim CI (modulo 08), incrementali avanzati con microbatch e CDC (modulo 09), fino all'architettura enterprise completa con governance, Semantic Layer e Airflow.

dbt non è solo uno strumento di trasformazione SQL: è un framework che porta le best practice del software engineering - testing, versionamento, documentazione, CI/CD - nel mondo dei dati. La sua forza è rendere queste pratiche accessibili a chi conosce SQL, senza richiedere competenze di sviluppo software profonde.

Il passo successivo naturale dopo questa serie è approfondire l'integrazione di dbt con il resto della modern data stack: Apache Kafka per l'ingestion in tempo reale, Snowflake/BigQuery per ottimizzazioni warehouse-specifiche, e tool BI come Metabase, Superset o Tableau per la visualizzazione dei dati trasformati.

Prova Questi Strumenti

Strumenti gratuiti correlati a questo argomento.

Commenti

Caricamento commenti...

Accedi per commentare

Discuti questo articolo nella community

Unisciti al gruppo Telegram per discutere con altri sviluppatori, fare domande e condividere le tue esperienze.

Ti è piaciuto questo articolo?

Esplora altri contenuti sul blog o scopri i miei progetti