Déploiement continu d’Azure Data Factory et Azure DevOps

Azure Data Factory est un service managé du cloud Azure, de type “low code”, aussi bien utile pour ses fonctionnalités d’ETL/ELT que d’ordonnanceur. On appréciera particulièrement d’y intégrer toute une chaîne de traitements et d’avoir ainsi un espace centralisé de lecture des logs d’exécution.

Si ce service commence à prendre de l’importance dans vos projets data, vous souhaiterez certainement développer dans une ressource qui n’est celle de production. Se posera donc alors la question du déploiement continu entre ressources.

Lier Data Factory à un repository

Avant de déployer, il est bien sûr nécessaire de versionner les développements ! Cela sera possible dans l’un des deux outils du monde Microsoft : GitHub ou Azure DevOps. Il est possible de définir le repository à la création de la ressource mais nous pouvons le faire, plus facilement, après l’instanciation.

A la première connexion au Studio Data Factory, nous configurons le repository, en choisissant ici une organisation et un projet Azure DevOps. Il n’est pas nécessaire d’importer le contenu de ce repository qui doit être vide pour l’instant.

Décocher la case “Import existing resource”

Notons tout de suite un fonctionnement propre à Data Factory que sera le travail sur deux branches :

  • une branche principale (nommée ici main), de collaboration
  • une branche de “publication” au nom réservé : adf_publish

Nous verrons lors du déploiement le rôle joué par cette seconde branche. Nous terminons la configuration en définissant la branche main comme la branche de travail.

Il sera possible par la suite de créer des branches apportant de nouvelles fonctionnalités ou réalisant des corrections puis d’effectuer des opérations de merge (fusion) avec la branche principale.

Terminons cette première partie sur une notion fondamentale : les environnements de destination lors du déploiement (pré-production, production…) ne doivent pas être liés au repository. C’est en effet une opération spécifique (un pipeline de release) qui viendra déposer le code, mais ce dernier ne doit pas pouvoir être modifié en dehors de l’environnement de développement.

Créer un premier service lié

Les services liés sont les premiers éléments à créer dans Data Factory car ils “hébergent” les datasets ou représentent une ressource de calcul comme c’est le cas pour le service de clusters Spark managé Azure Databricks. Ce sont aussi les services liés qui sont les plus dépendants des environnements (dev, uat, prod…) car ils pointent vers d’autres ressources auprès desquelles une authentification est bien souvent nécessaire.

Voici une bonne pratique : ne pas utiliser un nom spécifique à l’environnement (par exemple ls_DatabricksDev) car ce nom doit refléter la ressource dans chaque environnement et nous ne pourrons pas le modifier.

En revanche, plusieurs propriétés ainsi que l’authentification devront être modifiées lors du déploiement entre environnements.

Pour un service lié Databricks, nous choisissons de nous authentifier à l’aide d’une identité managée, qu’il faudra déclarer en tant que “Contributor” au niveau de l’access control (IAM) de la ressource Databricks.

Pour vérifier que l’authentification est bien réalisée, nous déroulons les listes “cluster version” et “cluster node type”, remplacées par un message “failed” si la connexion n’est pas valide.

Il s’agit là d’une autre bonne pratique : privilégier les identités managées car celles-ci ont un périmètre très bien défini, appartiennent à l’annuaire Azure Active Directory et ne demanderont pas de manipulation lors du déploiement continu, à l’inversion d’un token ou d’un password.

Lors de la création ou de la modification d’éléments dans le Studio Data Factory, nous pouvons sauvegarder ces changements à tout moment puis réaliser une publication à l’aide du bouton “Publish”.

L’action Publish correspond à un “commit – push” sur le repository lié.

Nous retrouvons dans le repository les deux branches évoquées lors de l’association à Azure DevOps.

La branche adf_publish contient en particulier deux fichiers nommés respectivement ARMTemplateForFactory.json et ARMTemplateParametersForFactory.json. Ceux-ci contiennent les noms propres à l’environnement et qui devront donc être remplacés lors du déploiement.

Le nom de la ressource ADF est le premier paramètre propre à l’environnement.

Afin de pousser la démonstration, nous ajoutons et démarrons un trigger dans le Studio Data Factory (nous verrons son importance un peu plus tard dans cet article).

Déployer par pipeline de release

Le pipeline de release convient à ce que nous cherchons à faire : déployer un environnement vers un autre, à partir d’un code versionné.

Nous démarrons le pipeline avec un “empty job”.

