Le Ruby Nouveau

Podcast francophone sur la tech en général, le Web et Ruby en particulier

005. Multitenancy et la séparation des environnements / fonctionnalités

Comment faire du code propre, modulaire et quels sont les pièges qu'on peut rencontrer ?

À la table, Ronan Limon Duparcmeur, Bob Maerten, Olivier Lacan, Sylvain Abélard et Agathe Begault.


Télécharger l'épisode (18 Mo) - Date de publication : 2017-02-27


• Pas d'actu cette semaine
On a enregistré pas mal d'épisode à l'avance et on est à cours d'actus à la fois fraiches et intéressantes.

Comment faire du code propre, modulaire et quels sont les pièges qu'on peut rencontrer ?

Comment implémenter de nouvelles fonctionnalités de manière exceptionnelle mais nécessaire et qu’on doit pouvoir modifier rapidement ?

Sylvain : Il existe 4 solutions

  1. Jouer avec les différents environnements. En démo, en préprod ou en test avec juste des if qui font les distinctions.
  2. Multitenancy : Une même appli pour 5 clients différents et des fonctionnalités spécifiques par client. 5 déploiements différents, 5 bases de données différentes, 5 versions de code différentes. Difficile à maintenir mais ça fonctionne.
  3. Application chez vous en cloud, cloisonnée, avec des tables qui appartiennent à un client spécifique. Sur les 10’000 messages reçus, le client n’en voit que mille,…
  4. Feature flag : en prod, tel client a acheté telle fonctionnalité, comment lui mettre à disposition rapidement ?

Comment faire du code propre, modulaire et quels sont les pièges qu’on peut rencontrer ?

Par environnement

Olivier : C’est très spécifique à un produit. Par exemple, envoi/réception de message d’un autre service qui ne sont pas les mêmes en fonction des environnements (staging/prod). Changement dans l’environnement lui-même avec des variables d’environnement (cf heroku). Fichiers de configuration partagés mais qui ne seront pas les mêmes pour staging ou prod avec 2, 3 if si besoin. Pour les feature flag, nous utilisons la gem flipper mais cela demande beaucoup de vigilance car si le switch est récurrent, l’idée est de laisser flag mais si c’est une seule fois, il faudra certainement l’enlever.

Sylvain: Tous les if dans le code, c’est du travail et il est important de ne pas les oublier. Il faut revenir de temps pour voir si c’est toujours vrai ou pas. Sinon, tu recevras un rapport de bug et tu découvriras que tu avais oublié que ton flag était là.

Bob/Agathe : Nous utilisons dotenv car cela permet de lire l’environnement au démarrage de l’application. Et effectivement, nous définissons des variables qui sont adaptées. Nous utilisons l’environnement pour définir ce qui doit être fait dans quel environnement.

Ronan : Fan du recours à l’environnement parce que pratique mais surtout peur des feature switches. Flipper est une super gem mais c’est flippant. Dès lors qu’on fournit la capacité à ‘facilement’ changer un comportement, qu’est-ce qui pourrait nous empêcher dans 6 mois d’avoir 50 petits dauphins, 50 switches à basculer et autant de complexités potentiels et de bugs à base de ‘j’ai oublié que si j’activais celui-là et celui-ci, il ne fallait surtout pas activer le troisième’. Les variables d’environnement, c’est tellement lourd à utiliser que c’est un bon frein à la prolifération des feature switches.

Olivier : Sur codeschool actuellement, on a 7 flippers activés qu’on devrait retirer et 16 au total qu’on utilise régulièrement mais dont certains ne sont pas tout le temps activés. Ce qui est génial avec Flipper, c’est que cela te permet d’afficher un truc différent. Quand tu fais des promotions sur un weekend, c’est super de se dire que soit 10%, soit 20% soit tous les utilisateurs en bénéficient. Nous utilisons flipper depuis un an et nous avons 16 checks au total. Au début, c’était uniquement l’équipe backend qui utilisait Flipper, aujourd’hui l’avant office l’utilise aussi. Ça s’est vendu parce que c’est assez facile d’utilisation. Le pire qui peut arriver c’est quand tu as oublié un check à 100%. Il faut y penser tous les 3, 6 mois. Flipper utilise pas mal de ressources à checker mais tu peux cacher tout ça avec cache ou redis. L’impact sur la vélocité de développement est génial pour nous parce que ça nous a permis de sortir un tas de features pour les admins et de découvrir des bugs qu’on n’aurait jamais pu découvrir sans ça. Les bénéfices sont plus importants que la peur décrite.

