Retour d'expérience, construction d'une application serverless

Retour d'expérience, construction d'une application serverless

Il y a quelque temps, j'écrivais un article sur la hype serverless et comment {% post_link une-api-rest-serverless-avec-aws-lambda-et-express mettre en place une petite API sur AWS Lambda %}. Cette connaissance est tirée d'une expérience professionnelle au cours de laquelle j'ai spécifié et mis en place ce type d'architecture. Cette expérience étant terminée, je vous propose de faire un petit retour d'expérience sur l'utilisation de ce type de technologies et aussi de découvrir comment nous avons travaillé.

Genèse

Nous sommes en juillet 2017, quand mon manageur entre dans l'open-space avec un grand sourire aux lèvres et m'annonce ce nouveau projet. À première vue, rien de particulier. On peu prendre notre application existante, la modifier ici et là et arriver à nos fins. Mais cette fois, c'est différent. On a de nouvelle contraintes, sur la scallabilité et le déploiement que notre base de code actuelle ne sais pas gérer. Et pour y arriver, une refonte complète du système actuelle est nécessaire. De plus, cette application est minime et n'a pas besoin de toute la machinerie disponible sur le système déjà en place. Après une étude des possibilités, nous choisissons de développer la nouvelle application en partant de zéro, avec en prime une liberté complète sur les technologies.

Pour tout un tas de raisons (que je ne détaillerais pas ici), nous avons choisis de travailler avec AWS. Certains composants tels que l'API, l'authentification, les traitements événementiels sont mis dans la cour du serverless, alors que d'autres tels que la simulation et la base de données rentre dans la cour habituelle d'une infrastructure.

Architecture

Front

Nous avons utilisé 3 services d'AWS pour gérer la mise à disposition des composants statiques du front. Pour le stockage des fichiers (JavaScript, CSS, HTML, images...) nous avons classiquement utilisé S3. Afin de servir ces fichiers en HTTP et d'y ajouter une couche de chiffrement (HTTPS), nous avons utilisé Cloudfront.

Comme l'application dois pouvoir être utilisée par plusieurs clients, chacun d'entre eux y accédant au travers d'un préfixe spécifique. Par exemple https:⁄⁄client01.gwd.bidule.fr, où "bidule" est le nom de l'entreprise et "client01" le nom du client. En utilisant Route53 nous avons définis des alias DNS redirigeant vers le service Cloudfront. Ainsi, le même front est déployé une seule fois et fournit à tous les clients.

Schéma de description de l'architecture du front

Back

Dans la partie backend, il y a des briques serverless et d'autres en server classique. Le tous dans une zone privée aussi appelée VPC (Virtual Private Cloud) avec certains composants faisant la passerelle avec Internet.

Schéma de description de l'architecture fonctionnelle du projet

Globalement, même si l'architecture de l'ensemble à évolué avec le temps, les concepts et les responsabilités exprimés ci-dessus restent les mêmes.

L'API

REST

Nous avons créé une API REST avec Express.js. Elle est sans état et va s'exécuter dans des FAAS (Function As A Service), chez AWS nommé Lambda. Ce mode de déploiement permet non seulement de bénéficier d'une capacité native de passage à l'échelle, mais aussi de bénéficier d'une tarification à l'utilisation. 1 requête ⇒ un coût 💸, 0 requêtes ⇒ pas de coût.

Cette technologie n'étant pas directement exploitable par un navigateur, et se situant de toute manière dans la zone privée du VPC, nous avons mis en place une passerelle nommée API Gateway. Elle a une patte sur le réseau publique (Internet) et une sur le réseau privé. Son rôle est de gérer la désérialisation, la validation et le routage de requêtes HTTP. Pour la configuration, une description Open API est nécessaire. Elle prend des arguments spécifiques à AWS, afin de gérer le routage vers les Lambda et l'authentification. Une fois de plus, la tarification de se service est par requête.

Authentification

La connexion des utilisateurs est assurée par un autre service d'AWS nommé Cognito. La gestion de l'inscription, de la validation des mails, du stockage des mots de passes, etc... Sont entièrement gérés par ce service. Et un SDK dédié permet de réaliser des actions tant coté front que back.

