Revenir au contenu principal
Produit

Créer des branches de bases de données comme du code : un modèle CI/CD pour Lakebase, en production chez Glaspoort

Comment Glaspoort déploie ses modifications de bases de données avec la même rigueur que son code applicatif, en créant des branches depuis la production, des bases éphémères par PR et en traitant les migrations comme source unique de vérité.

par Hadi Farhat, Gideon Spierings, Ricardo de Vries et Raymon Veldman

  • Comment Glaspoort a conçu sa configuration de branching Lakebase, avec le développement et la recette tous deux branchés directement depuis la production au lieu d'être superposés, afin d'éviter le « piège de la réinitialisation depuis le parent » où le rafraîchissement d'un environnement obsolète vous oblige à tout supprimer et reconstruire en dessous.
  • Le flux CI/CD par PR : comment chaque pull request obtient sa propre branche Lakebase fraîche et éphémère copiée depuis la production, comment les migrations sont rejouées et testées par rapport à une image d'application en direct avant que quoi que ce soit ne touche un environnement réel, et pourquoi les migrations elles-mêmes (et non les bases de données) sont traitées comme la source unique de vérité.
  • Le compromis entre deux modèles de promotion : fusionner dès que la CI réussit versus fusionner uniquement après qu'une PR a été entièrement promue via la recette. L'article explique pourquoi Glaspoort a choisi l'approche privilégiant la rapidité, et les garde-fous ajoutés (revalidation de la pile et pipeline de crise) pour garantir la sécurité.

Le problème que nous ne pouvions plus ignorer

Glaspoort construit et exploite des infrastructures de fibre optique aux Pays-Bas. Tout tourne autour de l'augmentation du nombre de connexions fibre, et pendant longtemps, l'équipe data a passé ses journées à créer des rapports BI pour soutenir cet objectif, alors même que la question à l'origine de chaque rapport était déjà dépassée au moment où celui-ci était finalisé. Le résultat était une prolifération de rapports ponctuels et des utilisateurs qui ne savaient pas vers qui se tourner pour leurs questions de suivi.

Nous avons donc rompu cette dépendance. Au lieu de livrer un énième rapport, nous avons développé une application front-end personnalisée dans laquelle les chefs de projet voient directement où se situent les opportunités pour leurs projets. La nouveauté se trouve sous le capot : nous utilisons directement les produits Databricks comme briques de construction dans l'application. Genie, pour dialoguer avec les données et lancer des analyses rapides. Les tableaux de bord AI/BI Dashboards, pour obtenir des insights et des analyses en libre-service. Des workflows automatisés avec Agent Bricks, qui alertent les chefs de projet dès que quelque chose sort de l'ordinaire sur leurs projets, et Lakebase, la base de données OLTP de Databricks, pour les données transactionnelles de l'application.

Cette combinaison rapproche deux mondes qui, jusqu'à récemment, vivaient séparés : l'environnement analytique et un front-end opérationnel, où les données analytiques rencontrent les données transactionnelles. L'équipe data consacre désormais son temps aux espaces Genie et aux métadonnées plutôt qu'à des rapports ponctuels. Mais rien de tout cela ne reste performant sans des fondations solides : CI/CD, tests de qualité des données, Infrastructure as Code et gouvernance des données. L'un des éléments de cette fondation a nécessité la conception la plus minutieuse, et c'est de cet élément dont traite la suite de cette histoire : comment nous déployons les modifications de la base de données qui alimente l'application.

Derrière cette application se trouve une base de données Databricks Lakebase. Il s'agit d'un Postgres OLTP serverless, qui s'exécute à côté du lakehouse plutôt que d'y être greffé. Le flux de données est simple à décrire et, comme nous l'avons découvert, plus intéressant à exploiter qu'il n'y paraît :

  • Les données préparées du lakehouse sont synchronisées dans une branche de production Lakebase, où elles atterrissent dans un schéma d'application en lecture seule.
  • L'application réécrit son propre état dans un schéma distinct sur cette même branche, de sorte que les données lues depuis le lakehouse et les données écrites par l'application cohabitent sans conflit.
  • En plus de cela, nous exploitons trois environnements logiques : développement, recette et production. Nous déployons les modifications de cette base de données de la même manière que pour le code de l'application, via des pull requests, de la CI et des promotions contrôlées.