Ronan : Ce qui est intéressant dans le scénario que tu proposes, c’est que la possibilité de faire ces bascules pendant que l’app tourne. Donc, changer la configuration en vol. C’est particulièrement adapté dans ce cas-là. Dans notre cas, c’est moins pertinent. Finalement une fois qu’un système est configuré et lancé, il ne changera jamais. Pour le coup, avoir des options de config qui sont ancrées dans la machine, dans l’environnement de la machine et qui ne soient pas facilement modifiables, ça fait sens. Mais dans ton cas c’est différent parce que la question c’est comment changer l’app en plein vol quand une promotion dure une semaine par exemple.

Olivier : Nous l’avons utilisé récemment pour un nouveau type de produit proposé sur codeschool qui s’appelle ‘Projects’ pour lequel nous n’étions pas sûr de la réussite. Il y avait plein de points qui pouvaient être problématiques. Nous l’avons testé en interne, avec plusieurs personnes et groupes différents pour savoir si ça fonctionnait . Mais on l’a aussi testé avec un pourcentage d’utilisateurs qu’on faisait varier. Et lorsque ces derniers l’utilisait, nous pouvions voir quand il y avait 100 personnes, 1000 personnes dessus. Ça donne énormément de données et de liberté.

Bob : Dans le cadre d’une démo et d’un serveur de staging, ce n’est pas évident de dire aux utilisateurs d’aller essayer cette feature sur tel serveur de staging parce qu’on l’a déployé sur telle branche de l’app. Généralement, les gens ont autre chose à faire que d’aller essayer des choses en test ou en demo alors qu’ils sont là pour utiliser le service. Dans ce cadre-là, les feature flag, c’est une bonne alternative pour qu’une partie de la population utilise la fonctionnalité à tester et au moins ils nous feront des retours.

Ronan : C’est sûr que de devoir monter un serveur pour bêtatester, c’est un peu compliqué.

Sylvain : En même temps, c’est sûr que comme ça, il ne va rien se passer sur ton environnement normal. Le coût du risque étant grand, cela peut se justifier. Après si tu veux avoir plein de profils différents, cela devient une fonctionnalité normale de dire que ‘oui quand il y a tel profil, il se passe ci’. Autre exemple qui a pu m’arriver sur les rythmes de livraison différents entre webservice et votre application. Quand vous ne savez pas quand le webservice va faire sa mise en production, ça peut être aussi simple qu’un if dans le code. Dans leur appel webservice habituel, ils envoient 15 champs. Le jour où ils en envoient 5 nouveaux, ça veut dire qu’ils sont passés en v3. Et donc j’utilise un if pour dire quand mon traitement du service est en v2 et quand mon traitement du service passe en v3. Pendant un certain nombre de temps, je n’ai que la v2 qui tourne, puis plus tard, dans le futur, je n’aurai que la v3. Et donc la mise en production est totalement décorélé de leur mise en prod à eux avec ce if qui dit ‘est-ce qu’ils parlent à la vieille ou à la nouvelle façon ?’.

Ronan : Du coup, tu codes en dur quelque chose qui est du ressort du déploiement ?

Sylvain : Non pas forcément. C’est vraiment une fonctionnalité. Ce webservice est utilisé par tout un tas d’autres entreprises, c’est une problématique de versionning d’API qui pourrait faire 3 épisodes à lui tout seul. Une partie des clients sont prêt pour le passage à la V3, ils vont l’utiliser dès que possible tandis qu’une autre partie ne veut pas que ça change. En tant que webservice, je vais devoir garder les 2 versions de mon endpoint ou utiliser des urls différentes ou le faire en mode silencieux, pour pouvoir gérer à la fois ceux qui ne répondent qu’à la v2 et ceux qui ont basculé sur la v3.

Agathe : Je voudrais revenir sur le testing des nouvelles fonctionnalités. Nous avons un serveur spécifique dédié, un serveur de release où nous allons déployer un pack de fonctionnalités et ensuite, nous demandons à un service extérieur de le tester. Nous leur fournissons un parcours de tests à effectuer. Ensuite nous passons en mode staging puis en mode production. Ça a un coût mais comme le disait Sylvain, le coût qu’on paie est un coût moindre que le coût qu’on paierait s’il y avait des problèmes en staging et/ou en prod.

