Ajouter des paramètres pour le déploiement du template ARM Azure Data Factory

La longueur du titre de cet article laisse présager du niveau de précision dans lequel nous allons nous lancer ! Je vais donc rapidement situer le contexte faisant appel à un tel besoin.

Nous cherchons à déployer de manière automatisée un environnement de développement Azure Data Factory sur l’environnement de production. Nous utilisons pour cela un pipeline de release Azure DevOps. Le processus DevOps pour ADF est expliqué dans ce livre blanc Microsoft ou dans cet excellent article de Florian Eiden.

Pour résumer les grandes lignes du fonctionnement, nous remplaçons dans un fichier de paramètres au format JSON les valeurs des chaînes de connexion de l’environnement de développement par celles de production. Ceci se fait au moyen de la case « override template parameters ».

Valeurs manquantes pour le service lié Databricks

La chose se complique lorsque nous utilisons un service lié de calcul de type Azure Databricks puisque les paramètres de ce service n’apparaissent pas par défaut dans le fichier de paramétrage ! Nous avons en particulier besoin de remplacer :

  • La Databricks Workspace URL
  • Le Secret name (où se trouve enregistré un Personal Access Token correspondant à l’espace de travail souhaité)

En effet, un choix arbitraire a été fait par Microsoft pour présenter uniquement certains paramètres mais un grand nombre de valeurs existent et elles sont visibles en éditant le fichier disponible dans le menu : Parametrization template.

Ce fichier au format JSON recense l’intégralité des paramètres disponibles par défaut, ainsi que leur visibilité au moyen d’une propriété qui n’est pas simple à interpréter :

= (signe égal) permet de conserver la valeur actuelle en tant que valeur par défaut pour le paramètre

– (signe moins) permet de ne pas conserver la valeur par défaut pour le paramètre

Le symbole | (pipe) permet de réaliser le lien avec une valeur stockée dans le coffre-fort Azure Key Vault.

Nous allons maintenant rechercher le nom des paramètres manquants. Pour cela, nous passons par la définition du service lié Databricks au format JSON.

Nous obtenons ainsi le nom exact des paramètres (ne pas se fier à l’interface graphique).

L’URL de l’espace de travail se nomme ainsi domain, le secret du Key Vault se désigne par secretName et ses deux propriétés dépendent du niveau TypeProperties et du sous-niveau accessToken pour le secret.

Nous pouvons donc maintenant citer ces propriétés dans le JSON des paramètres, en respectant l’arborescence des propriétés.

La définition des paramètres dans la branche Master

Arrive ici la partie peut-être la moins documentée (jusqu’à présent !). Il faut comprendre que ce fichier JSON doit figurer à la racine de la branche Master du dépôt contenant la synchronisation du Data Factory avec un gestionnaire de version comme GitHub ou un repo Azure DevOps, sous le nom arm-template-parameters-definition.json.

Il ne faut donc pas utiliser la branche spécifique à Data Factory qui se nomme adf_publish mais nous irons ensuite vérifier que les deux fichiers JSON qu’elle contient ont bien été modifiés en conséquence.

Pour lancer cette modification, nous devons tout d’abord faire les deux actions suivantes :

  • Refresh
  • Publish

Le volet latéral du Publish (pending changes) doit s’afficher même si aucune modification n’est visible.

Nous pouvons enfin vérifier que les paramètres sont disponibles dans le fichier ARMTemplateParametersForFactory.json de la branche adf_publish.

Modifier la valeur des paramètres pendant la release

Il ne reste qu’à utiliser ces noms de paramètres dans le pipeline de release Azure DevOps. Les nouvelles valeurs peuvent être définies comme des variables du pipeline pour plus de commodité.

Cet article a pu être écrit grâce à la documentation officielle de Microsoft ainsi que d’autres articles de blog, en anglais, que leurs auteurs en soient ici remerciés :

https://medium.com/@sanajitghosh/manage-ci-cd-pipelines-using-azure-devops-azure-data-factory-azure-databricks-8239a9ceef3

https://www.modern-dataengineering.com/post/how-to-add-custom-parameters-to-data-factory-templates

https://medium.com/@patrickpicard_50914/azure-data-factory-modifying-arm-template-parameters-53b11f38bced

Relancer un notebook Databricks en cas d’échec

Le code utilisé dans un notebook peut échouer pour une raison autre qu’un erreur de développement : fichier absent, API ne répondant pas, etc. Il peut donc être pertinent de relancer automatiquement un traitement en cas d’échec.

La documentation Databricks fournit un exemple de fonction, en Python ou en Scala, qui réalise ce mécanisme. Le code est bien sûr basé sur la fonction dbutils.notebook.run déjà présentée dans un précédent post.

Voici le code en Python, où un nombre d’essais maximum de 3 est paramétré par défaut :

 # Errors in workflows thrown a WorkflowException. 
def run_with_retry(notebook, timeout, args = {}, max_retries = 3):
   num_retries = 0
   while True:
     try:
       return dbutils.notebook.run(notebook, timeout, args)
     except Exception as e:
       if num_retries > max_retries:
         raise e
       else:
         print "Retrying error", e
         num_retries += 1