Lors de la connexion, le front reçoit un jeton JWT (JSON Web Token). Ce dernier contient quelques informations, notamment l'identifiant unique de l'utilisateur et une signature. C'est ce jeton qui est passé dans l'en-tête d'autorisation de chaque requête à l'API de l'application.

Une fois configuré dans l'API Gateway et pour chaque requête pour laquelle l'authentification est nécessaire, une vérification de la présence du jeton et de sa validité sera faite. Les informations présentes dans jeton décodés seront transmises à la Lambda avec le reste de la requête HTTP. Elle n'aura plus qu'a gérer la récupération du profil de la personne en se servant des informations extraites du jeton, et de gérer les autorisations applicatives.

Schéma de description de l'architecture de l'api

Base de données

Coté base de données, nous avons utilisé MySQL puis PostGreSQL. Dans les deux cas, avec AWS RDS et sa surcouche Aurora. Ces services permettent de gérer simplement le serveur de base de données et assurent pleinement la réplication des données sur plusieurs instances.

La gestion des sauvegardes est aussi assurée par ces services. Ils se font sous la forme de d'un instantané (AKA snapshot) de la machine. Ces sauvegardes ne sont exploitables que sur un environnement AWS et ne sont utile qu'en cas de crash.

Pour de la sauvegarde régulière et exploitable par les développeurs, nous avons mis en place notre propre système à base de dump des données. Nous avons utilisé pgdump-aws-lambda avec un appel régulier programmé dans AWS Cloudwatch. Le temps d'éxécution d'un Lambda étant limité à 5 minutes, nous avions prévue de passer a un conteneur déployer sur AWS ECS, dans une instance Spot pour dépasser cette limite de temps.

Indicateurs

Nous utilisons un outil externe nommé Sisense pour produire les indicateurs visibles dans l'application. Cet outil, pour suivre l'évolution de son contenue dois d'accéder à la base de données applicative pour reconstruire ses données et visualisations. N'étant pas omniscient, il doit aussi être notifié à chaque modification pour venir la prendre en considération.

L'application et Sisense se trouvant dans un VPC différent, un appairage a été mis en place pour que les deux puissent communiquer.

Schéma de description de l'appairage de VPC

Finalement, un gestionnaire de file d'attente (nommé sync manager) a dû être mis en place. En effet, Sisense ne sais pas gérer plusieurs demandes de reconstruction en parallèle ni dé-doublonner les demandes similaire. De plus, aucune notification de fin de construction n'est disponible, ce service a donc permis de surveiller l'état des constructions pour détecter les fins de traitement et d'en notifier l'utilisateur.

Simulation

Le logiciel se base sur des données fournies par un système de calcul plus lourd et long. L'utilisateur décrit une situation dans l'application, une simulation est lancée, elle produit des résultats et ces résultats sont visibles dans l'application et les indicateurs.

Cette simulation est réalisée par un modèle développé en interne et encapsulé dans une image Docker. Elle prend l'ensemble de ses paramètres en entrée et produit un résultat. Toutefois, un conteneur n'est capable de réaliser qu'un seul calcul à la fois et ce dernier utilise l'intégralité des ressources disponibles (mémoire vive et processeur). De plus, ce calcul prend du temps (en moyenne 15 secondes).

L'approche API HTTP permettant de dialoguer avec le modèle à été tentée, mais pour les raisons suivantes n'a pas été concluante. Ssi vous arrivez derrière 20 autres demandes, vous attendrez que ces demandes soient dépilées avant que vous puissiez avoir votre résultat. Il est probable qu'à ce moment, le client ai arrêté d'attendre la réponse (timeout), ce qui est probablement le cas avec un cycle de vie Lambda. Finalement, si la machine de calcul plante, elle disparait avec toutes les demandes de calcul en attente.

La file d'attente dois donc être déportée et un système de suivi des messages. Encore une fois AWS propose un service de file d'attente nommé SQS (Simple Queue Service) qui correspond à notre besoin. L'API viens donc mettre dans cette file des messages correspondant aux traitements à réaliser. Le modèle n'a plus qu'a récupérer le message, le traiter et de confirmer le bon traitement du message (afin qu'il ne soit pas traité deux fois). Si le traitement du message échoue et afin d'éviter les boucles, et après un certain nombre de tentatives, il entre dans une autre file d'attente dédiée aux échecs (dead letters).

