CI/CD per dbt: GitHub Actions, Slim CI con State e Testing Automatico su Pull Request
Come automatizzare testing e deployment dbt con GitHub Actions: Slim CI con --state e --defer per testare solo i modelli modificati (risparmiando fino al 90% del tempo CI), schema separati per ambienti di test e protezione del branch principale.
Perché il CI/CD è Fondamentale per i Progetti dbt
Un progetto dbt senza CI è un progetto che si rompe in produzione in modo silenzioso. Senza testing automatico sulle PR, ogni merge è un atto di fede: il modello compila? I test passano? I modelli downstream sono ancora coerenti? Il CI risolve questo problema eseguendo automaticamente i check ad ogni pull request, prima che il codice raggiunga main.
Il problema del CI naïve è il costo: se esegui dbt build su tutto
il progetto ad ogni PR, su un warehouse con 200+ modelli puoi spendere 20-30 minuti di compute
per ogni singola modifica. Slim CI risolve questo: esegui solo i modelli
modificati e i loro downstream, riducendo l'esecuzione all'1-10% del costo totale.
Concetti Fondamentali: State, Defer e Manifest
Prima di costruire il workflow, è necessario capire tre concetti che rendono possibile lo Slim CI:
manifest.json: Il DAG Serializzato
Ad ogni dbt run o dbt compile, dbt genera un file
target/manifest.json. Questo file contiene la rappresentazione completa
dello stato del progetto: tutti i modelli, le loro dipendenze, gli hash del codice SQL
compilato. È la base per lo Slim CI.
-- Genera il manifest senza eseguire modelli
dbt compile
-- Il manifest viene generato anche da:
dbt run
dbt build
dbt docs generate
-- Struttura semplificata del manifest.json:
-- {
-- "metadata": { "dbt_version": "1.8.x", "generated_at": "..." },
-- "nodes": {
-- "model.my_project.fct_orders": {
-- "unique_id": "model.my_project.fct_orders",
-- "depends_on": { "nodes": ["model.my_project.stg_orders", "..."] },
-- "compiled_code": "SELECT ...",
-- "checksum": { "name": "sha256", "checksum": "abc123..." }
-- }
-- },
-- "sources": { ... },
-- "exposures": { ... }
-- }
--state: Confrontare con il Manifest di Produzione
-- --state indica a dbt dove trovare il manifest di riferimento (produzione)
-- dbt confronta l'hash del codice corrente con quello nel manifest di produzione
-- Seleziona solo i modelli il cui SQL è cambiato rispetto a produzione:
dbt build --select state:modified
-- Seleziona i modelli modificati E tutti i loro downstream (il più comune in CI):
dbt build --select state:modified+
-- Modelli modificati + 2 livelli downstream:
dbt build --select state:modified+2
-- Tipi di modifiche rilevate da state:modified:
-- * SQL del modello cambiato
-- * Config del modello cambiata (materializzazione, tags, ecc.)
-- * Schema.yml del modello cambiato (descrizioni, test)
-- * Macro utilizzata dal modello cambiata
--defer: Usare i Modelli di Produzione per gli Upstream
-- Senza --defer: per testare fct_orders modificata, dbt dovrebbe prima ricreare
-- stg_orders, stg_customers, int_orders, ecc. nel schema CI. Costoso.
-- Con --defer: dbt usa le tabelle di PRODUZIONE per gli upstream non modificati,
-- e crea solo i modelli che hai effettivamente cambiato nello schema CI.
dbt build \
--select state:modified+ \
--defer \
--state ./prod-artifacts \
--target ci
-- Con --defer:
-- * stg_orders non modificata → dbt usa analytics_prod.staging.stg_orders
-- * fct_orders modificata → dbt ricrea solo fct_orders nello schema ci_pr123
-- Risparmio: 95%+ del compute nella maggior parte dei PR
Setup Completo: GitHub Actions per dbt CI
Step 1: Configurare il profiles.yml per l'Ambiente CI
# profiles.yml
my_project:
target: dev
outputs:
dev:
type: snowflake
account: "{{ env_var('SNOWFLAKE_ACCOUNT') }}"
user: "{{ env_var('SNOWFLAKE_USER') }}"
password: "{{ env_var('SNOWFLAKE_PASSWORD') }}"
database: analytics
warehouse: compute_wh_xs
schema: "dbt_dev_{{ env_var('DBT_DEVELOPER', 'unknown') }}"
threads: 4
# Schema CI unico per ogni PR (evita conflitti tra PR parallele)
ci:
type: snowflake
account: "{{ env_var('SNOWFLAKE_ACCOUNT') }}"
user: "{{ env_var('SNOWFLAKE_CI_USER') }}"
password: "{{ env_var('SNOWFLAKE_CI_PASSWORD') }}"
database: analytics_ci
warehouse: compute_wh_xs
schema: "dbt_ci_pr{{ env_var('PR_NUMBER', 'unknown') }}"
threads: 8
prod:
type: snowflake
account: "{{ env_var('SNOWFLAKE_ACCOUNT') }}"
user: "{{ env_var('SNOWFLAKE_PROD_USER') }}"
password: "{{ env_var('SNOWFLAKE_PROD_PASSWORD') }}"
database: analytics
warehouse: compute_wh_m
schema: prod
threads: 16
Step 2: Salvare il Manifest di Produzione su S3
# .github/workflows/dbt-prod.yml
# Workflow di produzione: salva il manifest dopo ogni deploy su main
name: dbt Production Deploy
on:
push:
branches: [main]
jobs:
dbt_prod:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
- name: Install dbt
run: pip install -r requirements.txt
- name: dbt deps
run: dbt deps
env:
DBT_PROFILES_DIR: .
- name: dbt build (produzione completa)
run: dbt build --target prod
env:
DBT_PROFILES_DIR: .
SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}
SNOWFLAKE_PROD_USER: ${{ secrets.SNOWFLAKE_PROD_USER }}
SNOWFLAKE_PROD_PASSWORD: ${{ secrets.SNOWFLAKE_PROD_PASSWORD }}
# Salva il manifest in S3 per lo Slim CI delle PR successive
- name: Upload manifest to S3
run: |
aws s3 cp target/manifest.json \
s3://my-dbt-artifacts/prod/manifest.json
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: eu-west-1
Step 3: Il Workflow Slim CI per Pull Request
# .github/workflows/dbt-ci.yml
# Workflow CI: eseguito su ogni PR verso main
name: dbt Slim CI
on:
pull_request:
branches: [main]
paths:
- 'models/**'
- 'macros/**'
- 'tests/**'
- 'snapshots/**'
- 'seeds/**'
- 'packages.yml'
- 'dbt_project.yml'
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
jobs:
slim_ci:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout PR branch
uses: actions/checkout@v4
- name: Setup Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Cache pip packages
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
- name: Install dbt
run: pip install -r requirements.txt
- name: dbt deps
run: dbt deps
env:
DBT_PROFILES_DIR: .
# Scarica il manifest di produzione per il confronto state
- name: Download production manifest from S3
run: |
mkdir -p prod-artifacts
aws s3 cp s3://my-dbt-artifacts/prod/manifest.json \
prod-artifacts/manifest.json
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_DEFAULT_REGION: eu-west-1
# Slim CI: build + test solo sui modelli modificati e downstream
- name: dbt Slim CI Build
id: dbt_build
run: |
dbt build \
--select state:modified+ \
--defer \
--state prod-artifacts \
--target ci \
--no-partial-parse
env:
DBT_PROFILES_DIR: .
PR_NUMBER: ${{ github.event.pull_request.number }}
SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}
SNOWFLAKE_CI_USER: ${{ secrets.SNOWFLAKE_CI_USER }}
SNOWFLAKE_CI_PASSWORD: ${{ secrets.SNOWFLAKE_CI_PASSWORD }}
# Commento sul PR con il risultato
- name: Comment PR with results
uses: actions/github-script@v7
if: always()
with:
script: |
const status = '${{ steps.dbt_build.outcome }}' === 'success'
? '✅ dbt Slim CI passed'
: '❌ dbt Slim CI failed';
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `${status}\n\nTarget schema: \`dbt_ci_pr${{ github.event.pull_request.number }}\``
});
# Cleanup: elimina lo schema CI dopo il test
- name: Cleanup CI schema
if: always()
run: |
dbt run-operation drop_schema \
--args '{"schema_name": "dbt_ci_pr${{ github.event.pull_request.number }}"}'
--target ci
env:
DBT_PROFILES_DIR: .
SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}
SNOWFLAKE_CI_USER: ${{ secrets.SNOWFLAKE_CI_USER }}
SNOWFLAKE_CI_PASSWORD: ${{ secrets.SNOWFLAKE_CI_PASSWORD }}
Macro per il Cleanup dello Schema CI
-- macros/admin/drop_schema.sql
-- Usata dal workflow CI per pulizia post-test
{% macro drop_schema(schema_name) %}
{% set drop_query %}
DROP SCHEMA IF EXISTS {{ target.database }}.{{ schema_name }} CASCADE
{% endset %}
{% do run_query(drop_query) %}
{{ log("Dropped schema: " ~ schema_name, info=True) }}
{% endmacro %}
Protezione del Branch Main
Il workflow CI ha valore solo se il branch main è protetto e il merge è bloccato finché il CI non passa. Configurazione in GitHub:
# Configurazione Branch Protection (Settings > Branches > main)
# Da fare una volta sola via GitHub UI o API:
# Required status checks:
# - dbt Slim CI / slim_ci
# Require pull request reviews: 1 reviewer
# Dismiss stale reviews: true
# Require status checks to pass before merging: true
# Require branches to be up to date: true
# Include administrators: true (raccomandato)
# Equivalente via GitHub API:
# gh api repos/OWNER/REPO/branches/main/protection \
# --method PUT \
# --field required_status_checks='{"strict":true,"contexts":["dbt Slim CI / slim_ci"]}' \
# --field required_pull_request_reviews='{"required_approving_review_count":1}'
Pattern Avanzati: Test Selettivi e Notifiche
Eseguire Solo i Test Critici in CI (Ottimizzazione Costo)
# In schema.yml: tagga i test critici
models:
- name: fct_orders
columns:
- name: order_id
tests:
- unique:
config:
tags: ['critical', 'ci'] # questi test girano sempre in CI
- not_null:
config:
tags: ['critical', 'ci']
- name: customer_id
tests:
- relationships:
to: ref('dim_customers')
field: customer_id
config:
tags: ['ci']
- dbt_expectations.expect_column_mean_to_be_between:
min_value: 1
max_value: 999999
config:
tags: ['slow', 'full-refresh-only'] # NON in CI normale
# Nel workflow CI, esegui solo i test taggati 'ci':
- name: dbt Slim CI Build (solo test critici)
run: |
dbt build \
--select state:modified+ \
--defer \
--state prod-artifacts \
--target ci \
--exclude tag:slow tag:full-refresh-only
Workflow Nightly: Full Build + Test Lenti
# .github/workflows/dbt-nightly.yml
# Ogni notte esegue il full build con TUTTI i test (inclusi quelli lenti)
name: dbt Nightly Full Build
on:
schedule:
- cron: '0 2 * * *' # 02:00 UTC ogni notte
jobs:
nightly:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install dbt
run: pip install -r requirements.txt
- name: dbt full build + all tests
run: dbt build --target prod --full-refresh --select tag:daily
# Include i test lenti esclusi dal CI
- name: Slack notification on failure
if: failure()
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_DATA_ALERTS }}
SLACK_MESSAGE: 'dbt Nightly Build FAILED - check GitHub Actions'
SLACK_COLOR: danger
Gestione degli Ambienti: Dev, Staging, Prod
# Pattern consigliato per la gestione multi-ambiente
# dbt_project.yml
# Ogni ambiente usa uno schema diverso grazie a generate_schema_name macro
dispatch:
- macro_namespace: dbt
search_order: ['my_project', 'dbt']
-- macros/get_custom_schema.sql
-- Override del comportamento default per gestire ambienti multipli
{% macro generate_schema_name(custom_schema_name, node) -%}
{%- set default_schema = target.schema -%}
{%- if target.name == 'prod' -%}
-- In produzione: usa il custom_schema se specificato, altrimenti default
{%- if custom_schema_name is none -%}
{{ default_schema }}
{%- else -%}
{{ custom_schema_name | trim }}
{%- endif -%}
{%- elif target.name == 'ci' -%}
-- In CI: prefissa tutto con il numero PR per isolamento
{%- if custom_schema_name is none -%}
{{ default_schema }}
{%- else -%}
{{ default_schema }}_{{ custom_schema_name | trim }}
{%- endif -%}
{%- else -%}
-- In dev: prefissa con il nome del developer
{%- if custom_schema_name is none -%}
{{ default_schema }}
{%- else -%}
{{ default_schema }}_{{ custom_schema_name | trim }}
{%- endif -%}
{%- endif -%}
{%- endmacro %}
requirements.txt per il Progetto dbt
dbt-core==1.8.7
dbt-snowflake==1.8.3 # adatta al tuo warehouse
dbt-utils==1.3.0
dbt-expectations==0.10.4
Pinna sempre le versioni esatte in requirements.txt. Una versione major
di dbt Core può introdurre breaking change nel comportamento degli adapter.
Aggiorna consapevolmente, non automaticamente.
Monitorare il Costo del CI: Metriche Importanti
Uno degli obiettivi del Slim CI è il risparmio economico. È utile tracciare le metriche del CI nel tempo per quantificare il valore e identificare modelli costosi da ottimizzare:
# .github/workflows/dbt-ci.yml - aggiunta metriche di costo
# Aggiungi questo step al workflow CI per loggare le metriche
- name: Log CI metrics
if: always()
run: |
echo "PR_NUMBER=${{ github.event.pull_request.number }}"
echo "MODELS_TESTED=$(cat target/run_results.json | python3 -c "
import json, sys
results = json.load(sys.stdin)
print(len([r for r in results['results'] if r['unique_id'].startswith('model.')]))")"
echo "TESTS_RUN=$(cat target/run_results.json | python3 -c "
import json, sys
results = json.load(sys.stdin)
print(len([r for r in results['results'] if r['unique_id'].startswith('test.')]))")"
echo "ELAPSED_SECONDS=$(cat target/run_results.json | python3 -c "
import json, sys
results = json.load(sys.stdin)
print(results['elapsed_time'])")"
-- Query per analizzare il risparmio Slim CI nel tempo
-- (richiede che tu loggi le metriche da qualche parte, es. BigQuery o Snowflake)
SELECT
pr_number,
run_date,
models_in_pr,
total_project_models,
ROUND(100.0 * models_in_pr / total_project_models, 1) AS pct_models_tested,
elapsed_seconds,
estimated_full_run_seconds,
ROUND(100.0 * (1 - elapsed_seconds::float / estimated_full_run_seconds), 1) AS time_saved_pct
FROM ci_metrics
ORDER BY run_date DESC
LIMIT 30
-- Risparmio tipico Slim CI su progetto reale (200 modelli, 5 modificati per PR):
-- Full run: 25 minuti | Slim CI: 2-3 minuti → 88-92% di risparmio tempo/costo
Conclusioni e Prossimi Passi
Slim CI è una delle ottimizzazioni più impattanti che puoi fare su un progetto dbt maturo. Riduce il costo del CI da “devo aspettare 30 minuti per ogni PR” a “il check è pronto in 2-3 minuti”, rendendo il workflow molto più fluido per il team.
Gli ingredienti chiave: manifest.json di produzione salvato in S3, --state
per il confronto, --defer per evitare di ricreare modelli upstream,
schema CI isolati per PR, e branch protection su main.
I prossimi due moduli approfondiscono temi avanzati: il modulo 09 esplora le strategie incrementali avanzate (microbatch, CDC, gestione late-arriving data), e il modulo 10 chiude la serie con dbt in produzione a livello enterprise (governance, Semantic Layer, orchestrazione Airflow).

Commenti
Caricamento commenti...
Accedi per commentare