Olivier : Il y a un service comme ça qui s’appelle Rainforest QA qui propose un genre de testing plus avancé que le test utilisateur. Ce n’est pas une mauvaise idée d’utiliser ce genre de service qui est un peu plus professionnel et qui va vraiment essayer de tout casser.

Sylvain : Rainforest, ce sont des vrais humains qui testent les parcours définis ?

Olivier : Pas sûr à 100% mais vraisemblablement.

Sylvain : Il y a une nuance intéressante qui est la différence entre testing en boîte blanche, boîte noire et boîte grise. C’est vraiment de la sécurité. En boîte blanche, la boîte de sécurité a accès au code, elle sait donc regarder où ça fait mal. En boîte noire, elle ne connait pas du tout le code. En boîte grise, c’est un mélange des deux, on ne donne pas accès à la machine mais on pointe les urls intéressantes. Nous, nous utilisons les tests de développeur, les tests d’utilisabilité et du testing en boîte noire donc une équipe séparée. Ce sont des Ingénieurs qualité et ils ont des parcours Selenium, c’est un robot qui clique dans le navigateur.

Agathe : On a ça mais ça ne répond pas toujours à tout. C’est pas assez créatif.

Multitenancy

Sylvain : Revenons sur cette histoire de multitenancy. Il n’est plus question d’environnements cette fois mais d’avoir plusieurs clients et plein d’utilisateurs qui parlent différemment. Agathe doit avoir ça aussi : une seule installation de l’appli qui répond à tous les API de plein de clients différents. À moins que tu ne déploies un nouveau serveur à chaque nouveau client. Quelles sont vos stratégies pour gérer des clients séparés de façon satisfaisante ? Par exemple, lorsque la société A ne doit pas voir le carnet d’adresses de la société B.

Bob : Nous avons eu une société qui a absorbé une autre société sur une appli Rails monolithique. Plutôt que d’aller chercher une gem ou d’essayer de faire les choses correctement, nous avons séparé les bases de données dans une premier temps. Après nous avons essayé de voir si on ne trouvait pas un modèle de données qui intègre un niveau supérieur. Enfin, nous avons rajouté tout un tas de dépendance dans les modèles pour pouvoir intégrer cette notion. Ce n’est pas évident car ça se fait au cas par cas. Et il y a aussi des contraintes juridiques : des clients qui ne veulent pas mélanger leurs données avec celles d’autres clients. Il y a pas mal de solutions mais pas de recettes miracles.

Ronan : Nous jouons la simplicité. Dans ces cas-là, c’est une autre base de données. L’intérêt des VM et des clouds, c’est la possibilité de multiplier les bécanes de son parc. Nous aimerions aussi tester les schémas Postgres pour avoir un seul serveur qui gère les bases de manière séparée, distincte, cloisonnée. Il y a même un vieux RailsCast qui en parle et Ryan Bigg qui en a fait un livre : Multitenancy with Rails dont un des chapitres parle des schémas Postgres. Le serveur considère qu’il a une deuxième base, un deuxième lot de tables qui ne se parlent pas. Ils ont leur propre table de schémas. Et si on regarde les données systèmes, chaque table part d’un schéma différent. En fait, ce sont des namespaces pour les bases. Publiques par défaut.

Olivier : Les migrations sur ce genre de système fonctionnent correctement ou c’est un peu flippant ? Comment faire si une migration passe sur une base mais pas sur les autres ?

Ronan : Autant que je sache, c’est la difficulté du système. Le jour où on le fait, j’en reparlerai.

Sylvain : En fait, tu as deux bases de données différentes donc soit le code est capable de comprendre qu’il n’a pas le même schéma de données, soit tu t’arranges pour faire la migration des deux schémas en même temps.

Olivier : C’est comme si tu avais un serveur de prod et un serveur de staging.

Sylvain : Nous avons un serveur de préproduction qui sert à faire tourner les tests automatisés et un serveur de staging qui est hyper cloisonné si on a besoin d’un clone de la base de données de prod et de l’appli de prod pour tester des fonctionnalités. Il est possible d’avoir autant d’environnements séparés selon les usages voulus.

