Travailler avec dbt Core : du développement à la production

Nous vous avons déjà parlé de dbt Core dans ce blog. Cet outil open source permet d'écrire des transformations de données de manière déclarative, de les valider et de les documenter. Celui-ci semble avoir le vent en poupe, se classant régulièrement en tête de nombreux classements d'outils de transformation de données.

Comme tout bon projet open source à succès, dbt Core a sa déclinaison managée : dbt Cloud. Ce service est extrêmement complet puisqu’il permet notamment de :

  • développer via un IDE intégré ;
  • tester et déployer ses transformations dans plusieurs environnements grâce à des pipelines ;
  • lancer automatiquement des pipelines grâce à l’orchestrateur intégré ;
  • héberger la documentation générée automatiquement ;
  • faire appel à diverses fonctionnalités via l’API.

dbt Cloud facilite grandement les phases de développement et d’industrialisation d’un projet et semble être le moyen le plus évident de travailler avec dbt Core.

Cependant, pour différentes raisons (politique d’entreprise, pécuniaire, etc.), vous pourriez être amené à faire l’impasse sur la solution Cloud et ainsi devoir vous contenter de la version Core qui a fait la réputation de dbt Labs. C'est ce que nous allons explorer dans cet article.

Qu’est ce que dbt Core ?

Avant de commencer, il est important de comprendre ce qu'est dbt Core et comment un projet est structuré.

dbt Core est une librairie Python qui fournit une interface en ligne de commande (CLI) autour de laquelle notre projet sera construit. Le CLI permet notamment de compiler et d'exécuter le projet. L'exécution peut se faire sur une grande variété de data plateformes grâce aux adaptateurs, qui sont également des bibliothèques Python.

On peut initier un projet via le CLI en utilisant la commande suivante :

dbt init

Cela va lancer une boîte de dialogue. Une fois que vous aurez répondu aux questions, dbt va :

  1. générer l’arborescence de fichiers du projet ;
  2. créer/mettre à jour le fichier de profiles.yml dans le dossier .dbt situé à la racine de votre dossier personnel.

Un projet dbt se compose de :

  • fichiers YAML de configuration. Le fichier project.yml se trouvant à la racine du projet étant, comme son nom l’indique, le fichier de configuration principal du projet ;
  • fichiers SQL dans lesquels sont décrites nos transformations.

Ces fichiers sont en réalité des templates Jinja qui seront compilés par dbt.

Le projet en lui-même ne contient aucune information de connexion. Celles-ci sont renseignées dans un profil stocké dans le fichier profiles.yml dont j'ai parlé précédemment. Ce fichier est global et peut contenir les profils de plusieurs projets dbt.

Si vous souhaitez en savoir plus sur les concepts de dbt, je vous invite à lire l’introduction et le chapitre prérequis de l’excellent article de Jeremy Nadal (vous pouvez également lire la suite, elle est vraiment cool :D ). Vous en apprendrez plus sur les modèles, les sources, les seeds et les tests.

Maintenant que nous savons à quoi ressemble un projet dbt, nous allons pouvoir continuer.

L’environnement de développement

Dans cette section, je vais vous guider à travers les étapes nécessaires à la mise en place d’un environnement de développement. Je vous présenterai également des outils qui vous permettront de gagner du temps et maximiser votre efficacité.

Installation de dbt Core

Comme nous l’avons vu, dbt Core est une librairie Python. Il va donc nous falloir un interpréteur Python. Les dernières versions de dbt Core sont compatibles avec les versions de Python 3.7 ou supérieures. Toutefois, gardez à l'esprit que les adaptateurs n'ont pas tous les mêmes compatibilités. Assurez-vous de choisir la version de Python en conséquence.

Bien évidemment, qui dit projet Python, dit environnement virtuel. C’est la base pour isoler les environnements et ainsi éviter les conflits de dépendances. Au sein de cet environnement virtuel, vous pourrez installer dbt Core et, si besoin, l’adaptateur correspondant à votre besoin.

Pour être certain que tous les développeurs du projet utilisent les mêmes versions de dbt Core et des adaptateurs, il est recommandé de fixer les versions de chaque dépendance dans un fichier requirements.txt.

dbt-core==1.4.1
dbt-postgres==1.4.1

Exemple de fichier requirements.txt

# Création d'un environnement virtuel
python -m venv nom_du_virtualenv

# Activation de l'environnement virtuel
source ./nom_du_virtualenv/bin/activate

# Installation des dépendances listées dans le fichier requirements.txt
pip install -r requirements.txt

Procédure d’installation de l’environnement virtuel (Linux)

Nous avons désormais un environnement de développement prêt à être utilisé. Il ne vous reste plus qu’à initier ou cloner un projet existant. Mais sans informations de connexion, vous ne pourrez pas faire grand-chose. Dans la prochaine section, nous allons voir comment celles-ci sont gérées.

Gérer les informations de connexion

