Azure Data Factory : réussissez vos pipelines ETL !

Cet article technique vise à présenter succinctement Azure Data Factory (ADF), comment le mettre en place dans un environnement DevOps sur Microsoft Azure, et d’autres conseils que j’ai récoltés au fur et à mesure pour réussir vos pipelines ETL ! Il vise un public technique avec une première expérience sur l’outil. Les techniques présentées fonctionnent à date, mais l’outil évoluant vite, comparez ce qui est présenté ici avec ce qui est préconisé avec la documentation (pas toujours à jour non plus !).

L’outil

Azure Data Factory est un ETL / ELT managé dans le cloud disponible sur Microsoft Azure, à la popularité grandissante. Il vise à remplacer progressivement SQL Server Integration Service (SSIS), l’ETL plus ancien et traditionnellement On-premises de Microsoft. Les deux ne sont cependant pas antagoniques : il est possible d’exécuter des packages SSIS dans ADF, peut être comme une première étape pour un lift-and-shift.

ADF garde une interface visuelle cohérente, qui sera familière à ceux qui ont l’habitude d’utiliser des outils ETL comme Talend ou Matillion. C’est cette interface que vous utiliserez pour la construction de vos pipelines.

Vision d’ensemble de l’interface ADF

Sous le capot, c’est soit un ensemble de machines gérées par Azure (pour les pipelines), soit un cluster Spark (pour les Data Flows, nous précisons la différence juste après) qui s’occupe de l’exécution. ADF n’est donc pas fondamentalement limité en matière de volumétrie, mais restera cependant, dans le cadre de ses composants logiques par défaut, adapté à des tâches ETL d’une complexité faible à moyenne. Je précise bien dans le cadre de ses composants logiques par défaut, car il est possible d’exécuter des notebooks Databricks, HDInsight, des Azure Functions et les Data Flows comme composants de votre pipeline pour ajouter la logique de transformation personnalisée qui vous est nécessaire.

Exemple de l’infrastructure managée mise en place pour une activité Copy (copier une donnée source vers une destination cible)

ADF a donc deux parties logiques principales :

  • Une partie dédiée à l’orchestration : les pipelines. C’est dans cette partie que l’on retrouve la logique pour extraire des données, les déplacer d’un endroit à l’autre. C’est aussi dans cette partie que vous pouvez orchestrer l’exécution des services de transformation mentionnés précédemment (Databricks…) : ADF jouera ici le rôle d’orchestrateur entre les différents services. De même, il est possible d’utiliser des composants de Machine Learning pour pouvoir intégrer vos modèles réalisés notamment avec AzureML, dans le pipeline. C’est sur cette partie que portera l’article.
  • Une partie dédiée uniquement aux préparations / transformations : les Data Flows. Cela concerne des tâches de data wrangling, ce qu’on ferait par exemple avec une librairie comme Pandas. Il faut utiliser le langage M de Power Query, dont les fonctions sont ensuite traduites en code Spark. Cette partie est encore jeune et pas très mature, je lui préfère actuellement l’exécution d’un notebook (qui peut être orchestré dans un pipeline) sur Databricks pour ces étapes de préparation.

Outre la partie “ETL / ELT managé dans le cloud”, l’un des avantages indéniables d’ADF, c’est la richesse de ses connecteurs (+90) pour divers endpoints et son intégration dans l’écosystème Azure. L’autre avantage, c’est la partie monitoring / industrialisation qui bénéficie de la maturité de Azure sur le sujet, notamment pour Azure DevOps. Vous pouvez aussi créer des pipelines hybrides, c’est-à-dire des pipelines qui vont se connecter directement à vos environnements On-premises pour en extraire / déposer des données, via les self-hosted integration runtime (un service à installer sur un de vos serveurs, servant de passerelle).