Olivier : Nous n’avons jamais vraiment fait ça mais cela devient de plus en plus nécessaire. Plus il y a d’équipes sur un seul produit qui testent des choses différentes, plus il est nécessaire d’avoir un environnement de préproduction sur des branches différentes pour tester toutes les variables et toute fonctionnalité qui peuvent exploser en production. Pour revenir sur les branches conditionnelles, nous avions une application qui faisait tourner Rails 5 avec différents podcasts dessus et des branches différentes avec les podcasts dessus. À part la nature des podcasts, c’était vraiment similaire. À la différence de Ronan, il n’y avait pas de différences de données, juste des variables d’environnement différentes. Nous pouvions donc déployer les applis de la même façon sur 3, 4 serveurs différents et les variables d’environnement changeaient les podcasts, les CSS,…

Agathe : Je vais revenir sur le filtre des données par rapport à un utilisateur ou un groupe d’utilisateurs. Si c’est une grosse entité, nous allons dédier une base de données mais sinon, nous avons créé une liste de gestion de contrôle d’accès, des ACL. C’est assez pratique parce que nous avons la possibilité de spécifier que tel compte a accès à tel client, tels types de données, pour tel pays,… Pour nous c’est plus simple, parce que nous n’avons pas besoin de gérer un autre environnement, un autre système avec une autre base de données.

Sylvain : Les ACL sont dans le code ou c’est configurable ?

Agathe : C’est configurable.

Sylvain : Donc si je veux changer quelque chose, je le fais dans ma prod, pas besoin d’un nouveau déploiement ?

Agathe : Tout à fait.

Sylvain : Vous avez des gems pour ça ou vous avez fait quelque chose de très custom ?

Agathe : Nous avons fait quelque chose que nous avions rendu visible à une époque mais nous l’utilisons moins qu’avant et aujourd’hui, nous avons des choses plus custom, spécifique à notre domaine.

Bob : J’ai une gem à proposer, c’est Pundit, un système qui permet de faire des autorisations et en plus de faire du filtrage sur les modèles. Les autorisations fonctionnent pour les contrôleurs mais aussi pour les scopes qui sont retournés par les modèles. Il y a un vocabulaire à apprendre pour utiliser la gem. Mais c’est assez progressif dans son usage et c’est du pur ruby object. C’est assez pratique parce que ça permet de limiter un scope pour un client, en fonction d’un profil, pour un contrôleur. On peut vraiment triturer ça dans tous les sens pour faire du contrôle d’accès.

Agathe : Clairement, c’était le problème de limiter au niveau des contrôleurs qui nous avait posé souci à l’époque et c’était pour cette raison qu’on avait sorti la gem que j’avais présenté à Paris.rb. Elle était un peu trop restrictive pour nous et c’est pour ça que nous avons fait quelque chose de plus custom.

Olivier : Je trouve ça intéressant de voir pas mal de gems récentes ou plus anciennes qui s’éloignent du langage spécifique du domaine. Pour Pundit, ce sont des politiques de sécurité pour chaque modèle, c’est un peu comme un serializer. C’est tellemnt plus facile de raisonner pour ce genre de choses et surtout de le tester sans avoir à imaginer comment ça fonctionne en interne.

Sylvain : C’est un de mes grands chevaux de bataille. J’aime beaucoup Ruby, la metaprogrammation sauf que derrière, cela peut donner des trucs un peu sales, un peu bizarres. À une époque, tout le monde ne voulait faire que de la metaprogrammation, que des DSL et aujourd’hui on est un peu revenu dessus. Comme dans les autres langages, les gens vont ajouter une bibliothèque qu’ils réutilisent et qu’ils appellent avec 3 paramètres.

Olivier : J’ai un truc parfait pour la metaprogrammation, c’est une gem que Jamis Buck qui bossait à Basecamp vient de sortir : Castaway. Elle permet de créer des screencasts et de présentations. Elle est écrite complètement en DSL et c’est parfaitement adapté. C’est un outil de création où tu dois apprendre le langage de la libraire pour savoir comment créer, comme dans n’importe quelle interface comme par exemple GarageBand ou Keynote. Mais là-dedans, c’est un DSL qui ressemble beaucoup à Rspec, à des DSL ruby communs mais clairement ce n’est pas du ruby. Tu as une idée d’un clip, d’une scène, d’une transition et bien la transition, ça va être une méthode à laquelle tu vas passer des arguments pour passer d’une section à une autre. C’est vraiment très intéressant et c’est diamétralement opposé à cette idée de ruby pur En même temps, c’est pas quelque chose que tu veux intégrer dans ton application. C’est respectueux dans ce sens-là : tu ne demandes pas à quelqu’un d’incorporer dans son domaine de business quelque chose qui n’a rien à voir avec le code qu’ils utilisent. C’est logique dans le sens où c’est indépendant de toute autre code.

