Dans l'article précédent, on a écrit des unit tests pour protéger notre logique. Et à l'article sur le déploiement, on a mis le projet sur Git et lancé dbt build en production. Il manque une pièce et c'est automatisé tout ça.
On va d'abord mettre en place une CI qui marche de bout en bout, puis on l'optimisera avec le Slim CI.
CI/CD, c'est quoi ici
Deux idées derrière ces lettres :
- CI (intégration continue) : à chaque pull request, une machine lance
dbt buildet les tests sur tes changements. Si quelque chose casse, la PR est bloquée. Tu ne merges jamais du code cassé, parce que c'est vérifié avant. - CD (déploiement continu) : une fois la PR mergée, le projet est déployé en production automatiquement, sans intervention manuelle.
L'esprit, c'est de déplacer la vérification du moment où ça casse en prod vers le moment où tu proposes ton changement. La machine relit ton travail à ta place.
Mettre en place sa première CI
On commence simple. Donc à chaque pull request, la CI installe dbt et lance un dbt build complet. On l'allègera juste après avec le Slim CI.
Quatre étapes : mettre en place les codes secrets, le profiles.yml, le workflow, puis le test avec une vraie pull request.
Étape 1 : les codes secrets dans GitHub
Les identifiants ne vont jamais dans le code. On les stocke dans les secrets du dépôt, où ils sont chiffrés.
- Sur la page de dépôt GitHub, clique sur l'onglet Settings.
- Dans le menu de gauche : Secrets and variables, puis Actions.
- Onglet Secrets, bouton New repository secret.
- Crée trois secrets, un par un (champ Name puis champ Secret), avec des noms exactement identiques à ceux qu'on utilisera ensuite :
| Name | Valeur |
|---|---|
SNOWFLAKE_ACCOUNT |
ton identifiant de compte, par exemple ABCD-EF12345 (sans .snowflakecomputing.com) |
SNOWFLAKE_USER |
ton utilisateur Snowflake |
SNOWFLAKE_PASSWORD |
ton mot de passe |

Une fois enregistré, tu ne peux plus afficher la valeur d'un secret, seulement la remplacer. C'est voulu. Si tu te trompes, tu recrées le secret avec la même clé.
Étape 2 : le profiles.yml pour la CI
Le runner GitHub n'a pas le dossier profile profiles.yml. Donc on crée un profiles.yml à la racine du projet (au même niveau que dbt_project.yml), qui ne contient que des env_var(...), donc aucun identifiant en clair :
dbt_projet:
target: ci
outputs:
ci:
type: snowflake
account: "{{ env_var('SNOWFLAKE_ACCOUNT') }}"
user: "{{ env_var('SNOWFLAKE_USER') }}"
password: "{{ env_var('SNOWFLAKE_PASSWORD') }}"
role: dbt_role
warehouse: dbt_wh
database: analytics
schema: ci
threads: 4
Le nom dbt_projet doit correspondre exactement au champ profile: de ton dbt_project.yml. Le schéma ci isole les builds de CI donc ils ne touchent ni ton dev ni ta prod.
Étape 3 : le workflow
Le workflow vit dans .github/workflows/ci.yml, à la racine du dépôt. Ce dossier n'existe pas par défaut, c'est à toi de le créer. Comme il commence par un point, ton explorateur Windows ne l'affiche pas forcément, mais dans VS Code tu peux taper directement le chemin complet .github/workflows/ci.yml à la création du fichier, et les dossiers se créent tout seuls.