Gardons cependant en tête que ADF présente quelques limitations à l’heure actuelle, mais que ces dernières sont amenées à être rapidement comblées en fonction des retours utilisateurs sur le forum, où vous pouvez appuyer des demandes de fonctionnalités (les équipes sont vraiment à l’écoute). C’est un outil qui évolue vite. Il manque notamment, selon moi (et parmi les requêtes du forum) :

  • La possibilité d’arrêter un pipeline en fonction de la valeur d’une variable : lien.
  • La possibilité de désactiver des composants dans le pipeline pour qu’ils ne soient pas exécutés (et inversement de pouvoir exécuter des composants individuellement et pas tout le pipeline à chaque fois). Le compromis actuel est de mettre la connexion entre les composants en gris pour ne pas prendre en compte le reste du flow.
  • Un composant pour l’envoi de notifications (emails…). Fonctionnalité en cours de développement : lien
  • L’astuce actuelle consiste à utiliser une Logic App pour cette tâche et la déclencher via le composant Web (requête HTTP) de ADF.
  • Les tests (vérification de type des colonnes, nombre de lignes pour une table…) : lien.

Je vais maintenant vous donner quelques conseils et astuces pour la création de vos pipelines. Le but n’est pas de faire le tour entier de l’outil, la documentation (très bien faite) existe pour cela, mais plutôt de mettre en avant des points que l’on apprend par “expérience” (ou parce que c’était caché au fin fond d’une doc !).

Organisation des Pipelines

Dans un premier temps, c’est l’organisation de vos pipelines et surtout de vos datasets qui est importante. Dans un petit projet, cela se ressent moins, mais il faut penser à l’évolution potentielle. A l’instar de ce qu’on ferait avec un autre ETL, on va donc chercher à créer des niveaux d’abstraction dans nos pipelines (des hiérarchies, voir cet excellent article), utiliser les dossiers, et créer des datasets génériques. On peut aussi viser à respecter les conventions de nommage.

Datasets

Les datasets (source / cibles de données) représentent souvent la partie la moins bien organisée, même pour des petits projets. Quand on débute, on va créer des datasets rigides, qui référencent plusieurs fois la même source de données, et on finit par s’y perdre. Dans un second temps, on comprend l’intérêt des paramètres des datasets pour pouvoir variabiliser le chemin du dossier, ou le nom du fichier. Là encore, l’erreur commune est de commencer à rajouter de la logique par des expressions compliquées (par exemple, enlever l’extension du nom du fichier et créer un dossier avec ce nom etc.) au niveau de la définition du dataset.

Le problème ? Vos datasets deviennent trop spécialisés et ne sont pas réutilisables. Si un composant a besoin de la même source de donnée, mais qu’il n’a pas besoin de cette logique au niveau du nom du fichier ? Cela vous forcera à recréer un autre dataset, et ainsi de suite.

Ce qu’on souhaite donc garder en tête, c’est que la logique ne doit pas être du côté des datasets pour la variabilisation. Les expressions doivent être effectuées au niveau de l’activité dans la pipeline et le résultat final passé au dataset.

Implémentation de la logique au niveau de l’activité :

Cependant, certains cas nécessitent un nouveau dataset pour la même source, c’est notamment le cas pour passer de la volonté de copier un fichier compressé en brut, à la volonté de sa décompression (un en brut, l’autre en zipDeflate…). En effet, il faudra dans ce cas modifier le type de compression au niveau du dataset.

Avec ce réflexe de variabilisation de chaque champ d’un dataset, on peut rencontrer un problème : que faire si j’ai seulement besoin d’accéder au contenu d’un dossier dans cette source, et ne pas descendre jusqu’à au niveau du fichier ? J’aurai alors une erreur car le paramètre pour le nom du fichier restera vide. L’astuce, c’est d’assigner un string vide au paramètre qu’on ne souhaite pas remplir : @toLower(‘’)

On peut aussi utiliser les wildcards : en donnant la valeur * au filename, je sélectionne tous les fichiers dans le dossier spécifié.