Le résultat produit ne pouvant plus repartir par le canal d'entrée, il est envoyé à une autre Lambda qui a pour seul rôle de récupérer ces données, de les traités, de les insérer en base de données et de notifier les autres éléments du changement (Sisense, les utilisateurs...).

Le modèle est déployé en utilisant le service Elastic Beanstalk avec son système de déploiement Docker. Ce système permet de définir des règles de passage à l'échelle automatique. Ici, nous le faisons en fonction de l'activité CPU, si la moyenne du groupe de machine est trop élevée alors nous ajoutons des machines, en revanche, si l'activité réduit, nous réduisons le nombre de machines.

Schéma de description de l'architecture de la simulation

TL;DR

En résumé voici à peu de choses près le schéma global du système applicatif.

Schéma complet de l'architecture du système

Construction

La création et mise en place de cette application à mobilisé de nombreuses compétences, à des temporalités différentes. Un certain nombre de processus ont donc été mis en place.

Equipe

L'équipe est composée de : 6 développeurs, 3 modélisateurs, 1 lead dev, 1 spécialiste infrastructure, 1 spécialiste données, 1 relai avec l'équipe QA, 1 responsable spécifications.

Ce projet est développé principalement en JavaScript avec Node.js et la librairie Express.js pour le back-end et VueJS pour le front-end. Ce choix de langage commun a permis de favoriser les échanges entre les développeurs back-end et front-end, ainsi que de favoriser le partage de compétences.

L'aspect full-stack de l'équipe a eu une valeur importante. Sans forcément maîtriser chaque brique, tous les membres ont étés amenés à être formé et a travailler avec chaque niveau d'applicatif (front, authentification, back, simulation, data, infrastructure...).

Agilité

Toute l'équipe travaille en méthodologie agile avec des sprint de 3 semaines et des releases de 3 sprints. En dehors des cérémonies habituelles (daily standup, print review, retrospective...), l'équipe est responsable et autonome sur son backlog. Elle gère ses items (entrée/sortie), gère l'alimentation du backlog lors de session de backlog grooming, l'estimation, la planification... Elle est aussi capable de mettre en attente une fonctionnalité trop peu mature.

Formation

Nous étions sur un grand nombre de nouvelles technologies. Une partie de l'équipe de départ n'avais jamais fais de VueJS, d'Express.js et encore moins de serverless. Il y avait donc là un réel challenge. Une transmission initiale de compétence de la part de ceux qui avaient la connaissance ou avaient participé aux PoCs. Soyons honnêtes, même avec toutes les formations imaginables, nous ne pouvions pas tout couvrir dès le départ. Nous avons donc décider d'investir suffisamment pour que l'équipe ait une compréhension suffisante pour démarrer le projet, expérimenter avec les technologies, rencontrer des difficultés, en trouver la solution...

Régulièrement, nous faisons un point pour repasser ensemble sur les difficultés rencontrées et expliquer la solution mise en place. Ce mécanisme et capitalisation par le partage d'information ont permis à tout le monde de monter en compétences relativement rapidement.

Rapidement, l'équipe a évolué, avec plus de ressources et des niveaux d'entrée divers. Comme nous n'avions que peu (pour ne pas dire pas du tout) documenté nos procédures et trouvailles. Ces arrivées dans l'équipe n'ont pas toujours été simple. Tant pour nous que pour eux. De la même manière, certains membres sont partis avec leur savoir, et même s'il a été partagé à un moment, la mémoire s'efface.

