CI/CD for dbt
Automating your dbt workflows ensures code quality and streamlines deployments. This guide covers implementing CI/CD pipelines for dbt projects using popular tools.
Why CI/CD for dbt?
Benefits
Code quality:
- Automated testing catches issues early
- Linting enforces style standards
- Documentation checks ensure completeness
Team velocity:
- Faster feedback on changes
- Automated deployments reduce manual work
- Confidence in production releases
Risk reduction:
- Staging environments catch issues before production
- Rollback capabilities
- Audit trail of all changes
CI/CD pipeline overview
A complete dbt CI/CD pipeline includes:
Continuous Integration (CI)
On every pull request:
- Install dependencies (
dbt deps) - Compile models (
dbt compile) - Run tests (
dbt test) - Lint SQL (
sqlfluff lint) - Check documentation coverage
Continuous Deployment (CD)
On merge to main:
- Deploy to staging
- Run full test suite
- Generate documentation
- Deploy to production (optional/manual)
GitHub Actions setup
Basic CI workflow
Create .github/workflows/dbt-ci.yml:
name: dbt CI
on:
pull_request:
branches: [main, develop]
jobs:
dbt-ci:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dbt
run: |
pip install dbt-core dbt-snowflake # Adjust adapter as needed
dbt --version
- name: Configure dbt profiles
run: |
mkdir -p ~/.dbt
cat > ~/.dbt/profiles.yml << EOF
my_project:
target: ci
outputs:
ci:
type: snowflake
account: ${{ secrets.SNOWFLAKE_ACCOUNT }}
user: ${{ secrets.SNOWFLAKE_USER }}
password: ${{ secrets.SNOWFLAKE_PASSWORD }}
role: ${{ secrets.SNOWFLAKE_ROLE }}
database: ${{ secrets.SNOWFLAKE_DATABASE }}
warehouse: ${{ secrets.SNOWFLAKE_WAREHOUSE }}
schema: ${{ secrets.SNOWFLAKE_SCHEMA }}_CI_${{ github.run_id }}
threads: 4
EOF
- name: Install dbt packages
run: dbt deps
- name: Compile dbt
run: dbt compile
- name: Run dbt tests
run: dbt test
env:
DBT_PROFILES_DIR: ~/.dbt
Advanced CI with Slim Testing
Optimize CI by testing only modified models:
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v44
with:
files: |
models/**/*.sql
models/**/*.yml
- name: Run Slim CI
if: steps.changed-files.outputs.any_changed == 'true'
run: |
# Download manifest from main branch
dbt docs generate --target-path target-main
# Run only modified models and their children
dbt run --select state:modified+ --state target-main
dbt test --select state:modified+ --state target-main
CD workflow
Create .github/workflows/dbt-cd.yml:
name: dbt CD
on:
push:
branches: [main]
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment: staging
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Deploy to Staging
run: |
dbt deps
dbt run --target staging
dbt test --target staging
dbt docs generate --target staging
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production # Requires manual approval
steps:
- name: Deploy to Production
run: |
dbt deps
dbt run --target prod
dbt test --target prod
GitLab CI setup
Basic pipeline
Create .gitlab-ci.yml:
stages:
- test
- deploy
variables:
DBT_PROFILES_DIR: "${CI_PROJECT_DIR}/profiles"
.dbt_template: &dbt_definition
image: python:3.11-slim
before_script:
- pip install dbt-core dbt-snowflake
- dbt deps
dbt-test:
<<: *dbt_definition
stage: test
script:
- dbt compile
- dbt test
only:
- merge_requests
- main
dbt-deploy-staging:
<<: *dbt_definition
stage: deploy
script:
- dbt run --target staging
- dbt test --target staging
environment:
name: staging
only:
- main
dbt-deploy-production:
<<: *dbt_definition
stage: deploy
script:
- dbt run --target prod
- dbt test --target prod
environment:
name: production
when: manual
only:
- main
Testing strategies
Unit testing
Test individual models in isolation:
# Run specific model tests
dbt test --select stg_customers
Integration testing
Test model chains:
# Test a model and all its children
dbt test --select dim_customers+
# Test upstream dependencies too
dbt test --select +dim_customers
State-aware testing
Only test changed models:
# Compare against previous state
dbt test --select state:modified --state path/to/artifacts
Code quality checks
SQL linting with sqlfluff
Add to your CI pipeline:
- name: Install sqlfluff
run: pip install sqlfluff
- name: Lint SQL
run: sqlfluff lint models/
Configure .sqlfluff:
[sqlfluff]
dialect = snowflake
templater = jinja
rules = all
[sqlfluff:indentation]
tab_width = 4
[sqlfluff:templater:jinja]
apply_dbt_builtins = True
Documentation coverage
Ensure all models are documented:
- name: Check documentation
run: |
python scripts/check_docs.py
Create scripts/check_docs.py:
import yaml
import sys
from pathlib import Path
def check_documentation():
"""Check that all models have descriptions."""
errors = []
for yml_file in Path('models').rglob('*.yml'):
with open(yml_file) as f:
content = yaml.safe_load(f)
if 'models' in content:
for model in content['models']:
if not model.get('description'):
errors.append(f"Model {model['name']} missing description")
if errors:
print("Documentation errors:")
for error in errors:
print(f" - {error}")
sys.exit(1)
else:
print("All models documented ✓")
if __name__ == '__main__':
check_documentation()
Environment management
Environment-specific configurations
# profiles.yml
my_project:
target: dev
outputs:
dev:
type: snowflake
schema: dbt_dev
warehouse: DEV_WH
ci:
type: snowflake
schema: dbt_ci_{{ env_var('GITHUB_RUN_ID') }}
warehouse: CI_WH
threads: 4
staging:
type: snowflake
schema: dbt_staging
warehouse: STAGING_WH
prod:
type: snowflake
schema: dbt_prod
warehouse: PROD_WH
Dynamic schema naming
Create macros/generate_schema_name.sql:
{% macro generate_schema_name(custom_schema_name, node) -%}
{%- set default_schema = target.schema -%}
{%- if target.name == 'ci' -%}
{{ default_schema }}
{%- elif custom_schema_name is none -%}
{{ default_schema }}
{%- else -%}
{{ custom_schema_name | trim }}
{%- endif -%}
{%- endmacro %}
Deployment patterns
Blue-Green deployment
Deploy to new schema, then swap:
- name: Blue-Green Deploy
run: |
# Deploy to blue schema
dbt run --target prod --vars '{schema_suffix: _blue}'
# Run tests
dbt test --target prod --vars '{schema_suffix: _blue}'
# Swap schemas (using dbt hooks or external script)
python scripts/swap_schemas.py
Incremental deployment
Only run changed models in production:
- name: Incremental Deploy
run: |
# Download production manifest
aws s3 cp s3://dbt-artifacts/manifest.json ./prod-manifest/
# Run only changed models
dbt run --select state:modified+ --state ./prod-manifest/
Notifications and monitoring
Slack notifications
Add to GitHub Actions:
- name: Notify Slack on Failure
if: failure()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
channel: '#data-alerts'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
Metrics tracking
Track deployment metrics:
- name: Track Metrics
run: |
echo "::set-output name=run_duration::$(cat logs/dbt.log | grep 'Completed successfully' | awk '{print $5}')"
echo "::set-output name=models_run::$(dbt ls --select state:modified | wc -l)"
Best practices
1. Fast feedback
Keep CI under 10 minutes:
- Use slim CI (only changed models)
- Parallelize test execution
- Cache dependencies
2. Staging parity
Ensure staging matches production:
- Same warehouse configuration
- Representative data volumes
- Same dbt version
3. Immutable deployments
Never modify production directly:
- All changes through PRs
- Code review required
- Automated testing gates
4. Rollback capability
Plan for failure:
- Keep previous manifests
- Document rollback procedures
- Test rollback process
FAQ
What tests should run in CI?
Run dbt deps, dbt compile, and dbt test on every PR. For larger projects, also run sqlfluff linting and check model documentation coverage.
How do I handle different environments in CI/CD?
Use separate targets in profiles.yml (ci, staging, prod). Set environment variables in your CI/CD platform to control which target is used.
Should I run full refreshes in CI?
Generally no - run slim CI with dbt test only. Full refreshes are expensive and should run in scheduled production jobs, not on every PR.
How do I prevent breaking changes?
Use state comparison (dbt ls --state) to detect model changes, require code reviews, run full test suites, and use staging environments before production.