Si vous avez été attentif, vous vous souviendrez que les informations de connexion sont stockées dans le fichier profiles.yml situé dans le dossier .dbt à la racine du dossier personnel de l'utilisateur. Étant donné que ce fichier ne se trouve pas dans le dossier du projet, nous pouvons être sûrs que les identifiants ne seront jamais commités par inadvertance.

Un profil peut contenir les informations de connexion de plusieurs environnements. Cependant, pour le développement, il est rare qu’on ait besoin de plus d’un environnement.

project_a_profile:  # Nom du profil
  target: dev  # Environnement par défaut
  outputs:  # Mapping des environnements
	dev:
  	  type: postgres
  	  host: <dev_host>
  	  port: <dev_port>
  	  user: <dev_user>
  	  password: <dev_password>
  	  dbname: dev_db
  	  schema: public
	prd:
  	  type: redshift
  	  host: "{{ env_var('PG_HOST') }}"  # Il est possible d'utiliser des variables d'environnement grâce à Jinja
  	  port: "{{ env_var('PG_PORT') | int }}"
  	  user: "{{ env_var('PG_USER') }}"
  	  password: "{{ env_var('PG_PASSWORD') }}"
  	  dbname: prd_db
  	  schema: public

Exemple de fichier profile.yml (Bien sûr, un développeur ne devrait pas avoir un accès en écriture sur la production via dbt !)

Lorsque vous utilisez le CLI, dbt va chercher les informations de connexion dans le profil correspondant à celui renseigné dans le fichier project.yml. Il suffit donc de demander aux développeurs qui rejoignent le projet de mettre à jour leur fichier profiles.yml.

Vous devriez désormais avoir tout ce qu'il faut pour pouvoir utiliser le CLI et commencer les choses sérieuses. Mais attendez, je vais maintenant vous présenter des outils qui devraient vous faciliter la vie.

Des outils bien pratiques

dbt Power User

Chacun a ses préférences en termes d’IDE/éditeur et n’importe lequel peut faire l’affaire pour éditer les fichiers du projet. Toutefois, si vous utilisez VS Code, vous pourrez utiliser une extension bien pratique pour dbt : dbt Power User. Une fois configurée, elle vous permettra, entre autres, de :

  • utiliser des snippets pour les balises Jinja ;
  • naviguer facilement d’un modèle à l’autre grâce aux références ;
  • afficher les parents et enfants du modèle en cours d’édition à partir du DAG ;
  • exécuter des modèles et d’en afficher un aperçu.

SQLFluff

SQLFluff est un linter pour SQL développé en Python. En plus de traquer les erreurs, SQLFluff va vous permettre d’avoir une cohésion stylistique dans le code SQL produit, quel qu'en soit l’auteur. SQLFluff peut être exécuté manuellement, déclenché par pre-commit ou utilisé dans un pipeline de CI.

Par défaut, SQLFluff utilise Jinja comme templater. Il existe également un templater pour dbt qu’il faut installer et configurer (vous trouverez la procédure ici). Bien que plus lent que son pendant Jinja, vous n’aurez pas besoin de mocker les fonctions utilisées dans vos templates.

La documentation de SQLFluff recommande d'ailleurs l’utilisation des deux templaters. Pour le développement, on préfèrera le templater Jinja pour sa rapidité, tandis qu’on utilisera plutôt le templater dbt pour les pipelines de CI pour tester ses macros.

Pre-commit

Pre-commit est un outil permettant d’exécuter des hooks avant chaque commit. Ces hooks peuvent inclure l’exécution de tests unitaires, la vérification de la conformité avec des conventions de code, etc.

SQLFluff dispose d’ailleurs d'une intégration pour pre-commit, permettant ainsi de s'assurer que le code SQL est bien formaté et écrit en respectant les règles de style. Une fois installé, il suffit de configurer le hook dans le fichier .pre-commit-config.yaml à la racine du projet.

dbt Core en production

Maintenant que vous avez développé vos transformations, il est temps de trouver un moyen de les exécuter en dehors de votre poste de travail. Dans cette section, je vais vous présenter deux approches pour utiliser dbt Core en production.

L’approche intuitive : utiliser une VM

Je pense qu’installer notre projet sur une VM est la première chose qui vient à l’esprit de la plupart des gens. En effet, cette approche ressemble le plus à ce qui est mis en place pour l’environnement de développement. Pour cette raison, je ne vais pas m’attarder sur la procédure de l’installation du projet sur la VM.

Comme dbt est léger (il ne réalise lui-même aucun traitement de données), il ne nécessite pas beaucoup de ressources. Par exemple, une instance AWS EC2 t3.micro devrait être suffisante dans la plupart des cas.

Il reste cependant un point à régler : comment exposer le CLI pour qu’il puisse être utilisé par votre ordonnanceur.

dbt-rpc est un plugin développé par dbt Lab qui permet d’exposer dbt Core grâce à un serveur rpc (pour Remote Procedure Call). Mais comme le projet va être abandonné d’ici la fin de l’année, nous n’allons pas nous attarder sur cette solution.