La première chose à faire est de lier l’artefact (le code versionné) au pipeline. Attention, c’est bien la branche main et non adf_publish qui est attendue ici.

La branche par défaut est la branche main.

Passons ensuite au paramétrage du stage, qui pourra être renommé par le nom de l’environnement cible (uat, prod…).

Nous recherchons dans la Market Place les tâches correspondant à Data Factory. Une installation sera nécessaire à la première utilisation.

Je vous recommande le produit développé par SQLPlayer. En effet, nous allons voir de multiples avantages (et un inconvénient…).

Autoriser le composant à accéder à une ressource Azure va demander de créer une service connection. Des droits suffisants sur Azure sont nécessaires pour réaliser cette opération.

Dans la boîte de dialogue ci-dessous, les champs attendus concernent l’environnement cible :

  • nom du groupe de ressource
  • nom de la ressource Azure Data Factory
  • chemin vers le répertoire des fichiers JSON (branche adf_publish)
  • nom de la région où est située la ressource
Le fichier de configuration sera précisé plus tard.

Pour indiquer le path du dossier contenant les fichiers du template ARM, nous utilisons une nouvelle boîte de dialogue, en prenant soin de descendre un cran au-dessous du repository.

Veillez également à cocher la case “Stop/Start triggers”. En effet, le déploiement ARM ne pourra avoir lieu si des triggers sont actifs. C’est ici un avantage de cette extension : il n’est pas nécessaire d’ajouter par exemple un script PowerShell réalisant le stop puis le start. Une limite de l’approche par PowerShell consiste dans le fait que l’intégralité des triggers sont redémarrés. Il serait donc nécessaire de supprimer les triggers qui ne sont pas utilisés, plutôt que de les mettre en pause.

A ce stade, une première release pourrait être lancée et elle déploiera à l’identique l’environnement de développement.

Modifier les paramètres propres à l’environnement

Nous avons évoqué ci-dessus les fichiers JSON de la branche adf_publish. Ceux-ci permettent de réaliser un déploiement de template ARM (Azure Resource Manager), qui correspond à une approche dite Infra as Code, propre à l’univers Azure de Microsoft.

Pour déployer les développements vers un autre environnement, nous devons remplacer certaines valeurs :

  • nom de la ressource Data Factory
  • chaines de connexion
  • identifiant et mot de passe
  • URL
  • certains paramétrages spécifiques (ex.: type et dimension d’un cluster Databricks)

Nous allons ici préciser toutes les modifications à apporter au moyen d’un fichier CSV qui sera stocké sur la branche main du repository, dans un répertoire deployment et nommé config-{stage}.csv où la valeur de stage indique l’environnement cible.

Voici un exemple de fichier CSV qui transforme les informations nécessaires pour changer de workspace Azure Databricks. Les noms de colonnes en première ligne doivent être respectés.

type,name,path,value
linkedService,AzureDatabricks,typeProperties.domain,"https://adb-xxx.x.azuredatabricks.net"
linkedService,AzureDatabricks,typeProperties.workspaceResourceId,"/subscriptions/xxx-xxx-xxx-xxx/resourceGroups/rg-dbx-prd/providers/Microsoft.Databricks/workspaces/dbxprd"

Une documentation complète, sur la page de l’extension, explique comment renseigner ce fichier. Nous serons particulièrement vigilants quant à la gestion des secrets (tokens, mots de passe, etc.). Une astuce pourra être d’utiliser des variables d’environnement dans ce fichier, elles-mêmes sécurisées par Azure DevOps. Une meilleure pratique consistera à utiliser un coffre-fort de secrets Azure Key Vault, lui-même déclaré comme un service lié.

EDIT : en cas d’utilisation d’un firewall sur la ressource Azure Key Vault, il sera nécessaire d’ajouter l’IP de l’agent DevOps à ce firewall. Or, cette adresse IP n’est pas fixe.

Nous pouvons maintenant lancer la release.

Nous vérifions enfin dans le Studio Data Factory que les éléments développés sont bien présents, en particulier les triggers. Ceux-ci sont alors dans l’état “démarré” ou “arrêté” de l’environnement de développement.

Cette approche pourrait être perturbante si tous les triggers de développement sont arrêtés (les faire tourner ne se justifie sans doute pas). Mais nous avons bénéficier d’une autre fonctionnalité pour améliorer notre déploiement.

Réaliser un déploiement sélectif

Une autre option de l’extension sera particulièrement intéressante : le déploiement sélectif. Une case de la boîte de dialogue permet de déclarer la liste des objets que l’on souhaite ou non déployer. La syntaxe est explicité sur la page GitHub de l’extension.

