J'ai eu des plateformes Data où chaque déploiement DBT se faisait à la main : connexion SSH au serveur, git pull, dbt run --target prod, prière pour que ça passe. Un vendredi soir, un développeur fait un dbt run sans --target et écrase les tables de prod avec les données de dev. Le lundi matin, 4 dashboards PowerBI vides. Voici comment j'ai mis en place une pipeline CI/CD GitLab qui déploie DBT + Terraform sur Snowflake sans intervention manuelle — et sans risque d'écraser la prod.

Le problème : pourquoi le déploiement manuel est un cauchemar
Le pattern que tout le monde fait au début :
# ⛔ Le déploiement manuel
ssh data-server
cd /opt/dbt-project
git pull origin main
dbt run --target prod
dbt test --target prod
Ça marche pour 5 modèles. À 380, c'est l'enfer :
- Pas de revue de code avant déploiement
- Pas de tests automatisés avant la prod
- Un
dbt runsans--targetécrase la prod avec les données de dev - Pas de rollback si quelque chose casse
- Pas de trace de qui a déployé quoi et quand
L'architecture cible
Trois pipelines GitLab CI qui correspondent à trois étapes du cycle de vie :
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ MR / PR │ │ Merge to │ │ Terraform │
│ (Slim CI) │ │ main │ │ (IaC) │
│ │ │ (Deploy) │ │ │
│ dbt build │ │ dbt deploy │ │ terraform │
│ state:modif. │ │ to prod │ │ plan & apply │
│ + defer │ │ │ │ │
└──────────────┘ └──────────────┘ └──────────────┘
- Pipeline CI (sur Merge Request) : teste seulement les modèles modifiés avec Slim CI
- Pipeline CD (sur merge to main) : déploie en production
- Pipeline Terraform (sur changement IaC) : provisionne l'infrastructure Snowflake
Pipeline 1 — Slim CI : tester que ce qui a changé
Le concept du Slim CI : comparer le manifest DBT courant avec celui de la prod, et ne builder que les modèles modifiés.
Le fichier .gitlab-ci.yml
# .gitlab-ci.yml — étape CI
stages:
- ci
variables:
DBT_TARGET: ci
DBT_SNOWFLAKE_ACCOUNT: $SNOWFLAKE_ACCOUNT
DBT_SNOWFLAKE_USER: $SNOWFLAKE_CI_USER
DBT_SNOWFLAKE_PASSWORD: $SNOWFLAKE_CI_PASSWORD
DBT_SNOWFLAKE_WAREHOUSE: $SNOWFLAKE_CI_WH
DBT_SNOWFLAKE_DATABASE: ANALYTICS_CI
DBT_SNOWFLAKE_SCHEMA: ci_${CI_COMMIT_SHORT_SHA}
dbt_slim_ci:
stage: ci
image: python:3.11-slim
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
before_script:
- pip install dbt-snowflake
- |
cat << EOF > profiles.yml
default:
target: ci
outputs:
ci:
type: snowflake
account: "{{ env_var('DBT_SNOWFLAKE_ACCOUNT') }}"
user: "{{ env_var('DBT_SNOWFLAKE_USER') }}"
password: "{{ env_var('DBT_SNOWFLAKE_PASSWORD') }}"
warehouse: "{{ env_var('DBT_SNOWFLAKE_WAREHOUSE') }}"
database: "{{ env_var('DBT_SNOWFLAKE_DATABASE') }}"
schema: "{{ env_var('DBT_SNOWFLAKE_SCHEMA') }}"
EOF
script:
# 1. Télécharger le manifest de prod (artefact ou S3)
- mkdir -p state
- curl -sL -o state/manifest.json ${S3_MANIFEST_URL}
# 2. Installer les dépendances DBT
- dbt deps --profiles-dir .
# 3. Slim CI : builder seulement les modèles modifiés + leurs dépendances
- dbt build
--select state:modified+
--defer
--state state/
--profiles-dir .
--target ci
# 4. Tests sur les modèles modifiés
- dbt test
--select state:modified
--defer
--state state/
--profiles-dir .
--target ci
Comment ça marche
state:modified+: compare le manifest actuel avec celui de prod. Ne sélectionne que les modèles dont le code a changé et leurs dépendances.--defer: les modèles non modifiés sont résolus vers la prod existante (pas besoin de les reconstruire).ci_${CI_COMMIT_SHORT_SHA}: chaque MR gets son propre schéma Snowflake. Pas de collision entre développeurs.
Le gain
| Métrique | Avant (dbt run complet) | Après (Slim CI) |
|---|---|---|
| Modèles buildés | 380 | 5-15 (seulement les modifiés) |
| Temps CI | 47 min | 3-5 min |
| Crédits Snowflake par MR | 14 | 0,5 |
| Tests exécutés | Tous (380) | Seulement les modifiés |
Pipeline 2 — Déploiement CD : du merge à la prod
Quand la MR est mergée sur main, le déploiement en prod est automatique.
# .gitlab-ci.yml — étape CD
stages:
- ci
- deploy
dbt_deploy_prod:
stage: deploy
image: python:3.11-slim
rules:
- if: $CI_COMMIT_BRANCH == "main"
before_script:
- pip install dbt-snowflake
- |
cat << EOF > profiles.yml
default:
target: prod
outputs:
prod:
type: snowflake
account: "{{ env_var('SNOWFLAKE_ACCOUNT') }}"
user: "{{ env_var('SNOWFLAKE_PROD_USER') }}"
password: "{{ env_var('SNOWFLAKE_PROD_PASSWORD') }}"
warehouse: COMPUTE_PROD_WH
database: ANALYTICS_PROD
schema: public
private_key_path: "{{ env_var('SNOWFLAKE_KEY_PATH') }}"
EOF
script:
- dbt deps --profiles-dir .
- dbt build --profiles-dir . --target prod
- dbt test --profiles-dir . --target prod
# Sauvegarder le manifest pour le prochain Slim CI
- aws s3 cp target/manifest.json ${S3_MANIFEST_URL}
environment:
name: production
url: https://snowflake.example.com
only:
- main
Points clés :
- Le déploiement utilise un service account (pas un utilisateur humain) avec authentification par clé privée (key pair), pas par mot de passe.
- Le manifest de prod est sauvegardé sur S3 après chaque déploiement — c'est ce qui permet au Slim CI de comparer.
environment: productionactive la protection GitLab (approval manuelle possible).
Pipeline 3 — Terraform : provisionner Snowflake as Code
L'infrastructure Snowflake (warehouses, roles, databases, grants) est gérée par Terraform dans un dépôt séparé. La pipeline applique les changements après revue.
Structure du dépôt Terraform
terraform-snowflake/
├── modules/
│ ├── warehouse/
│ ├── role/
│ └── database/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ └── terraform.tfvars
│ ├── recette/
│ │ ├── main.tf
│ │ └── terraform.tfvars
│ └── prod/
│ ├── main.tf
│ └── terraform.tfvars
└── .gitlab-ci.yml
Le .gitlab-ci.yml Terraform
stages:
- validate
- plan
- apply
variables:
TF_VERSION: "1.9.0"
.terraform:setup: &terraform_setup
image:
name: hashicorp/terraform:${TF_VERSION}
entrypoint: [""]
before_script:
- export SNOWFLAKE_ACCOUNT=$SNOWFLAKE_ACCOUNT
- export SNOWFLAKE_USER=$SNOWFLAKE_TF_USER
- export SNOWFLAKE_PRIVATE_KEY=$SNOWFLAKE_TF_KEY
- cd environments/${ENV}
terraform_validate:
<<: *terraform_setup
stage: validate
script:
- terraform init -backend=false
- terraform validate
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
terraform_plan:
<<: *terraform_setup
stage: plan
script:
- terraform init
- terraform plan -out=tfplan
artifacts:
paths:
- environments/${ENV}/tfplan
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
terraform_apply:
<<: *terraform_setup
stage: apply
script:
- terraform init
- terraform apply -auto-approve tfplan
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual # approval manuel pour prod
environment:
name: ${ENV}
Un module warehouse typique
# modules/warehouse/main.tf
variable "name" { type = string }
variable "size" { type = string }
variable "auto_suspend" { type = number }
variable "environment" { type = string }
resource "snowflake_warehouse" "wh" {
name = "${var.name}_${upper(var.environment)}"
warehouse_size = var.size
auto_suspend = var.auto_suspend
initially_suspended = true
}
# Grants automatiques
resource "snowflake_grant_privileges_to_warehouse" "usage" {
warehouse_name = snowflake_warehouse.wh.name
privileges = ["USAGE"]
roles = ["FR_DATA_ENGINEER_${upper(var.environment)}"]
}
# environments/prod/main.tf
module "compute_wh" {
source = "../../modules/warehouse"
name = "COMPUTE"
size = "X-LARGE"
auto_suspend = 60
environment = "prod"
}
module "transform_wh" {
source = "../../modules/warehouse"
name = "TRANSFORM"
size = "LARGE"
auto_suspend = 60
environment = "prod"
}
Les variables GitLab CI à configurer
Dans Settings → CI/CD → Variables de ton projet GitLab :
| Variable | Description | Protégée | Masquée |
|---|---|---|---|
SNOWFLAKE_ACCOUNT |
Identifiant du compte Snowflake | ✅ | — |
SNOWFLAKE_CI_USER |
Service account CI (lecture seule sur prod) | ✅ | — |
SNOWFLAKE_CI_PASSWORD |
Mot de passe du CI user | ✅ | ✅ |
SNOWFLAKE_PROD_USER |
Service account prod (key pair) | ✅ | — |
SNOWFLAKE_PROD_PASSWORD |
Mot de passe prod (si pas de key pair) | ✅ | ✅ |
SNOWFLAKE_TF_USER |
Service account Terraform | ✅ | — |
SNOWFLAKE_TF_KEY |
Clé privée Terraform | ✅ | ✅ |
S3_MANIFEST_URL |
URL S3 du manifest de prod | ✅ | — |
Les 5 pièges que j'ai vu en production
Piège 1 — Utiliser un utilisateur humain pour le CI/CD
Un développeur part en vacances. Son token Snowflake expire. Le pipeline plante. Créez des service accounts dédiés avec AUTHENTICATOR = 'SNOWFLAKE' et des mots de passe ou clés qui n'expirent pas.
Piège 2 — Pas de schéma CI isolé par MR
Sans schéma isolé, deux MR en parallèle s'écrasent mutuellement. Le pattern ci_${CI_COMMIT_SHORT_SHA} garantit l'isolation. Nettoyez les schémas CI après merge avec une tâche de cleanup :
cleanup_ci_schema:
stage: cleanup
script:
- snowsql -q "DROP SCHEMA IF EXISTS ci_${CI_COMMIT_SHORT_SHA}"
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: on_success
Piège 3 — Manifest de prod non sauvegardé
Si vous ne sauvegardez pas le manifest.json après chaque déploiement prod, le Slim CI ne peut pas comparer. Le dbt build se fait sur tous les modèles. Utilisez S3, GCS, ou les artefacts GitLab pour stocker le manifest.
Piège 4 — Terraform sans state backend distant
Sans backend distant, le state Terraform est local. Deux développeurs qui appliquent en parallèle écrasent le state. Utilisez le backend HTTP de GitLab ou S3 avec locking :
terraform {
backend "http" {
address = "https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/terraform/state/${ENV}"
lock_address = "https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/terraform/state/${ENV}/lock"
unlock_address = "https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/terraform/state/${ENV}/lock"
}
}
Piège 5 — Pas de terraform plan en artefact
Le terraform plan doit être sauvegardé en artefact GitLab. Ça permet de reviewer le plan avant d'appliquer, et d'éviter les surprises :
artifacts:
paths:
- environments/${ENV}/tfplan
expire_in: 1 week
Le résultat
| Métrique | Avant (manuel) | Après (CI/CD) |
|---|---|---|
| Déploiement | SSH + dbt run manuel |
Merge to main → auto |
| Tests avant prod | Aucun | Slim CI sur chaque MR |
| Temps de review CI | N/A | 3-5 min |
| Risque d'écraser la prod | Élevé (un dbt run sans target) |
Nul (target auto) |
| Infrastructure | Manuelle (SQL scripts) | Terraform + plan/apply |
| Rollback | git revert + dbt run manuel |
Re-merge du commit précédent |
| Trace de déploiement | Aucune | GitLab CI pipeline history |
Le CI/CD n'est pas une luxe pour les équipes Data. C'est la seule façon de déployer en production sans peur. Si ton pipeline GitLab ne teste pas tes modèles DBT avant la prod, tu déploies à l'aveugle.