name: dbt CI
on:
pull_request:
branches: [main]
jobs:
ci:
runs-on: ubuntu-latest
env:
SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}
SNOWFLAKE_USER: ${{ secrets.SNOWFLAKE_USER }}
SNOWFLAKE_PASSWORD: ${{ secrets.SNOWFLAKE_PASSWORD }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install dbt-snowflake
- run: dbt deps
- run: dbt build --profiles-dir .
Ce qui se passe, étape par étape :
A chaque pull request vers main, GitHub démarre une machine, récupère ton code, installe Python puis dbt, ensuite installe les packages (dbt deps), et lance dbt build.
Le bloc env: injecte tes infos de connexions dans des variables d'environnement, que le profiles.yml lit via ses env_var. Et --profiles-dir . dit à dbt de chercher le profiles.yml à la racine du projet.
ubuntu-latest est un runner hébergé par GitHub donc il démarre une machine Linux dans le cloud, exécute tes étapes, puis la détruit. Tu n'as rien à configurer. C'est gratuit dans les limites incluses.Étape 4 : tester avec une pull request
Ton workflow se déclenche sur les pull requests vers main. Tu ne peux pas ouvrir une PR de main vers main, il te faut donc une branche source.
D'abord, pousse ces fichiers sur ta branche de travail :
git add -f profiles.yml
git add .github/workflows/ci.yml
git commit -m "Mise en place de la CI dbt"
git push
Ensuite, on va créér une branche, faire un petit changement ou ajouter un commentaire pour ouvrir aprés une pull request :

git checkout -b test-ci
# On ajoute un commentaire dans un modèle
git add .
git commit -m "test de la CI"
git push -u origin test-ci
Sur GitHub, clique sur "Compare & pull request" pour ouvrir la PR de test-ci vers main.

Le workflow se lance, et tu vois le check apparaître directement dans la PR et dans l'onglet Actions.
Quand il passe au vert, tu peux merger car le ci.yml arrive alors sur main et protège désormais toutes les futures pull requests.

Le problème du "tout reconstruire"
Notre CI marche, mais elle reconstruit tout le projet à chaque PR. Sur un petit projet, ça passe mais sur un projet de centaines de modèles, c'est interminable et ça coûte cher en compute, alors que la PR n'a touché que deux modèles.
Le Slim CI règle ça car il ne construit et ne teste que ce qui a changé, plus ce qui en dépend en aval. C'est là que state:modified entre en jeu.
Le Slim CI avec state:modified
Le principe de state:modified : il compare l'état actuel du projet à un manifest de référence, passé avec --state. En CI, ce manifest de référence, c'est celui de ta dernière production. La commande devient :
dbt build --select state:modified+ --defer --state ./prod-artifacts --profiles-dir .
state:modified+: seulement les modèles modifiés depuis la prod, et tout ce qui en dépend en aval (le+).--state ./prod-artifacts: le dossier contenant lemanifest.jsonde ta dernière prod, qui sert de point de comparaison.--defer: pour les modèles non reconstruits, dbt pointe vers la version qui existe déjà en prod, au lieu d'essayer de les rebuild. C'est ce qui permet de ne traiter que ta poignée de modèles tout en gardant unref()qui fonctionne.
manifest.json est généré par dbt à chaque exécution, dans target/. La CI le récupère avant de lancer le build. Sans ce manifest de référence, state:modified n'a rien à comparer.Le conseil pratique : commence avec le dbt build complet qu'on a mis en place, vérifie que toute la chaîne fonctionne (secrets, profiles, build vert), et ajoute le state:modified+ --defer .
Le déploiement (le CD)
La CI valide mais le CD déploie. Une fois donc la PR mergée dans main, on veut que la prod se reconstruise toute seule. Pour ça, un second workflow, déclenché cette fois sur les push vers main (et un merge de pull request, c'est justement un push sur main).
Crée .github/workflows/cd.yml :
name: dbt CD
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
env:
SNOWFLAKE_ACCOUNT: ${{ secrets.SNOWFLAKE_ACCOUNT }}
SNOWFLAKE_USER: ${{ secrets.SNOWFLAKE_USER }}
SNOWFLAKE_PASSWORD: ${{ secrets.SNOWFLAKE_PASSWORD }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install dbt-snowflake
- run: dbt deps
- run: dbt build --target prod --profiles-dir .
- name: Sauvegarder le manifest de prod
uses: actions/upload-artifact@v4
with:
name: prod-manifest
path: target/manifest.jsonDeux différences avec la CI :
Le déclencheur est push sur main (et non pull_request), et la commande utilise --target prod. C'est le même fichier profiles.yml, mais une cible différente car la CI écrit dans le schéma ci, le CD dans le schéma prod.
Les secrets sont les mêmes ici par simplicité mais réalité souvent la prod utilise un rôle, un utilisateur ou même un compte dédié donc tu créerais alors des secrets séparés pour la prod.

La dernière étape sauvegarde le manifest.json produit par ce run de prod, comme artefact GitHub. C'est exactement le manifest dont le Slim CI a besoin : à la prochaine pull request, ton workflow de CI le télécharge (avec actions/download-artifact) dans ./prod-artifacts, et state:modified a enfin son point de comparaison. La boucle est bouclée : la prod produit le manifest, la CI s'en sert pour ne reconstruire que ce qui a changé. Chaque déploiement prépare le terrain du suivant.
dbt Cloud
Tout ce qu'on vient de construire à la main, dbt Cloud le propose en intégré. Si tu utilises dbt Cloud, tu connectes ton dépôt, tu configures un job CI, et il gère pour toi le manifest de prod, le --defer et le state:modified. Pas de YAML GitHub ou actions à écrire.
Le choix dépend du contexte car GitHub Actions (ou GitLab CI) te donne le contrôle total et reste gratuit sur de petits volumes mais dbt Cloud enlève toute cette complexité au prix d'un abonnement. Dans les deux cas, le principe du Slim CI reste le même.
Il ne reste qu'une brique à connaitre pour faire tourner tout ça proprement et vite en production et c'est l'orchestration et la performance et donc planifier les exécutions, les enchaîner avec le reste de ta stack, et optimiser temps et coût sur Snowflake.
Aller plus loin
Cet article fait partie de la formation dbt complète, du premier modèle au déploiement en production.
👉 Suivre toute la formation dbt
dbt tourne sur Snowflake dans ce parcours. Pour maîtriser le socle (warehouses, rôles, ingestion) :
👉 Accéder à la Formation Snowflake
Et pour t'entraîner en conditions d'examen, la certification dbt Analytics Engineering teste précisément la CI/CD et le Slim CI.
👉 Préparer la certification dbt sur DataCertification.fr
Tu veux que je t'accompagne sur ton projet data (Snowflake, dbt, modélisation, industrialisation) ?
👉 Réserver un appel de 30 minutes
Questions fréquentes
Où mettre les identifiants Snowflake pour une CI dbt ?
Dans les secrets du dépôt GitHub, jamais dans le code. On va dans Settings, puis Secrets and variables, puis Actions, et on crée un secret par identifiant (compte, utilisateur, mot de passe etc...). Le workflow les injecte ensuite dans des variables d'environnement, que le profiles.yml lit via env_var. Les secrets sont chiffrés et non relisibles après enregistrement.
Faut-il commiter le profiles.yml pour la CI ?
Le runner n'a pas le dossier de config, donc dbt a besoin d'un profiles.yml. La règle "ne pas commiter profiles.yml" vise les fichiers contenant des identifiants en clair. Un profiles.yml qui n'utilise que des env_var ne contient aucun secret, il est donc safe à versionner. Attention juste à ce qu'il ne soit pas exclu par le .gitignore, sinon il n'arrivera pas sur le runner.
C'est quoi le Slim CI en dbt ?
Le Slim CI ne construit et ne teste que les modèles modifiés depuis la dernière production au lieu de reconstruire tout le projet.
C'est quoi --defer en dbt ?
L'option --defer indique à dbt que, pour les modèles non sélectionnés, il doit pointer vers la version qui existe déjà en production plutôt que de les reconstruire. Combinée à state:modified, elle permet de ne traiter que tes modèles modifiés tout en gardant des ref() qui fonctionnent, puisque les modèles en amont sont résolus vers la prod.
