Salta al contenuto principale

08 - CI/CD per dbt: GitHub Actions, Slim CI con State e Testing Automatico su Pull Request

dbt 16 min CI/CD per dbt: GitHub Actions, Slim CI con State e Testing Automatico su Pull Request Come automatizzare testing e deployment dbt con GitHub...

Cosa ne pensi di questo articolo?

In Breve

dbt 16 min CI/CD per dbt: GitHub Actions, Slim CI con State e Testing Automatico su Pull Request Come automatizzare testing e deployment dbt con GitHub...

dbt 16 min

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.

dbt CI/CD dbt github actions slim CI dbt dbt state defer dbt

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).

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