Logique

La plupart des composants sont intuitifs d’utilisation. Cependant, certains besoins nécessitent une attention particulière.

Décompression de fichiers

Un cas qui peut paraître simple d’emblée (et qui devrait l’être), mais qui pose parfois souci au début, c’est la décompression de données sources vers la cible (par exemple, un livrable au format .zip ou .tar.gz sur un FTP, qu’on souhaite décompresser sur notre Data Lake). La configuration doit se faire à la fois au niveau des datasets et de l’activité Copy.

Pour décompresser un fichier, je dois spécifier, dans le dataset : le type de compression du dataset source en fonction du format de compression des données (en zipDeflate si c’est un zip, tarDeflate si c’est un .tar.gz…). Ensuite, je dois bien m’assurer que le filename (paramètre du dataset) n’est pas spécifié dans le dataset de Sink (cible) de l’activité Copy, il doit être vide. Seul le folder doit être spécifié.

Ensuite, deux cas :

  • Mon fichier compressé à un seul niveau (pas de sous-dossiers) : dans l’activité Copy, partie Sink, mettre le Copy Behavior à None.
  • Sinon, utiliser Preserve Hierarchy / Flatten Hierarchy.

Bon à savoir : il y a une option dans la partie Source de l’activité Copy, Preserve zip file name as folder (apparaît si le dataset de source à une compression zipDeflate) afin de spécifier si je souhaite que lors de la décompression, ADF créé un nouveau dossier en plus avec le nom du fichier, ou si je souhaite décompresser les fichiers directement à l’endroit spécifié.

Parallélisme et boucles imbriquées

Parfois, quand mon pipeline est trop imbriqué, je ne peux pas accéder aux variables de composants de niveau supérieur (ex : j’ai une itération ForEach, à l’intérieur une condition IF, et à l’intérieur du IF une activité Copy : je ne pourrai pas obtenir le nom de chaque fichier de l’itération). Il faudra utiliser le composant Set Variable pour avoir une variable globale dynamique.

Cependant, si j’ai un mode d’exécution en parallèle défini dans la boucle ForEach, il est possible que la variable utilisée par une pipeline  (lorsque je l’assigne avec SetVariable) soit modifiée par l’autre pipeline entre temps (c’est le problème des variables partagées), ce qui crée des incohérences dans la suite des traitements. Il faut donc garder ce point en tête.

Scheduling et Monitoring

Je peux configurer des notifications en cas de succès ou échec de mon pipeline via la catégorie Alert & Metrics dans la partie Monitoring.

Si après création des alertes, rien ne se passe lorsque vous lancez vos pipelines en mode Debug pour tester, cela ne vient pas de la configuration (j’ai perdu du temps sur ce point…). Simplement, que ce soit pour l’affichage des métriques au niveau du dashboard ou le déclenchement des alertes, seuls les runs de pipelines déclenchées par un trigger sont prises en compte. Pour vos debugs, vous ne pourrez que consulter l’onglet Debug des runs de pipeline. Cela fait sens après réflexion, afin de ne pas polluer les métriques d’exécution avec les tests.

Ainsi, pour lancer mon pipeline en “conditions réelles” (déclenchement des triggers…), je peux aller dans mon pipeline et faire Add Trigger -> Trigger now.

C’est aussi par cette manière (via le pipeline) qu’il est mieux de créer le trigger scheduler.

Précisons aussi que le dashboard, et globalement les métriques de run, ont un lag. Les résultats ne sont pas en temps réel, il faut souvent attendre quelques minutes entre l’exécution du pipeline et son affichage.

De même, attention pour l’activation des triggers : le déclencheur ne s’applique que lorsque vous avez publié (via Publish) dans Data Factory, et non lorsque vous enregistrez le déclencheur dans l’interface utilisateur. En effet, un trigger est aussi un fichier JSON (ARM) publié sous Artifact lors du Build.