Pour remplacer dbt-rpc, les équipes de dbt Labs travaillent sur dbt-server : une API web basée sur FastAPI. Mais à l’heure où j’écris ces lignes, le projet n’en est encore qu’à ses balbutiements et n’est pas encore stable.

En attendant une solution officielle plus pérenne, le plus simple reste encore d’utiliser le protocole SSH. Si vous utilisez AWS, à la place de SSH, vous pourrez recourir au service Systems Manager qui va vous permettre d'exécuter des commandes directement sur votre VM. L’avantage est qu’il est très bien intégré au service Step Functions qui va vous permettre d’orchestrer facilement votre chaîne d’alimentation.

Voilà pour l’approche intuitive. Passons maintenant à une approche qui devrait avoir plus de succès auprès des DevOps de votre équipe.

L’approche DevOps friendly : la conteneurisation

La conteneurisation est un bon moyen d’isoler son application en encapsulant toutes les dépendances dont elle a besoin dans un conteneur. De ce fait, l’application est indépendante du système-hôte et il est facile de la déployer sur un grand nombre d’environnements.

L’idée est de générer une image Docker pour chaque release du projet dbt. Cela peut facilement être mis en place par un pipeline de CI/CD.

# Image de base
FROM --platform=linux/amd64 python:3.9.15-slim-bullseye

# Installation des dépendances
RUN apt-get update \
  && apt-get dist-upgrade -y \
  && apt-get install -y --no-install-recommends \
	git \
	software-properties-common \
	make \
	build-essential \
	ca-certificates \
	libpq-dev \
  && apt-get clean \
  && rm -rf \
	/var/lib/apt/lists/* \
	/tmp/* \
	/var/tmp/*

# Variable d'environnement
ENV PYTHONIOENCODING=utf-8
ENV LANG=C.UTF-8

# Mise à jour des packages Python
RUN python -m pip install --upgrade pip setuptools wheel --no-cache-dir

# Définition du répertoire de travail
WORKDIR /usr/app/

# Installation des dépendances Python du projet (dbt-core, adptateur, etc.)
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt

# Copie des sources du projet
ADD dbt_project/ .

# Copie du fichier profiles.yml
COPY profiles.yml .

ENTRYPOINT ["dbt"]

Exemple de Dockerfile à mettre à la racine du projet dbt

Dans le Dockerfile ci-dessus, vous pouvez voir que le fichier profiles.yml est copié dans le même répertoire que les sources du projet. C’est tout à fait normal. Par défaut, dbt va chercher le fichier profiles.yml dans le répertoire du projet.

En parlant du fichier profiles.yml, les informations de connexion sont stockées dans des variables d’environnement comme je vous ai montré plus haut. Les variables seront renseignées lors du déploiement du conteneur.

Il ne reste plus qu’à builder votre image, à l’uploader dans un dépôt et à la déployer où vous le souhaitez. Comme le point d'entrée du conteneur est la commande dbt, votre orchestrateur pourra l’exécuter avec les commandes du CLI (run, build, test...).

# Pour builder votre image
docker build -t <nom_du_projet>:<version> /chemin/vers/dossierDuDockerfile

# Pour lancer des commandes dbt (run, build, test...)
docker run --env-file <fichier_contenant_les_variables_d_env> <nom_du_projet>:<version> <commande_dbt>

Logs et Docs

Dans un souci d’observabilité, il est important de remonter les logs produits par dbt dans un service/plateforme de monitoring. Par défaut, dbt écrit les logs dans le dossier logs de votre projet dbt.

De plus, dbt peut générer un site statique de documentation. Celui-ci peut être utilisé comme catalogue de données. En le consultant, les utilisateurs de votre plateforme pourront savoir d’où proviennent les données et comment elles sont calculées, renforçant ainsi leur confiance envers les données qu’ils consomment. Comme le site est statique, il est très simple de l’exposer. Par exemple, sur AWS, vous pourriez simplement héberger les fichiers sur un bucket S3.

dbt docs generate

Cela va créer les fichiers suivants dans le répertoire target de votre projet : index.html, catalog.json et manifest.json

Il va donc falloir faire en sorte de récupérer ces fichiers pour les uploader là où on le souhaite.

Dans le cas de la première approche, si vous utilisez un orchestrateur qui exécute des actions directement sur la machine, alors vous pourriez facilement ajouter des étapes pour récupérer les fichiers de log et de documentation et les uploader où vous le souhaitez.

Une autre solution fonctionnant pour les deux approches est d’écrire un wrapper qui va encapsuler le CLI dbt. Ainsi, après une exécution, le wrapper pourrait générer la documentation et uploader les fichiers de logs et de documentation.

#!/bin/bash

dbt $@

dbt docs generate

# Upload des fichiers de docs
# ...

# Upload des fichiers de logs
# ...

Exemple de wrapper

Conclusion

Voilà, vous devriez maintenant être armés pour développer et mettre en production vos projets dbt. Je vais profiter de cette conclusion pour vous annoncer qu’Ippon est maintenant partenaire de dbt. N’hésitez surtout pas à faire appel à nous pour vos projets.