image3.png

C'est sur ce dernier point que réside la question la plus intéressante. Dès lors que vous décidez qu'une base de données OLTP mérite la même rigueur que le code d'une application, vous devez répondre à une question difficile :

Comment permettre à chaque PR d'effectuer des tests sur une base de données semblable à celle de production, sans que les collaborateurs ne se marchent sur les pieds, et sans perdre les données récentes qui donnent tout leur sens aux tests ?

Les modes de défaillance ici sont familiers pour quiconque a déjà partagé une base de données au sein d'une équipe. Des PR qui réussissent de manière isolée et échouent lorsqu'elles sont fusionnées. Des environnements de développement et de recette qui se sont discrètement éloignés de la réalité de la production. Et le jour de la mise à jour que personne ne souhaite, où récupérer des données propres implique de démanteler les environnements, de reconfigurer chaque chaîne de connexion et de réappliquer manuellement chaque droit d'accès.

Cet article explique comment nous avons évité la majeure partie de ces désagréments, et présente la décision dont nous débattons encore activement.

La gestion des branches Lakebase, en 60 secondes

Si vous n'avez pas encore utilisé les branches Lakebase, voici le seul modèle mental dont vous aurez besoin pour cet article.

Une branche Lakebase est une branche Postgres de type copy-on-write dérivée d'un parent. En créer une ne copie pas les données ; cela duplique l'état instantanément et à moindre coût, et la branche ne diverge de son parent qu'au fur et à mesure que vous y écrivez. Chaque branche est entièrement isolée, avec son propre point de terminaison et ses propres données. Vous pouvez en créer une en quelques secondes et la supprimer tout aussi rapidement.

Si cela ressemble à Git, c'est tout à fait le but. L'analogie qui structure tout ce qui suit est simple : une branche de fonctionnalité (feature branch) dans Git correspond à une branche de base de données dans Lakebase. Une PR obtient sa propre branche de code et sa propre branche de base de données, les tests sont exécutés sur les deux, et lorsque le travail est validé, il est promu vers la production.

Il y a une contrainte à garder à l'esprit pour la section suivante, car elle constitue une différence essentielle avec le branching Git. Pour réinitialiser une branche à partir de son parent, vous devez d'abord supprimer les propres enfants de cette branche. Un parent ne peut pas être réinitialisé si des branches en dépendent.

Le piège de la réinitialisation depuis le parent

Voici l'architecture vers laquelle la plupart des équipes se tournent en premier, car elle reflète la façon dont nous dessinons les environnements sur un tableau blanc :

  • Une seule branche de développement à longue durée de vie dérivée de la production.
  • Une branche de recette dérivée du développement.
  • Des branches de fonctionnalités dérivées du développement.

C'est une hiérarchie claire : la production à la racine, puis le développement, et enfin la recette et les fonctionnalités qui en découlent. Elle introduit également deux modes de défaillance prévisibles : les branches dérivent, et la correction de cette dérive est si coûteuse que les équipes finissent par y renoncer.

Tout d'abord, le développement et la recette dérivent de la production. La production continue de recevoir des données synchronisées récentes et de véritables écritures d'application ; ce n'est pas le cas des branches de développement et de recette à longue durée de vie. En un sprint ou deux, vous vous retrouvez à tester sur une base de données qui ne ressemble plus du tout à celle vers laquelle vous déployez.

