Concevoir un SaaS multitenant
Cet article se repose sur mon talk Concevoir un SaaS multitenant fait à Cloud Nord le 12 octobre 2023.
Kestra est une plate-forme d’orchestration et de scheduling de donnée hautement scalabe, qui crée, exécute, planifie et surveille des millions de pipelines complexes. Pour une introduction à Kestra, vous pouvez lire mon article sur le sujet.
Une des évolutions récente de Kestra dont j’ai été chargé fut le support du multitenant, cet article va vous raconter la conception qui a été faite pour l’ajout de cette fonctionnalité.
Le Multitenant et ses différents modèles
Le multitenant peut être défini comme un principe d’architecture logicielle permettant à un logiciel de servir plusieurs organisations clientes (tenant en anglais, ou locataire en français) à partir d’une seule installation.
Le multitenant simule plusieurs instances logiques dans une instance physique unique. Le but étant de maîtriser les coûts d’hébergement et d’opération.
Il existe plusieurs modèles de multitenant.
Une instance par tenant
- Une instance de l’application est démarrée pour chaque tenant, le multitenant est géré au-dehors de l’application.
- Pros : simplicité.
- Cons : coût d’opération, il faut démarrer une application par tenant.
Une base par tenant
- Une base de données est démarrée pour chaque tenant, la seule logique à implémenter est la sélection de la base.
- Pros : simplicité, coût d’implémentation.
- Cons : coût d’opération, il faut démarrer une base par tenant.
Un schéma par tenant
- Un schéma de base de données est créé pour chaque tenant, la seule logique à implémenter est la sélection du schéma.
- Pros : simplicité, coût d’implémentation, coût d’opération.
- Cons : nécessite une base offrant des schémas, la base unique est le SPOF.
Tenant au sein des tables/messages
- Une information
tenantId
est ajoutée à chaque ligne de chaque table. - Pros : flexibilité, coût d’opération.
- Cons : complexité, coût d’implémentation.
L’architecture de Kestra
Avant tout, il faut expliquer l’architecture de Kestra.
Kestra est séparé en plusieurs composants qui communiquent entre eux par messages asynchrones via une queue. Les métadonnées des flows sont stockés dans un repository.
Les différents composants sont:
- L’Executor: contient la logique d’orchestration.
- Le Scheduler: traite les différents événement déclencheur d’un flow (triggers).
- Le Worker: exécute les tâches d’un flow.
- L’ Indexer: composant optionnel qui permet d’indexer la queue dans la base de données.
- Le Webserver: sert l’interface graphique et l’API de Kestra.
Le Worker est le seul composant à accéder aux systèmes externes nécessaires à l’exécution d’un flow (bdd distance, service web, service cloud, …) ainsi qu’au stockage interne de donnée de Kestra.
Kestra offre plusieurs modes de déploiements : standlone avec tous les composants en un seul processus ou micro-service avec un composant par processus.
Kestra propose deux runners:
- Le runner JDBC : Queue et Repository sont implémentés via une base de donnée (H2, PostgreSQL, MySQL).
- Le runner Kafka : Kafka est utilisé comme Queue et Elasticsearch comme Repository. Ce runner n’est disponible que dans l’édition d’entreprise.
Le multitenant chez Kestra
Le projet du SaaS
Kestra travail à une version disponible en mode SaaS, Kestra Cloud.
À ce jour, les contraintes du SaaS sont les suivantes :
- Un « gros » cluster haute dispo avec un runner Kafka par fournisseur cloud / région.
- Chaque tenant a ses propres ressources (namespace, flow, execution).
- Isolation de la donnée.
- Un utilisateur est global à tous les tenants.
La notion de namespace
Un flow est dans un namespace, les namespaces sont hiérarchiques, un peu comme un répertoire de filesystem.
Les namespaces permettent une configuration spécifique (task, secret, …) ainsi que la définition de rôles et d’accès en version entreprise.
Une des réflexions qui a été menée était de savoir si la notion de namespace pouvait nous servir dans l’implémentation de Kestra.
Les modèles évalués
Trois différents modèles de multitenant ont été évalués.
- Tenant par namespace : chaque namespace est un tenant. Cette solution est proche de la solution Un schéma par tenant mais en utilisant le namespace qui est une propriété propre à Kestra.
- Tenant par namespace de base : chaque namespace de base est un tenant, un tenant pouvant alors avoir plusieurs namespace, les enfants du namespace de base. C’est une déclinaison du modèle précédent.
- Tenant via une nouvelle propriété
tenantId
.
Un des runners de Kestra utilisant Kafka et Elasticsearch qui ne supportent pas la notion de schéma, seule une déclinaison du modèle Tenant au sein des tables/messages était possible. Les trois solutions proposent donc d’ajouter le tenant dans une nouvelle propriété ou d’utiliser une propriété existante (namespace) pour limiter les changements nécessaires.
Le choix
Tenant via une propriété tenantId
.
L’utilisation du namespace aurait été pratique, car nous avions déjà une isolation des flows par namespace ainsi qu’une gestion des droits (RBAC). Mais cela aurait fortement réduit les fonctionnalités d’un utilisateur de notre Cloud, car un namespace ou un namespace de base n’aurait pu être utilisé par différents utilisateurs. Le seul modèle qui répondait à tous nos besoins sans limiter de futures fonctionnalités était donc l’ajout d’une nouvelle propriété tenantId
.
L’implémentation
- On ajoute
tenantId
à tous les objets du model. - On filtre sur
tenantId
dans chaque requête BDD. - On résout le
tenantId
dans la couche API.
Ça semble simple non ? 🤷
Un plan se déroule toujours sans avec accrocs !
Ajouter une propriété à un grand nombre de classes / filtre / requêtes / … amène un fort risque d’oublie, et donc de bug. Et c’est bien sûr ce qu’il s’est passé. Malgré un grand soin lors de l’implémentation, il manquait quelques endroits où le tenant n’était pas passé (principalement dû à l’utilisation des builders de Lombok … je ne vais pas m’appesantir dessus).
De même certaines parties de Kestra nécessitent d’avoir connaissance de l’intégralité des données (par exemple de l’intégralité des flow), il a fallu faire en sorte que, par exemple, quand on liste les flows depuis l’API la liste soit filtré par tenant, mais pas quand on liste les flow depuis le scheduler de flow.
Pour faciliter la migration de nos utilisateurs existant, nous avons permis l’utilisation d’un tenant par défaut, qui est le tenant donc l’identifiant est null. Ce fut la cause de nombreux autres bugs …
Pour conclure, le multitenant est essentiel dans la mise en place d’un SaaS, et après avoir choisit avec soin son modèle d’implémentation, attendez-vous à une implémentation longue, laborieuses, et source de bug. Pour pallier au risque de bugs, nous avons choisi de merger les PR sur le multitenant en début de cycle de release, ce qui nous a permis de la tester pendant un mois sur nos propres environnements de test avant de le délivrer à nos utilisateurs et ainsi découvrir ainsi une grande partie des bugs qui avait été introduit. Je vous recommande fortement de prévoir une période de test importante comme nous l’avons fait.
Le mot de la fin : implémenter une architecture multitenante n’est pas facile, il faut donc idéalement l’implémenter au plus tôt dans une base de code.