Et voici ce que l’on obtient à l’exécution. Attention à bien préciser le chemin relatif du notebook ainsi piloté, si celui-ci n’est pas situé au même niveau que le ce “master notebook”.

Attention à ne pas abuser de ce processus ! Il est essentiel de comprendre la nature des erreurs rencontrées et d’y apporter des réponses au travers du code.

Utiliser les variables d’environnement pour faciliter le déploiement continu des notebooks Databricks

Une fois l’infrastructure définie autour d’un cluster Databricks, les notebooks sont les éléments qui vont évoluer au gré des développements. Il faut bien sûr a minima définir deux environnements : l’un de développement, l’autre de production. Nous verrons ainsi plusieurs astuces et bonnes pratiques permettant de réaliser le processus du déploiement continu des notebooks.

Nous avons pu voir dans de précédents articles :

  • Comment définir un point de montage vers un compte de stockage Azure
  • Comment versionner les notebooks Databricks par exemple sous GitHub

Nous allons utiliser ici la notion de variable d’environnement, propre au cluster Spark.

Le schéma ci-dessous illustre le mécanisme DevOps qui sera mis en place.

Mais pour l’instant, focalisons-nous sur le chemin menant vers les données. Nous utilisons ici deux environnements identiques d’un point de vue de l’architecture, dont une vision simplifiée est donnée sur le schéma ci-dessous :

L’accès au point de montage défini sur le compte de stockage Azure se fait par exemple au moyen des commandes Databricks dbutils :

dbutils.fs.ls('/mnt/dev/mysfilesystem/')

Les variables d’environnement sont quant à elles définies au niveau d’un cluster. Elles seront donc accessibles de n’importe quel notebook attaché au cluster. Nous les trouvons en dépliant le menu des options avancées, onglet Spark.

Par convention, nous utilisons une casse majuscule pour le nom des variables.

Attention à ne pas mettre d’espace autour du signe « = ». Les guillemets ne sont en revanche pas indispensables autour de la valeur de la variable.

Un redémarrage du cluster sera alors nécessaire, suite à la modification des variables d’environnement.

Maintenant, différentes commandes, dans les langages supportés, vont nous permettre d’accéder aux variables définies. Pour la compatibilité dans un même notebook, les lignes de scripts seront ici précédées du langage dans lequel elles sont écrites, vous pourrez ainsi copier ce code tel quel dans n’importe quel notebook Databricks.

Liste des variables d’environnement :

%sh printenv

Valeur de la variable en Shell :

%sh echo $MOUNT_PATH

Valeur de la variable d’environnement en Python :

%python

import os

key = 'MOUNT_PATH'
value = os.getenv(key)

print("Value of 'MOUNT_PATH' environment variable :", value)

A noter que la commande getenv() peut être remplacée par environ.get() issue également de la librairie os. Les différences entre les deux sont traitées dans cette question sur StackOverFlow.

Valeur de la variable d’environnement en Scala :

%scala
sys.env("MOUNT_PATH")

Il est donc maintenant possible d’utiliser ces variables dans les chaînes d’accès au système de fichier, avec un code qui réagira alors en fonction de l’environnement !

%python
dbutils.fs.ls(os.getenv('MOUNT_PATH') + '/mysfilesystem/')

La définition des variables d’environnement est également réalisable si vous utilisez un “automated cluster“, c’est-à-dire un cluster créé à la volée lors du lancement d’un job planifié.

La configuration du cluster est disponible en cliquant sur le bouton “edit”.

Ne laissez pas vos access keys dans vos notebooks !

Dans une architecture complète Cloud tout en services managés, Azure Databricks est extrêmement efficace pour se connecter aux données des comptes de stockage Azure : Blob Storage, Data Lake Store gen1 ou gen2.

Pour autant, il n’est pas raisonnable d’utiliser la solution de facilité et de se connecter au travers des commandes suivantes (exemple en Python) :

storage_account = "<nom de votre ressource Azure Storage>"
container = "<nom du container souhaité>"
storage_account_access_key = <"iciuneclévisiblecequilnefautjamaisfaire!">
dbutils.fs.mount(
  source = "wasbs://"+container+"@"+storage_account+".blob.core.windows.net",
  mount_point = "/mnt/"+container,
  extra_configs = { "fs.azure.account.key."+storage_account+".blob.core.windows.net":  storage_account_access_key })

La procédure permettant de cacher les informations de sécurité est relativement simple, la voici en détails.

Databricks CLI

Le principe est de créer des secret scopes, au moyen de l’interface de lignes de commandes (CLI) de Databricks. Celle-ci s’obtient au travers de l’installation d’un package python.

pip install databricks-cli
Installation du package databricks-cli (ici dans un virtualenv dédié)

Pour une première utilisation, il est nécessaire d’associer l’espace de travail Azure Databricks avec le poste où sera exécuté le CLI. Une fois le package installé, saisir la commande suivante :

databricks configure --token

Deux informations sont alors attendues : l’URL du service managé puis un jeton d’identification préalablement créé depuis l’espace de travail Databricks, à partir du menu Users settings > Access tokens.

Générer un token d’accès pour Databricks CLI