La solution évidente consiste à actualiser le développement à partir de la production, et c'est là que la contrainte de la section précédente devient un fardeau. Pour réinitialiser le développement à partir de son parent, vous devez d'abord supprimer les enfants du développement, ce qui, dans cette topologie, signifie la recette et toutes les branches de fonctionnalités qui en découlent. Ainsi, un simple besoin d'actualisation des données se transforme en cascade : supprimer la recette et toutes les branches de fonctionnalités, réinitialiser le développement, recréer les environnements, reconfigurer chaque chaîne de connexion qui pointait vers les anciennes branches, et réappliquer chaque droit d'accès Postgres, car ces droits résident sur la branche que vous venez de supprimer.

image1.png

Aucune de ces étapes n'est difficile en soi. Mais répétées régulièrement, elles incitent l'équipe à éviter discrètement les actualisations, ce qui signifie que tout le monde recommence à tester sur des données obsolètes et divergentes. C'était le problème d'origine. Cette topologie simpliste ne vous coûte pas seulement un après-midi difficile ; elle décourage les bonnes pratiques qui maintiennent l'intégrité de l'environnement.

L'architecture Glaspoort : toujours dériver de la production

La solution réside dans un changement simple de votre vision de la topologie, et ses effets sont considérables : chaque branche d'environnement à longue durée de vie est un enfant de la production, et non d'un autre environnement.

Le développement et la recette sont deux branches créées directement à partir de la production. Elles se situent côte à côte sous la production, et non superposées. Aucune n'est le parent de l'autre, donc actualiser l'une ne vous oblige jamais à supprimer l'autre.

Ce simple changement désamorce le piège. Lorsque le développement ou la recette dérive, nous le réinitialisons à partir de la production (une opération via l'interface utilisateur aujourd'hui), environ une fois par sprint ou dès que nous voulons des données fraîches. Comme rien ne dépend du développement ou de la recette, il n'y a pas d'enfants à supprimer au préalable, pas de chaînes de connexion à redistribuer au sein de l'équipe, ni de marathon de réapplication des droits d'accès. La réinitialisation est peu coûteuse, nous la faisons donc réellement, et les environnements restent ainsi fidèles à la réalité. Si une réinitialisation omet une modification, la relecture de migration suivante l'applique simplement à nouveau.

image2.gif

Animation : le cycle de vie par PR. S'affiche sur le blog publié ; présenté ici sous forme d'image fixe.

Le flux par PR

Le flux de travail quotidien des développeurs superpose des branches éphémères à cette topologie stable :

  1. Un développeur ouvre une PR éphémère (TTL défini sur 1 heure).
  2. La CI crée une nouvelle branche pr-xxxx éphémère à partir de la production. Nous créons toutes les branches à partir de la production et ne fusionnons jamais les bases de données en retour. Ces branches par PR sont jetables : nous les archivons dès la fermeture de la PR, ce qui nous permet de rester sous la limite par défaut de Lakebase de 10 branches non archivées par projet.
  3. La CI rejoue les migrations sur cette nouvelle branche. Une vérification git diff détermine si une simulation de migration est réellement nécessaire, de sorte que les PR qui ne modifient pas les migrations ignorent cette étape.
  4. Nous ne testons pas seulement la migration de manière isolée. La CI déploie la nouvelle image de l'application sur un emplacement de staging, la pointe vers la branche fraîchement migrée et exécute l'intégralité de la suite de tests sur ce binôme.
  5. Chaque PR valide conjointement la migration et la nouvelle image de l'application, avant que l'une ou l'autre ne touche un environnement réel.
  6. Lorsque la PR est fusionnée, la CI recommence. Elle recrée une branche à partir de la production, rejoue les migrations et relance les tests, car l'environnement a pu évoluer depuis que la PR est passée au vert. Si les migrations réussissent, elles sont ensuite appliquées à la branche cible, sur place. Pour la première fusion, cette cible est le développement.
  7. Le passage en recette puis en production se fait via des étapes de validation manuelle, chacune rejouant les migrations sur la branche suivante.