Les alertes créées et déclenchées sont aussi disponibles au niveau du groupe de ressource : partie Monitoring -> Alerts. Ce sont les_ Action Groups_ qui sont utilisés par les alertes pour envoyer des notifications (email…). Pour les modifier : groupe de ressource -> partie Monitoring -> Alerts -> Manage actions.

DevOps

En suivant les bonnes pratiques DevOps, nous chercherons pour notre projet à :

  • avoir des environnements séparés : dev, recette, production ;
  • avoir le versioning de nos pipelines (ici sur git) ;
  • avoir un pipeline de release pour le déploiement de nos modifications réalisées en dev sur les autres environnements.

Mise en place des environnements

On déploie généralement, pour chaque projet, un groupe de ressources par environnement. Cet article n’ayant pas vocation à revenir sur ce point, ni expliquer les étapes, vous trouverez plus d’informations dans cet article.

Extrait de l’article mentionné précédemment

Services liés

Les services liés correspondent aux différents services avec lesquels vous interagissez dans votre pipeline : connexions aux sources de données, Key Vault pour le stockage des mots de passe, Databricks etc. Comme vous aurez aussi ces ressources répliquées dans les différents environnements, il vous faudra, pour le pipeline de release, variabiliser leur connexion (nous verrons juste après comment le faire).

Dans tous les cas, il vous faudra un Key Vault pour le stockage des identifiants nécessaires à l’accès des autres services. Vous ne devriez jamais les stocker sur une branche git.

Pour l’accès à Key Vault, attention de bien ajouter la Managed Identity de votre ADF (quand vous créez un Linked Service avec Key Vault, le panneau va afficher le nom de la Managed Identity) aux Access Policies du Key Vault.

Pensez aussi à donner un nom générique à vos services liés. Si vous êtes actuellement dans l’environnement de dev, n’appelez pas votre service “KeyVaultDev” mais simplement “KeyVault”. Cela facilite le déploiement entre environnements.

Versioning

ADF bénéficie d’une intégration simplifiée avec Azure DevOps. Notre premier réflexe devrait être de lier directement le projet ADF de l’environnement de dev à un un projet Git spécifique. ADF supporte les repos Github et Azure DevOps. Pour cela, rendez-vous dans Paramètres -> Source Control -> Git Configuration.

La spécification de tout ce qui est fait sur ADF (pipelines, datasets, services liés…) est au format JSON. Ce sont ces fichiers de définition qui seront sauvegardés sur Git. Le lien fait, chaque enregistrement des modifications effectuées sur ADF mettra à jour les fichiers correspondants sur Git.

Pensez à créer une branche de dev dans votre repo Git, et à la sélectionner dans ADF lorsque vous réalisez des modifications / développements. Une fois vos modifications terminées, vous ferez une pull request sur la branche de master.

Sur le repository qui sera lié avec ADF, vous trouverez donc vos branches standards (dev, master), et une branche supplémentaire, adf_publish, que ADF va se charger de créer pour vous. Le but de cette branche est d’héberger le code ARM (Azure Resource Manager, l’IaC de Azure) de votre projet qui sera généré une fois que vous cliquez sur “Publish” dans l’interface principale. C’est en quelque sorte l’étape de Build. ADF prend votre projet et transforme cela en config ARM.

Il est donc important de comprendre que seul l’ADF de dev sera lié avec votre repository Git. Les ADF dans les groupes de ressource de recette et production ne sont pas liés à un repository et seront “alimentés” par le pipeline de release.

Pour cliquer sur ce bouton “Publish”, vous devez vous trouver sur la branche master. D’où le cycle : développements sur la branche de dev -> pull request sur master -> publish.

Je peux aussi exporter en code ARM tout mon projet ADF (pipelines, datasets, integrations runtimes, triggers…) réalisé manuellement dans le mode visuel via le bouton ARM Template -> Export ARM Template dans l’interface principale.