Les commandes du package sont maintenant en interaction avec la ressource Databricks, en voici quelques exemples :

databricks workspace ls
databricks clusters spark-versions
databricks fs ls dbfs:/delta/
Quelques commandes du CLI Databricks

Créer un secret scope

Passons maintenant aux commandes qui définiront le secret scope (un scope peut contenir plusieurs informations secrète).

databricks secrets create-scope --scope scopeAzStorage
databricks secrets put --scope  scopeAzStorage  --key accessKeyAzStorage

La seconde commande ouvre alors un éditeur de texte, type VI, dans lequel on copiera par exemple l’access key d’un Azure Blob Storage.

Créer (proprement) un point de montage

Voici maintenant le code Python que l’on exécutera dans un notebook pour définir un point de montage, c’est-à-dire un accès simplifié aux fichiers contenu sur un compte de stockage Azure, ici en reprenant le nom du container dans le chemin d’accès.

dbutils.fs.mount(
  source = "wasbs://"+container+"@"+storage_account+".blob.core.windows.net/",
  mountPoint = "/mnt/"+container,
  extra_configs = { "fs.azure.account.key."+storage_account+".blob.core.windows.net" :dbutils.secrets.get(scope = "scopeAzStorage" , key = "accessKeyAzStorage" )}) 

A vous maintenant les commandes magiques… et ceci, en toute sécurité !

%fs ls /mnt/

(Cet article détaille par écrit la vidéo disponible ici.)

Versionning des notebooks sous Azure Databricks

A l’aide de GitHub

Comme pour tout développement, les notebooks méritent d’être archivés et versionnés. Tout notebook sera ainsi automatiquement sauvegardé et versionné dans l’espace de travail Azure Databricks (voir la documentation officielle).

Menu Revision history du notebok

Tant que le menu latéral Revision history est visible, il n’est pas possible de modifier le contenu du notebook.

Azure Databricks permet également d’utiliser un gestionnaire de versions externe parmi les trois solutions suivantes :

  • GitHub
  • Bitbucked Cloud
  • Azure DevOps Service

Dans un même espace de travail, il ne sera possible d’associer qu’un seul des trois gestionnaires (mais il serait sans doute étrange de versionner à différents endroits…). Notons que GitLab ne fait pas partie de cette liste, à ce jour, je n’ai pas réussi à le lier à Azure Databricks. Ce n’est pas le cas non plus de la version Enterprise de GitHub.

Rappel des notions et principes de base de Git

repository : c’est le répertoire de dépôt d’un projet de développement
master : version initiale et de référence du code
branch : lors de la suite des développements, il est important créer une nouvelle branche pour ne pas dégrager le master
commit : envoi de la liste des modifications effectuées
pull request : demande de prise en compte de modifications réalisées par un autre développeur
merge : appliquer les modifications à une autre branche, souvent le master

Nous allons découvrir maintenant comment se fait le lien entre l’espace de travail Azure Databricks et GitHub. Il faut tout d’abord se rendre sur la page dédiée aux paramètres de l’utilisateur (User Settings).

Paramétrage de l’intégration Git

Depuis le site GitHub, une fois identifié, il faut créer un jeton d’accès personnel, en suivant les écrans ci-dessous. Celui-ci devra disposer des droits complets sur le repo.

Génération d’un jeton d’accès personnel dans GitHub
Accorder le contrôle complet des repositories
Association réalisée avec succès

Nous pouvons maintenant quitter la page des paramètres de l’utilisateur pour nous rendre dans le notebook de notre choix. Le menu Revision history laisse apparaître le lien Git: Synced.

Association du notebook avec un repo GitHub
Enregistrement d’une première version
Première synchronisation réussie

Le fichier est maintenant bien créé sur notre compte GitHub dans le repo associé. Chaque nouvelle révision pourra être enregistrée et commitée, en associant un commentaire.

Enregistrement (et commit) d’un révision

Par défaut un notebook python est enregistré au format .py. Les commandes magiques ne sont pas perdues et seront correctement réinterprétées à l’import du fichier sur un autre espace de travail. Afin de converser les propriétés d’affichage du notebook dans GitHub, il suffit de forcer l’extention à .ipynb lors de la première synchronisation.

Ainsi, chaque nouvelle sauvegarde se fait donc sur la branche principale (master) mais il est bien sûr possible de créer de nouvelles branches du développement, en cliquant à nouveau sut Git: synced.

création et sélection d’une nouvelle branche

La création d’une nouvelle branche fait apparaître un hyperlien vers la pull request sur le compte GitHub.


Lien vers la pull request

La comparaison des modifications et l’éventuel merge des versions se fait ensuite sur la page GitHub.

Comparaison des modifications sous GitHub

Rappelons enfin qu’il est possible d’importer un fichier par son URL, et donc par l’URL obtenue depuis GitHub. Cette fonctionnalité, couplée à l’utilisation des paramètres dans un notebook, permet de recopier le notebook d’un environnement de développement à un environnement de production.


Import d’un fichier dans l’espace de travail
Import par URL

Dans un prochain article, nous explorerons les interactions entre Azure Databricks et Azure DevOps.