Sylvain : C’est un truc qui arrive souvent. Tu crées un outil qui est full-proof, idiot-proof, qui est non seulement très puissant pour exprimer tes problèmes dans le domaine que tu as choisi et qui en plus évite de faire des bêtises. Même si le stagiaire se trompe dans l’utilisation du code, ce qu’il voulait faire ne marche pas, voire un message adapté vient lui répondre, c’est génial.

Ronan : C’est vrai que Castaway met bien en avant qu’un DSL c’est utile dès lors qu’il s’agit d’exprimer un domaine qui a besoin de son propre langage. Bien souvent en ruby, les DSL prématurés sont plus là dans un souci d’assécher un petit peu le code, de faire du DRY à base de “j’ai 3 méthodes qui se ressemblent trop souvent, je vais les métaprogrammer”. On crée des DSL qui sont moins là pour exprimer un domaine à travers un langage mais plutôt pour satisfaire l’ego du développeur qui métaprogramme, pour avoir l’illusion d’avoir un code plus sec alors qu’en réalité, il est hyper complexe car très indirect, c’est typique de la couche d’abstraction pas bienvenue.

Sylvain : Nous n’avions pas prévu de parler de DSL dans cet épisode mais ça nous a amené à la notion de DRY. La question de base était de vouloir répondre à des choses de manières différentes en fonction du domaine, nous avons exploré plein de pistes et le DSL est tout à fait légitime. Nous avions prévu de parler de choses compliquées comme le DDD, Domain Driven Design, mais ça va être un peu long à développer dans cet épisode. N’hésitez pas à aller regarder par vous-même avec le livre Domain Driven Design. Il faut le lire 4 ou 5 fois pour comprendre ce qu’il dit.

Ronan : Pour rajouter quelque chose, Ruby est un langage spécifique et nous avons beaucoup parlé des if et des conditionnelles. Je suis en train de regarder s’il n’est pas possible de jouer sur les modules et les inclusions. C’est-à-dire : que ce soit une variable d’environnement, un fichier de conf, peu importe la façon, si je sais que j’ai un comportement différent, une configuration différente, je peux peut-être ajouter des modules aux bons endroits pour modifier le comportement de mon app. Sans avoir besoin de rajouter des conditions au coeur des objets affectés pour essayer de limiter l’impact de la configurabilité. La classe qui gère la paie n’a pas besoin de savoir qu’en environnement différent, elle fonctionne différemment. Il s’agit de coder le scénario le plus courant et de rajouter un module qui vient un peu le modifier. J’appelle ça ‘la technique de Goldorak’. Quand il part à l’attaque, il n’est pas forcément équipé pour affronter Golgoth. Il doit se battre sous l’eau, sous la terre et ce n’est pas adapté à la configuration de Goldorak. Pour l’aider, il y a ses petits potes qui débarquent et qui se greffent sur lui, qui lui rajoutent ce qui lui manque. C’est exactement ça que je suis en train de tester, je vous dirai dans quelques épisodes si ça marche bien ou si c’est complètement catastrophique.

Olivier : C’est une bonne idée parce que tu gères la configuration, l’initialisation de ton app au démarrage et après, tu inclues le comportement différent. Nous avons tendance à faire ça, nous aussi, sur les outils internes de codeschool : injecter du comportement dans certains cas et pas dans d’autres. C’est plus facile de raisonner comme ça parce qu’en haut de ta classe, tu sais exactement ce qu’elle a comme comportement, ce qu’elle peut et ne peut pas faire. C’est beaucoup plus simple à maintenir sur le long terme.


Découvertes des invités

• Ronan Limon Duparcmeur
Comment coder proprement dans les applis webs – Clean Code in the browser
• Olivier Lacan
Un film sur la linguistique et la communication entre deux êtres qui n'ont quasiment pas de points de commun – Arrival
• Bob Maerten
Alternative OpenSource à Pocket pour enregistrer le contenu d'une page web en format text – Wallabag
• Sylvain Abélard
Les conférences et les livres de Sandi Metz : indispensables ! – Sandi Metz
Nothing is Something
Pragmatic Object-Oriented Design in Ruby – POODR
Pratiques de code pour coder, tester, refactorer un bout de code "simple" – 99 bottles of OOP
• Agathe Begault
Beauté et vices des androides – WestWorld