L’avantage, c’est qu’une fois que vous avez votre code ARM dans la branche adf_publish, vous êtes prêt pour l’étape de déploiement dans les différents environnements (le pipeline de Release). L’inconvénient, c’est que cette étape de Build est manuelle, il faut cliquer sur le bouton Publish.

Variabilisation

Nous devons préparer notre projet pour qu’il soit facilement paramétrable. Vous réaliserez normalement ces étapes de variabilisation avant d’avoir cliqué sur Publish, de sorte que les templates ARM soient prêts à l’emploi. Ces variables pourront être modifiées dans le pipeline de Release, et ainsi permettre des valeurs changeantes selon les environnements, c’est là le but.

Paramètres du pipeline

Pour changer la valeur des paramètres du pipeline selon l’environnement, je dois créer un fichier arm-template-parameters-definition.json (et pas le arm_template_parameters.json par défaut qui ne contient pas les paramètres du pipeline). Il doit se situer à la racine de la branche sur le repository git du projet.

Avec facilité, je peux aussi aller dans Manage -> Parameterization template pour l’éditer directement, cela créera automatiquement le fichier si il n’est pas déjà présent.

Voici un exemple de configuration de ce fichier.

De mon point de vue, il n’est pas forcément intéressant de variabiliser les paramètres d'un pipeline, car vous devrez les spécifier de nouveau lors de la mise en place du trigger. Dans une approche multi-environnements, vous créerez généralement le trigger final dans l’environnement de production, et vous n’avez pas forcément envie de déployer un trigger actif d’un environnement à l’autre. C’est un choix personnel, à considérer.

Les paramètres globaux

Les paramètres globaux sont récents (été 2020) et permettent d’avoir des paramètres référencés par plusieurs pipelines. En effet, si plusieurs pipelines ont des paramètres en commun, les spécifier pour chaque pipeline entraînerait une duplication.

Un autre avantage, c’est qu’il est simple pour nous de les inclure dans les templates ARM : il suffit de cliquer sur Include in template ARM dans leur menu.

Variabiliser les paramètres de vos services liés

La variabilisation des paramètres des services liés n’est pas intuitive. Pour cela, il faut cliquer sur ce bouton au survol du service lié :

Ensuite, vous aurez un code JSON définissant le service lié, par exemple :

{
    "name": "AzureDataLakeStorage",
    "properties": {
        "annotations": [],
        "type": "AzureBlobFS",
        "typeProperties": {
            "url": "https://mondatalake.dfs.core.windows.net/",
            "accountKey": {
                "type": "AzureKeyVaultSecret",
                "store": {
                    "referenceName": "AzureKeyVault",
                    "type": "LinkedServiceReference"
                },
                "secretName": "adls-acess-key"
            }
        }
    }
}

Ce qui nous intéresse, c’est la clé typeProperties et ses enfants. Ce sont les propriétés que l’on va pouvoir variabiliser. Par exemple, secretName.

Rendez-vous ensuite dans la panneau de paramétrage du template :

Dans la partie surlignée en rouge, ajoutez la clé, ici secretName. Notez que si notre clé avait été imbriquée dans une autre clé, il aurait fallu ajouter le tout pour respecter l’arborescence.

Pipeline Build/Release

Comme vous le savez désormais, la phase de Build est réalisée manuellement par un clic sur le bouton Publish. Ce que nous souhaitons maintenant, c’est déployer nos ressources sur d’autres environnements. On considère que la partie de variabilisation précédente est faite, et que notre template ARM est ainsi prêt dans la branche adf_publish.

Workflow de déploiement ADF( doc )

Sur Azure DevOps, rendez-vous dans Pipelines -> Releases. Créez un nouveau pipeline. A terme, on cherche à accomplir le résultat suivant :

Votre Artifact doit être de type Azure Repository. Sélectionnez ensuite le projet, puis le repo dans lequel se situent les fichiers de l’ADF de dev. Sélectionnez ensuite la branche adf_publish. Il est possible de faire en sorte que votre pipeline de release se déclenche automatiquement à chaque nouveau commit sur la branche (donc à chaque Publish).

