application web.
On peut distinguer deux grandes parties dans l’application : les pages concernant l’acquisition de nouveaux clients (la page d’accueil et le formulaire de création de projet), et les pages de suivi (qui permettent le suivi d’un projet jusqu’à son aboutissement, puis de la rentabilité d’un produit d’assurance). Ces deux parties impliquent des domaines métiers de l’assurance différents et indépendants.
L’application web utilise des technologies répandues, comme le framework Next.js. Il utilise le design system Carbon de IBM, teste les parcours utilisateurs avec playwright et déploie sur gitlab-pages avec l’aide de gitlab-ci. Concernant son architecture, elle s’inspire de l’architecture JAMStack. La plupart des pages sont construites au moment du déploiement et déployées sur un CDN.
Dans des stacks et des contextes similaires, nous observons que les équipes grandissent à vive allure. On remarque également un ralentissement de la vélocité de l'équipe lorsque sa taille atteint environ 12 personnes. Les problématiques que l’on observe sont les suivantes.
Le couplage fait référence à la manière dont différentes parties d'un système dépendent les unes des autres. Plus le couplage est fort, plus les éléments du système sont interconnectés, et dépendent les uns des autres pour fonctionner correctement.
Le code de notre application est dans une seule base de code couplée. Pourquoi en arrive-t-on là ? En général, ces organisations ont sorti des fonctionnalités très rapidement pour satisfaire un marché ou prendre des feedbacks utilisateurs. Mais cette rapidité s’est faite au détriment de la qualité du code et du couplage entre ses composants.
Cela ne concerne pas seulement les composants internes, mais aussi les dépendances externes. Par exemple, si votre application utilise Vue.js comme framework principal, et que celle-ci est fortement couplée à la version actuelle de Vue, la migration vers une nouvelle version sera fatalement aussi conséquente que ce framework est utilisé. Ce type de couplage à des librairies externes rend chaque mise à jour de la librairie plus difficile.
L’état de l’art a du mal à évoluer. Les points techniques de l’équipe et la prise de décision sont de plus en plus difficiles à mener. À 12 personnes, les tech lead ont du mal à trancher face à la diversité des sujets.
De plus, l’équipe prend plusieurs sujets en même temps, et 12 personnes essaient d’avancer tous les sujets. Le product owner se plaint de devoir réexpliquer à plusieurs personnes les sujets qui passent de main en main.
De même, du côté des développeurs, ceux-ci se plaignent de devoir passer de sujet en sujet toujours plus complexe, mais aussi de devoir naviguer entre des parties très différentes de l'application en matière d’état de l’art. Il est très différent de créer une nouvelle fonctionnalité, ou bien d’apporter une modification sur une fonctionnalité faite il y a deux ans.
Les nouveaux arrivants dans l'équipe rencontrent des difficultés pour se familiariser avec la base de code qui grandit toujours plus, ainsi qu’aux librairies dont celle-ci dépend. Comprendre l'ensemble de l'application et son domaine métier est une tâche toujours plus compliquée. En conséquence, la charge cognitive croissante détériore la capacité de l’équipe à apporter des modifications rapidement. Cela ralentit le processus d'onboarding.
Lors de déploiement de l’application, toute la pipeline sur l’intégralité du code front doit s'exécuter (build, lint, test et déploiement). Fatalement, plus du code s’ajoute au logiciel, plus sa pipeline de déploiement sera longue.
Autre souci, pour déployer une version de l’application, tous les sujets en cours doivent être terminés. Même si certains développements sont prêts, ils doivent attendre que toutes les autres tâches soient bouclées, ce qui rallonge souvent le délai de mise en production.
Pour répondre à tous ces symptômes, nous nous proposons d’explorer une solution basée sur un découpage en micro-frontends. Ce découpage nous permettra d'atteindre plusieurs objectifs:
En principe, le framework Next.js oblige ses utilisateurs à créer un dossier pages qui gérera le routing. Le framework ne donne aucune autre contrainte. Nous voyons régulièrement des projets où tout le reste est mis sous le dossier components ou app.
Nous avons des projets qui sont architecturés comme ceci.
On a cette arborescence :
Pour commencer, nous allons nous concentrer sur le découpage du dossier en deux parties représentants les deux domaines métiers indépendants au sein de Micro-Assu : "acquisition" et "suivi". À l’intérieur de ces dossiers, peu importe l’organisation, tant que chaque dossier correspond à un domaine métier bien distinct et ne comporte pas d’import de l’autre. On se base sur les tests e2e pour garantir que rien ne casse après ces changements.
Lors de cette division, si l’application n’a pas de design system, il peut s’avérer que les deux dossiers doivent se partager certains composants graphiques. Dans notre exemple, nous utilisons un design system, donc la problématique ne se pose pas. Dans d’autres contextes, l’absence de design system peut contraindre à créer une librairie de composants graphiques.
On arrive à cette arborescence, visible à partir de ce commit :
Pour garantir cette indépendance, la bibliothèque TS-Arch peut-être utilisée afin de vérifier que les deux modules sont bien indépendants.
À partir d’ici, nous avons déjà résolu les problèmes liés à l’onboarding et à la charge cognitive, car on a isolé les composants qui traitent d’un domaine métier dans un dossier spécifique. Pour s’intéresser à ce domaine métier, seul le code à l’intérieur de ce dossier sera important à comprendre.
La prochaine étape est de trouver une solution pour déployer les différentes parties de l’application de manière indépendante.
Pour déployer les différentes parties de l’application de manière indépendante, il est essentiel de penser à une architecture qui permette de maintenir cette séparation, tout en facilitant les déploiements. C'est là qu'intervient Module Federation.
Module Fédération est une fonctionnalité introduite par Webpack 5 qui permet aux applications de charger des modules au runtime depuis des applications distantes. Cette approche facilite le partage de code entre différentes applications, permettant ainsi une intégration flexible.
Quelles sont les motivations qui l’ont amené ?
Dans de gros projets, des parties de code pourraient être utilisées comme des modules indépendants, du fait qu’il n’aient pas la même temporalité d’évolution. Module Fédération est venu simplifier ce processus de découpage et d’assemblage au runtime entre ces différentes parties de code. Module Fédération permet aux équipes de travailler sur des parties de l’application différentes avec un cycle de déploiement indépendant, tout en assurant que tout s’assemble en runtime.
Concrètement, Module Fédération propose de découper une application en module. Un module est un ensemble de composants, classes, ou fonctions regroupées pour accomplir une tâche spécifique. Voici à quoi pourrait ressembler les différents modules de Micro-Assu :
On retrouve trois types de modules :
En bref, Module federation permet de découpler les cycles de déploiement de différents modules d'une application.
L'objectif ici est de séparer le code en différents modules, qui auront chacun leur propre repository. Cela permet de mieux organiser le code, de faciliter la maintenance, et d'offrir la possibilité de déployer ces modules indépendamment.
Pour découper un projet en plusieurs packages et repositories, voici les étapes détaillées que vous pouvez suivre.
Git-submodules permet de gérer des repositories comme des sous-projets au sein d'un projet principal.
Commençons par déplacer les dossiers suivi et acquisition du dossier src vers l'extérieur, pour en faire des packages indépendants.
mv src/suivi ../suivi
mkdir src/pages
Chaque module doit toujours contenir un dossier pages. Ce dossier pages est essentiel pour que Next.js puisse gérer les routes associées à chaque module.
Ensuite, crée un dossier app-shell.
mkdir app-shell
mv src/* app-shell/
Le dossier app-shell contiendra maintenant tout le reste de l'application, à l'exception des modules suivi et acquisition.
Créons le nouveau repository git qui aura le code du module suivi (pour les repo app-shell et acquisition, ce seront les mêmes étapes). Préalablement, il faut créer les dépôts Git qui accueilleront les trois nouveaux modules.
cd ../suivi
git init
git remote add suivi <url repo suivi>
git checkout -b main // si la branche main est absente
git add .
git commit -m “Initial commit for suivi module”
git push -f suivi main
rm -rf ../suivi
cd ..
git add .
git commit -m “Remove local suivi directory, use suivi submodules instead”
git submodule add <url dépôt suivi> suivi
On se retrouve avec l’arborescence suivante au niveau de notre repo :
Maintenant, chacun de nos modules sont des applications autonomes.
Avec Module Fédération, l'objectif est de rendre accessibles les composants de suivi et acquisition dans app-shell, tout en permettant de les déployer indépendamment. Dans app-shell, nous configurons Module Fédération pour charger les modules remotes suivi et acquisition.
import { NextFederationPlugin } from '@module-federation/nextjs-mf';
export default {
output: 'export',
webpack: (config, options) => {
config.plugins.push(
new NextFederationPlugin({
name: 'app-shell',
filename: 'static/runtime/remoteEntry.js',
exposes: {},
remotes: {
'acquisition': 'acquisition@https://gitlab.octo.tools:15095/micro-frontend-article-acquisition-tous-les-octo-f67fe2f398279a/_next/static/runtime/remoteEntry.js',
'suivi': 'suivi@https://gitlab.octo.tools:15095/micro-frontend-article-suivi-tous-les-octos-atel-813b8855e51590/_next/static/runtime/remoteEntry.js'
}
})
);
return config;
},
};
Ici, app-shell est configuré pour charger les modules remotes suivi et acquisition, via les URLs des fichiers remoteEntry.js. Cela permet à app-shell de consommer ces modules comme s'ils faisaient partie de l'application.
Pour bien comprendre la configuration de module federation:
Pour rendre les composants de acquisition disponibles pour app-shell, nous devons les exposer via Module Federation :
import { NextFederationPlugin } from '@module-federation/nextjs-mf';
export default {
output: 'export',
webpack: (config, options) => {
config.plugins.push(
new NextFederationPlugin({
name: 'acquisition',
filename: 'static/runtime/remoteEntry.js',
exposes: {
'./CreationProjet': './src/components/formulaire-creation-projet/index.tsx',
'./PresentationValeur': './src/components/presentation-valeur/index.tsx'
}
})
);
return config;
}
};
Dans notre configuration de module acquisition, les composants CreationProjet et PresentationValeur sont exposés pour être utilisés dans app-shell.
Si vous voulez voir le projet en entier, allez voir : le commit step 3
La mise en place de cette architecture avec Module Fédération modifie la manière dont la CI gère les builds, les déploiements, mais aussi le testing.
Chaque module est construit et testé de manière indépendante, ce qui simplifie et accélère les processus de build. Les pipelines dédiés à chaque module sont généralement plus rapides car ils traitent des unités de code plus petites, ce qui permet de détecter et résoudre les problèmes plus rapidement.
De même concernant les tests unitaires et d’intégration dans le module. Ils deviennent plus efficaces car chaque module peut être testé indépendamment des autres. Les équipes peuvent ainsi se concentrer sur des modules spécifiques sans avoir à tester l'ensemble de l'application à chaque modification, ce qui accélère le cycle de développement.
Chaque module ayant son propre pipeline, il devient possible de déployer des parties de l'application indépendamment, ce qui réduit les risques associés aux déploiements groupés. Chaque module peut être déployé dès qu'il est prêt, sans attendre d'autres modules. Cela rend le processus de déploiement plus flexible.
Une réflexion sur les tests end-to-end doit être menée lors du découpage en micro-frontend. Comment vérifier que malgré ce découpage, un parcours utilisateur impliquant plusieurs domaines métier est toujours fonctionnel ? Ceci est indispensable pour prendre la décision de pousser en production.
Dans notre cas, nous avons gardé les tests playwright au niveau le plus haut du projet. À chaque fois que nous poussons une nouvelle version de notre submodule, la pipeline de test-e2e qui teste le workflow de l’application est lancée. Cela garantit qu’à chaque modification, l’application mise entre les mains des utilisateurs fonctionne.
Dans cet article, nous vous avons exposé le processus de conversion d'une application monolithique Next.js en une architecture de micro-frontends, permettant un déploiement indépendant de chaque composant.
Ce processus s’est fait en trois étapes. Dans un premier temps nous avons mis en lumière les problématiques de delivery ressentis par l'équipe. Cette démarche de découpage est utile uniquement si des douleurs similaires sont ressentis par votre équipe. Dans un deuxième temps nous avons fait émerger deux modules indépendants dans notre application monolithique afin de diminuer la charge cognitive nécessaire pour développer dessus. Et dans un troisième temps, nous avons rendu le déploiement de ces modules indépendants grâce à module fédération.
Enfin, on a discuté des conséquences de cette nouvelle architecture sur les pipelines de déploiement et le testing.