CI/CD for dbt: GitHub Actions, Slim CI with State and Automatic Testing on Pull Request
How to automate dbt testing and deployment with GitHub Actions: Slim CI with --state and --defer to test only modified models (saving up to 90% of CI time), separate schema for test environments and main branch protection.
Why CI/CD is Fundamental to dbt Projects
A dbt design without CI is a design that breaks in production silently. Without automatic PR testing, every merge is a leap of faith: Does the model compile? Do the tests pass? Are the downstream models still consistent? CI solves this problem automatically running checks on every pull request, before the code reaches main.
The problem with naïve CI is the cost: if you execute dbt build about everything
the project at each PR, on a warehouse with 200+ models you can spend 20-30 minutes of computing
for every single change. Slim CI solves this: just run the templates
modified and their downstream, reducing execution to 1-10% of the total cost.
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...
Sign in to comment