Un détail mérite d'être souligné pour les équipes issues d'un monde centré sur git. Nous ne conservons qu'une branche master dans git. Il n'y a pas de branche develop ou release à longue durée de vie dans le contrôle de version. Le chemin de déploiement du dev à la recette puis à la production est entièrement géré par des étapes de validation manuelle dans Azure DevOps, qui déclenchent la CI et transfèrent les modifications d'une branche à l'autre. La topologie de l'environnement réside dans Lakebase et le pipeline ; git reste simple.

Pourquoi le rejeu des migrations est la source unique de vérité

Ce qui sécurise l'ensemble du processus, c'est que nous ne fusionnons jamais les bases de données entre elles. Nous ne déployons pas une modification en copiant les données d'une branche de fonctionnalité vers le développement. Nous la déployons en réexécutant les migrations sur la branche cible. Nous utilisons vitest pour lancer les tests de fumée sur l'emplacement de préproduction.

Dans ce modèle, les branches sont délibérément éphémères et jetables. La description durable et de référence de ce que doit être le schéma n'est pas une base de données à longue durée de vie qui aurait pu dériver. C'est l'ensemble ordonné des migrations. C'est pourquoi une réinitialisation est sûre, même s'il manque quelque chose : si une branche actualisée n'inclut pas une modification, le rejeu de migration suivant l'applique à nouveau. Les migrations sont si centrales que les bases de données actives sont toujours reproductibles à partir d'elles, plutôt que précieuses à cause d'elles.

C'est là la véritable clé du succès. Créer des branches à partir de la production maintient les données à jour. Traiter les migrations comme la source unique de vérité garantit l'exactitude du schéma. Ensemble, cela signifie qu'aucun environnement n'est jamais trop précieux pour être jeté et reconstruit.

Le choix : deux modèles de déploiement

C'est la partie que nous pensons être la plus utile pour les autres équipes, car il s'agit d'un véritable choix plutôt que d'une bonne pratique avec une seule réponse correcte. Une fois la topologie ci-dessus établie, vous devez encore décider quand une PR peut être fusionnée. Nous avons évalué deux modèles, et ils présentent des compromis opposés.

Option 1 : Fusionner après validation de la CI (empilement de PR)Option 2 : Fusionner après CD vers dev et recette (déploiement par PR)
FonctionnementUne PR peut être fusionnée dès que la CI sur sa branche pr-xxxx est au vert.Une PR ne peut pas être fusionnée tant qu'elle n'a pas été déployée en développement et en recette.
AvantagesLes membres de l'équipe peuvent s'appuyer immédiatement sur les modifications des autres.Chaque PR avance à son propre rythme ; les correctifs urgents (hotfixes) sont prioritaires et ne font pas la queue derrière d'autres modifications.
InconvénientsPlusieurs PR fusionnées s'empilent lors du déploiement en dev et en recette. Un correctif urgent ne peut pas contourner la file d'attente sans un pipeline parallèle.Les membres de l'équipe doivent attendre que la recette soit au vert avant de pouvoir s'appuyer sur une modification.

En résumé : l'option 1 optimise la vélocité des développeurs au sein d'une équipe soudée. L'option 2 privilégie l'indépendance et un parcours fluide pour les correctifs urgents. Si votre équipe travaille sur des modifications qui se chevauchent et fait confiance à la CI, l'empilement vous permet d'avancer le plus rapidement. Si vous devez fréquemment déployer un correctif qui ne peut pas attendre d'autres modifications en cours, le déploiement par PR vaut largement la peine.

Nous avons choisi l'option 1. Nous sommes une petite équipe travaillant sur des modifications qui se chevauchent. Attendre que la recette passe au vert avant que quiconque puisse s'appuyer sur votre travail est un coût que nous paierions tous les jours, alors que le risque lié à l'empilement ne se concrétise qu'occasionnellement. Et quand c'est le cas, il est maîtrisé : chaque étape de validation réexécute les migrations empilées sur une copie fraîche de la production avant de toucher à la vraie branche, ainsi l'ensemble est validé comme un tout. Pour les cas véritablement urgents, nous conservons un pipeline d'urgence distinct qui va directement en production avec les mêmes vérifications préalables. La vélocité par défaut, une issue de secours prête à servir.