A postériori, nous aurions dû nettement plus travailler sur la documentation de tout cela (et bien d'autre), dans un format lisible et digérable en peu de temps.

Validation

Chaque évolution, que ce soit une fonctionnalité ou d'une correction du bug, est réalisé dans une branche séparée. Des tests unitaires y sont associés. Quand elle est jugée fonctionnelle et mature, une requête d'intégration est ouverte sur le système de gestion de version (Merge Request ou Pull Request). Elle fait état des changements réalisés, de leurs raisons, de l'objectif atteint et des critères de validation.

Les autres membres de l'équipe prennent ensuite le temps de relire le code produit. De discuter des différentes approche et risques d'effet de bord, jusqu'à tomber d'accord sur un code acceptable. Quand 50 % de l'équipe a revu la requête et qu'aucun refus n'a été réalisé, un test manuel est réalisé par un membre de l'équipe pour confirmer que les critères de validation sont validés. Le code ainsi validé est ensuite intégré dans l'application en fonction de la planification qui a été définie.

Données

La base de donnée est relationnelle et traite une grande quantité de données géographique. Nous avons utilisé PostGreSQL avec son extension PostGIS. Malheureusement, AWS Aurora ne proposant pas les bases PostGreSQL en 2017, nous avons commencé les travaux avec MySQL, qui, jusqu'à un certain point, a très bien fait le travail.

En milieu d'année 2018, la migration de données de MySQL vers PostGreSQL a été réalisée avec l'outil pgLoader. Cette migration nous a demandé peu de configuration, mais beaucoup de tests et c'est finalement bien déroulée. Elle a notamment été simplifiée grâce à l'utilisation dans le code du back d'un ORM (Object Relationnal Mapping) nommé Sequelize qui est compatible avec les deux bases de données. Par chance, nous avions identifié cette migration dès le début.

Communication

L'équipe n'a pas eu la chance de travailler dans un même bureau au cours du projet. En effet, non seulement l'entreprise est étalée sur deux sites Lyon et Rennes avec des membres de chaque côté, mais l'équipe est composée de prestataires externes travaillant complètement à distance.

Des méthodes de communication efficaces ont dû être mises en place. De part la nature de l'entreprise, nous avions déjà quelques habitudes, notamment sur la visioconférence et le tchat avec Hangouts. Nous avons utilisé Slack avec l'équipe et avons définis un enssemble de procédure permettant à l'ensemble de l'équipe de savoir, si une personne est disponible ou dérangeable, de lancer rapidement des visioconférence, de suivre le flux de développement, de poser des question à la responsable des spécifications ou au Product Owner, etc...

Nous utilisons abondamment le statut Slack afin de faire ressortir notre statut actuel. Pour les utilisateurs d'IRC rien de nouveau. Nous avions des commandes personnalisés comme /cofee, /meeting ou encore /focus permettant de rapidement passer d'un état à l'autre. Coté visioconférence, l'entreprise utilisant déjà les outils de la suite Google, nous avons continué à utiliser Hangouts.

À côté de ça, l'ensemble des réunions de projets et cérémonies agile se déroulent dans des salles de visioconférence. Avec des tableaux partagés permettant d'afficher l'état du sprint, le dessin de rétrospective... Cette expérience à aussi pour effet secondaire d'ouvrir la possibilité de télétravail aux employés de l'entreprise 🏆.

Conclusion

J'ai omis volontairement les couplets sur les groupes de sécurité, la testabilité et le déploiement. Non pas qu'ils soient inintéressants, au contraire. Mais l'article est déjà suffisamment riche et ces sujets pouvant faire l'objet d'un article à part entière, je l'ai mis de côté. En quelques mots :

  • Chaque brique applicative est dans un groupe de sécurité et des règles définissent les modes de communication entre les groupes,
  • Nous utilisons des tests unitaires et d'intégration que nous jouons dans un environnement de CI pour chaque Pull Request,
  • Nous déployons l'infrastructure avec Terraform et le code avec Code Build.

Cette architecture et organisation d'équipe ont premièrement été mises en place sur une application nommée GWD en 2017 et mise en production à compter de mars 2018 (environs 6 mois plus tard.). La rapidité de développement et qualité perçue du rendu a amené l'entreprise à choisir cette même architecture pour le produit Urban Economics en septembre 2018 puis à nouveau en juin 2019 avec une autre application.

Malgré cette dynamique créatrice, l'entreprise a tiré sa révérence en août 2019. Les produits réalisés tombent eux aussi malheureusement dans l'oubli 😢, mais les savoir acquis et l'expérience, eux restent 😉.