Par exemple, nous retirons le déploiement d’un autre service lié, qui ne sera pas utile en production.

Dans une architecture plus complète, intégrant un Data Lake, nous pouvons obtenir le schéma suivant. Chaque cluster Databricks disposera d’un secret scope, lié à un coffre-fort Azure Key Vault. Celui-ci contiendra la définition d’un principal de service (client ID et client secret) permettant de définir un point de montage vers la ressource Azure Data Lake gen2 de l’environnement.

Pour remplacer certains paramètres avancés des services liés, cet article pourra servir de ressource.

Webhooks pour MLFlow Registry

Une nouvelle fonctionnalité est disponible dans Databricks, en public preview, depuis février 2022 : les webhooks liés aux événements MLFlow. Il s’agit de lancer une action sur un autre service, déclenchable par API REST, lors d’un changement intervenant sur un modèle stocké dans MLFlow Registry (changement d’état, commentaire, etc.).

Cette fonctionnalité n’a rien d’anecdotique car elle vient mettre le liant nécessaire entre différents outils qui permettront de parfaire une approche MLOps. Mais avant d’évoquer un scénario concret d’utilisation, revenons sur un point de l’interface MLFlow Registry qui peut poser question. Nous disposons dans cette interface d’un moyen de faire évoluer le “stage” d’un modèle entre différentes valeurs :

  • None
  • Staging
  • Production
  • Archived

Ces valeurs résument le cycle de vie d’un modèle de Machine Learning. Mais “dans la vraie vie”, les étapes de staging et de production se réalisent sur des environnements distincts. Les pratiques DevOps de CI/CD viennent assurer le passage d’un environnement à un autre.

MLFlow étant intégré à un workspace Azure Databricks, ce service se retrouve lié à un environnement. Bien sûr, il existe quelques pistes pour travailler sur plusieurs environnements :

  • utiliser un workspace Azure Databricks uniquement dédié aux interactions avec MLFlow (en définissant les propriétés tracking_uri et registry_uri)
  • exporter des experiments MLFlow Tracking puis enregistrer les modèles sur un autre environnement comme décrit dans cet article

Aucune de ces deux approches n’est réellement satisfaisante car manquant d’automatisation. C’est justement sur cet aspect que les webhooks vont être d’une grande utilité.

Déclencher un pipeline de release

Dans Azure DevOps, l’opération correspondant au déploiement sur un autre environnement se nomme “release pipeline“.

Pour simplifier la démonstration, nous définissions ici une pipeline contenant uniquement un tâche de type bash, réalisant un “hello world”.

Dans un cas d’utilisation plus réalise, nous pourrons par exemple déployer un service web prédictif contenant un modèle passé en production, grâce à une ressource comme Azure Machine Learning.

Nous vérifierons par la suite qu’une modification dans MLFlow Registry déclenche bien une nouvelle release de cette pipeline.

Précisons ici un point important : les webhooks Databricks attendent une URL dont l’appel sera fait par la méthode POST. Il existe une API REST d’Azure DevOps qui permettrait de piloter la release mais le paramétrage de celle-ci s’avère complexe et verbeux.

Nous passons donc un ordonnanceur intermédiaire : Azure Logic App. En effet, cette ressource Azure nous permettra d’imaginer des scénarios plus poussés et bénéficie également d’une très bonne intégration avec Azure DevOps. Nous définissons le workflow ci-dessous comprenant deux étapes :

  • When a HTTP request is received
  • Create a new release
Veillez à bien sélectionner la méthode POST.

Le workflow nous fournit alors une URL que nous utiliserons à l’intérieur du webhook. Il ne nous reste plus qu’à définir celui-ci dans Databricks. La documentation officielle des webhooks se trouve sur ce lien.

Il existe deux manières d’utiliser les webhooks : par l’API REST de Databricks ou bien par un package Python. Nous installons cette librairie databricks-registry-webhooks sur le cluster.

Depuis un nouveau notebook, nous pouvons créer le webhook, tout d’abord dans un statut “TEST_MODE”, en l’associant à un modèle déjà présent dans MLFlow Registry.

Ceci nous permet de tester un appel par la commande .test_webhook() qui attend en paramètre l’identifiant du webhook préalablement créé. Une réponse 202 nous indique que cette URL est acceptée (bannissez les réponses 305, 403, 404, etc.).