En marge : les sujets abordés en cours de route

Quelques sujets sont revenus à plusieurs reprises lors des sessions de travail. Ils ne constituent pas le cœur du sujet, mais ce sont les détails qui déterminent si le modèle survit à l'épreuve de la production.

Authentification de la base de données et rotation des jetons. L'application ne conserve pas de secret de base de données à longue durée de vie. Un mot de passe statique dans Azure Key Vault est utilisé pour générer un identifiant de base de données à courte durée de vie (un jeton avec un TTL de 60 minutes) via `databricks postgres generate-database-credential`. Comme le jeton expire après une heure, le client doit le rafraîchir de manière proactive ; nous gérons cela avec une fonction de mot de passe asynchrone sur le pool de connexions qui met l'identifiant en cache et le renouvelle avant son expiration.

Autorisations et provisionnement des accès. Nous ne gérons pas le DDL des tables via des migrations, et les autorisations au niveau de l'objet sont appliquées manuellement aujourd'hui plutôt que par du code. C'est le point le plus évident à améliorer sur notre liste. Mais comme nous appliquons ces autorisations en production et que les branches d'environnement sont réinitialisées en recréant une branche à partir de la production, les branches enfants en héritent automatiquement à chaque actualisation. L'étape d'autorisation manuelle n'a lieu qu'une seule fois en production, et non de manière répétée sur chaque enfant, ce qui reste assez rare pour que nous puissions nous en accommoder pour l'instant.

Accès à la base de données : un utilisateur d'application unique. Nous ne provisionnons pas de rôle Postgres par utilisateur final. L'application se connecte via un utilisateur d'application unique, et l'autorisation pour les utilisateurs individuels réside dans la couche applicative plutôt que dans la base de données. Cela simplifie le provisionnement ; le compromis est que le contrôle d'accès par utilisateur est appliqué au-dessus de Postgres, et non par lui-même.

Ce que cela a permis de réaliser pour Glaspoort

Un cas d'usage qui nécessitait auparavant une équipe complète et des mois de travail est désormais mis en production en quelques jours avec une petite équipe. Nos itérations ont été multipliées par dix, et une seule équipe livre en continu de la nouvelle valeur sur une plateforme qui grandit avec l'organisation.—Raymon Veldman, responsable Business Control & IT

Le changement le plus visible est la rapidité alliée à la sérénité. Chaque PR dispose désormais de sa propre base de données à l'image de la production en quelques minutes, de sorte que les développeurs ont cessé de se coordonner autour d'environnements de dev et de recette partagés, et les conversations du type « qui a cassé la recette ? » ont tout simplement disparu. Comme les branches d'environnement sont toujours des enfants de la production, nous n'avons plus jamais de journée de réinitialisation à partir du parent : plus besoin de supprimer les branches enfants, de reconfigurer les chaînes de connexion ou de perdre des après-midis à réappliquer des autorisations. Les migrations sont rejouées à chaque étape de déploiement, et nous déployons la nouvelle image de l'application sur un emplacement de préproduction connecté à la branche fraîchement migrée et lançons la suite complète de tests avant que quoi que ce soit ne touche un environnement réel. Ainsi, ce qui arrive en production a déjà fait ses preuves deux fois. Et en cas d'urgence, les correctifs suivent le même chemin sécurisé que tout le reste, simplement plus vite, au lieu de contourner le pipeline.

Vous choisissez Lakebase pour une base de données d'application et vous vous posez les mêmes questions sur la CI/CD ? Commencez par ceci : des environnements à longue durée de vie branchés directement sur la production, des branches éphémères par PR, et les migrations comme source unique de vérité. En savoir plus sur Databricks Lakebase

(Cet article de blog a été traduit à l'aide d'outils basés sur l'intelligence artificielle) Article original

Recevez les derniers articles dans votre boîte mail

Abonnez-vous à notre blog et recevez les derniers articles directement dans votre boîte mail.