Nous allons ensuite ajouter deux stages, l’un pour la recette (staging), l’autre pour la production. Les deux seront identiques dans leur structure, seule la valeur des variables va changer. Créez donc d’abord celui de Staging, puis dupliquez le par la suite, en changeant simplement les variables utilisées (on y vient après).

A l’intérieur du stage, ajoutez une activité ARM Deployment. Sélectionnez les informations demandées puis le groupe de ressource dans lequel faire le déploiement (ici, celui de staging). Spécifiquement, c’est la partie Template et Override template parameters qui nous intéresse. On va remplacer les valeurs par défaut de nos paramètres de dev (d’où la partie sur la variabilisation pour qu’on puisse les surcharger). Vérifiez auparavant que le chemin pour template et template parameters sont corrects. En effet, ARM se compose toujours de deux fichiers : le fichier de templating et le fichier de paramètres.

Vous pourriez ici, si vous le souhaitiez, mettre directement les valeurs sans passer par des variables. Cependant, l’avantage des variables est de permettre de ne pas avoir à modifier ce fichier pour chaque environnement, seule la valeur de la variable changera. On utilise les groupes de variables à cette fin.

L’intérêt de créer un groupe de variables, plutôt que désigner directement des variables dans la pipeline de release, c’est qu’on peut créer un groupe de variables par environnement, contenant pour chacun les mêmes variables (même nom), et simplement les assigner chacun à leur stage respectif. Ainsi, nous n’avons quasiment rien à modifier lorsque nous dupliquons un stage.

Pour créer un groupe de variables, rendez-vous dans Pipelines -> Library.  Ici, nous avons créé un groupe par stage, et un autre groupe pour les variables statiques qui ne changent pas entre les environnements.

Ensuite, retournons dans notre pipeline de Release, partie Variable, puis Variable groups. Cliquez sur Link variable group, et ajoutez chaque groupe de variables créé précédemment, en l’assignant au bon stage (notion de scope).

Cela fait, nous pouvons retourner où nous en étions dans notre pipeline de Release (la partie Override template parameters de l’activité ARM Template Deployment) et référencer les variables avec cette syntaxe : $(variable).

ATTENTION : si je change des paramètres dans le fichier ARMTemplateParametersForFactory.json (suite au retrait de paramètres des services liés etc. et un nouveau Publish) et que je lance mon pipeline de release, j’aurai un message d’erreur du type : “Deployment template validation failed: The template parameters ‘BlaBla’ in the parameters file are not valid; they are not present in the original template and can therefore not be provided at deployment time...” Pourquoi ? Car dans la partie Override template parameters de la tâche ARM template deployment, les anciens paramètres y sont toujours présents, et il vous faudra donc les supprimer.

Il ne vous reste plus qu’à cliquer sur Create release et tester que le déploiement de vos ressources dans les différents environnements se passe comme prévu.

Au fur et à mesure de vos cycles de modifications et de déploiement, vous constaterez que les suppressions de composants (pipelines, services liés, datasets…) réalisées en dev ne sont pas répercutées dans les autres environnements. Cela est dû au mode de déploiement ARM de l’activité ARM template deployment de votre pipeline, qui est mis sur Incremental. Il existe diverses solutions (mettre le mode Complete à la place, ajouter l’activité Azure Data Factory Delete Items de cette extension, dans le pipeline de Release, utiliser un script Powershell), voir le sujet stackoverflow pour ce point.

Conclusion

Vous disposez désormais de bonnes techniques pour améliorer l’organisation, l’exécution et le déploiement de vos pipelines ! Pour aller plus loin, vous pouvez vous renseigner sur l’IaC avec Terraform et son intégration dans votre pipeline Azure DevOps, notamment à travers ce livre !