Le webhook peut se déclencher sur les événements suivants :

  • MODEL_VERSION_CREATED
  • MODEL_VERSION_TRANSITIONED_STAGE
  • TRANSITION_REQUEST_CREATED
  • COMMENT_CREATED
  • MODEL_VERSION_TAG_SET
  • REGISTERED_MODEL_CREATED

Le webhook peut ensuite être mis à jour au statut ACTIVE par la commande ci-dessous.

Il ne reste plus qu’à réaliser une des actions sélectionnées sur le modèle stocké dans MLFlow Registry et automatiquement, une nouvelle release Azure DevOps se déclenchera.

Maintenant, il n’y a plus qu’à laisser libre cours à vos scénarios les plus automatisés !

Ci-dessous, le schéma résumant les différentes interactions mises en œuvre.

Synchroniser un groupe Azure AD avec un groupe Databricks

Si vous travaillez à plusieurs sur Azure Databricks, vous vous êtes sûrement déjà confronté.e.s à des réflexions sur les droits à accorder à chacun.e, selon les profils : data analyst, data scientist, data engineer, ML engineer… Rappelons qu’une gestion fine des droits ne sera possible qu’en licence dite Premium.

Il est évident qu’un administrateur de l’espace de travail ne souhaitera pas gérer des autorisations nominatives et ainsi, devoir reprendre la gestion à droits à chaque arrivée ou départ sur un projet ! Nous souhaitons donc nous référer à des groupes et c’est justement une notion bien intégrée dans l’annuaire Azure Active Directory (AAD).

Databricks dispose également d’une gestion de groupes d’utilisateurs dans sa console d’administration.

Malheureusement, il n’est pas possible d’utiliser un alias de groupe AAD dans Databricks et ces deux notions de groupes ne sont pas synchronisées entre elles !

Nous pourrions aussi imaginer un déploiement Terraform qui viendrait décortiquer un groupe AD et ajouter chaque utilisateur au moyen d’une boucle (voir ce post Medium) mais cette démarche semble vraiment trop lourde à mettre en œuvre et à maintenir…

Un outil vient à notre secours : Azure Databricks SCIM Provisioning Connector et c’est mon collègue Hamza BACHAR qui me l’a soufflé.

Nous commençons par enregistrer cet outil en tant qu’enterprise application dans Azure AD.

Il s’agit d’un produit développé par la société Databricks Inc, ce qui est plutôt rassurant.

Il est recommandé de renommer l’application en intégrant le nom de la ressource Databricks car il faudra autant d’enregistrements d’applications que de workspaces.

L’application est maintenant enregistrée dans notre abonnement Azure et dispose de deux identifiants : application ID et object ID.

Nous cliquons maintenant sur le menu Provisioning qui va nous permettre d’associer l’application avec un workspace Databricks. Le mode de provisioning doit être basculé sur Automatic.

Nous devons fournir ici deux informations :

  • l’URL du workspace à laquelle sera ajoutée le point de terminaison de l’API SCIM
  • un Personal Access Token qui aura été généré depuis les user settings de Databricks

Nous testons la connexion puis clic sur “Save” pour conserver le paramétrage.

Nous laissons par défaut les autres options disponibles.

Le provisioning peut maintenant être démarré.

Il ne reste plus qu’à positionner des users ou groups directement dans cette application pour qu’ils soient automatiquement synchronisés avec la console d’administration Databricks ! Il est toutefois nécessaire d’attendre quelques minutes (cela peut aller jusqu’à 40 minutes). On privilégiera les “groupes de sécurité” plutôt que de type “M365” et les “nested groups” ne sont pas aujourd’hui supportés.

Il est aussi possible de “provisionner à la demande” un utilisateur.

Un écran vient ensuite confirmer que l’action a bien été réalisée et l’utilisateur est directement ajouté au workspace.

A noter que le nouvel utilisateur est restreint sur ses droits, par défaut.

Il n’est pas administrateur du workspace et se trouve restreint sur la création de nouveaux clusters.

EDIT

Il est nécessaire qu’une personne ayant le droit d’enregistrement d’applications réalise la première manipulation. Mais ensuite, d’autres personnes pourront avoir besoin d’utiliser cette interface.

Nous allons donc autoriser d’autres utilisateurs au “self-service” de cette application. Il faut tout d’abord activer le “single sign-on”, en mode “linked”.

Fournir pour cela l’URL de connexion.

On paramètre enfin le menu Self-service.

Si l’on souhaite plutôt ajouter des users ou des groups de manière programmatique, on pourra se pencher sur ce code PowerShell.