1) Un dépôt de code par application, pour plusieurs déploiements
Une base de code est un dépôt de code versionné (grâce à des outils comme Git, Mercurial ou SVN). A partir d’une base de code, on peut construire des releases immutables qui pourront être déployées dans différents environnements d’exécution. Un déploiement est une instance en fonctionnement de l’application.
Il y a deux anti-patterns assez simple. Tout d’abord, s’il y a plusieurs applications à partir d’une même base de code, c’est qu’il y a plusieurs applications et que celles-ci doivent être découpées et placées dans des bases de code distinctes. Ensuite, si une base de code est partagée par plusieurs applications, il serait judicieux de factoriser le code dans une bibliothèque (avec sa propre base de code) qui sera utilisée par les autres applications.
2) Déclaration et isolations des dépendances
Une application cloud ne suppose pas d’existence implicite de paquets et dépendances au niveau du système pendant son déploiement. Pour fonctionner correctement, l’idée est de déployer l’application avec ses dépendances. Elles forment ainsi un “tout”, une sorte de bundle. Pour se faire, le travail à réaliser consiste à déclarer les dépendances de façon explicite et précise en amont de la construction, et d’isoler ces dépendances au moment de l’exécution. Cela est permis grâce à des outils comme Maven (Java), NuGet (.NET), Npm (Javascript) ou encore Bundler (Ruby). Ils permettent de définir dans des manifestes des dépendances avec des versions précises, et ont pour rôle d’assurer ensuite que les dépendances sont satisfaites.
3) Stockage de la configuration dans l’environnement
L’idée consiste à séparer le code de la configuration. La configuration concerne tout ce qui peut varier d’un déploiement à l’autre : l’url d’un serveur SMTP, d’une base de données, les identifiants pour se connecter à une API comme Twitter ou Google Maps. Il ne faut pas les confondre avec des paramètres internes de l’application, qui restent invariables selon les déploiements. Pour pouvoir déployer une application sur plusieurs environnements, on ne peut pas définir les configurations dans le code. On a parfois tendance à mettre tout cela dans un fichier XML, mais cela a des limites en termes de flexibilité. Ce fichier n’est pas amené à être modifié, et donc les configurations seront les mêmes pour chaque déploiement.
De plus, des données sensibles telles que des credentials ne devraient pas apparaître clairement dans du code ou bien dans un fichier de configuration, pour des questions de sécurité principalement. D’ailleurs, un test intéressant pour évaluer si les configurations sont externalisées consiste à imaginer la mise de votre code en libre accès demain. Est-ce que cela révélerait des informations sensibles ?
La piste à suivre consiste à placer cette configuration dans des variables d’environnement, qui seront injectées au moment de l’exécution.
4) Les services externes comme des ressources attachables et détachables
Une application Cloud dépend généralement de services externes (bases de données, stockage de fichiers, serveur SMTP, …) et s’y connecte via des ressources, généralement une URL et des credentials pour l’authentification. Nous venons de voir que ces ressources ne peuvent être renseignées dans le code, mais plutôt via des configurations externes. On a donc prévu dans le code que le lien avec un service externe se fera grâce à la configuration de l’environnement.
L’idée est de pouvoir détacher et attacher une ressource externe à tout moment. Si un des services externes est défaillant, et bien on modifie simplement la ressource au niveau de l’environnement Cloud. On ne doit pas avoir besoin de toucher au code pour pouvoir changer de service externe par un autre.
5) Assemblage, publication et exécution
Ce principe soutient la stricte séparation des étapes de constructions et d’exécution. A partir d’une base de code, on produit un artifact (un build, comme par exemple un fichier JAR pour du code Java), auquel on adosse des éléments de configuration externes qui produisent une release immutable. Cette release est prévue pour être déployée dans environnement d’exécution (que ce soit dev, staging, production, …). Enfin, la phase d’exécution fait fonctionner l’application dans l’environnement d’exécution via le lancement d’un ou plusieurs processus.
L’objectif de la séparation des phases est surtout de maximiser la capacité de livraison, d’épargner aux équipes de la méfiance lors des mises en production grâce à des gains de confiance dans la chaîne de production. C’est aussi utile en production, où les outils de déploiement permettent des opérations de rollbacks pour revenir aux étapes précédentes en cas de problèmes. Toute cette étape sera grandement facilitée par la mise en place d’un pipeline de CI/CD (Continuous Intégration + Continuous Delivery et Deployment). Il est donc important de versionner et d’archiver chaque release.
6) Des processus sans états
Chaque processus est sans état et ne fait ainsi aucune supposition sur le contenu de la mémoire avant de traiter une requête, ni sur son contenu après la requête. L’idée est donc qu’un état ne devrait pas être maintenue par votre application. Qui dit cloud dit passage à l’échelle, avec des multiplications de processus pour soutenir la charge. Ainsi, avec plusieurs processus qui exécuteront la même tâche, on ne peut pas supposer qu’une future requête soit traitée par le même processus.
Si un de ces processus a besoin de stocker de la donnée, cela se fera via un service externe. En effet, les données produites par les processus sont souvent volatiles (mises en cache ou écrites sur le disque temporairement). Si une transaction en 3 requêtes doit être effectuée, il faut partir du principe que les 3 seront traitées par des processus différents, et que cela ne doit pas poser problème.
7) Exposition des services par des ports
L’idée est qu’une application 12-factor est auto-suffisante et ne se base pas sur l’injection d’un serveur web au moment de l’exécution (Serveur Apache, Tomcat)… C’est l’application qui expose elle-même le port, généralement HTTP. Et c’est le Cloud provider qui devrait prendre en charge l’assignation du port et le mapping avec l’extérieur, notamment pour gérer efficacement le routage.
Votre application est composée d’un ou plusieurs services, et chacun d’entre eux est accessible depuis l’extérieur et par d’autres services via une URL et un port. Un service peut devenir un service externe pour une autre application.
8) Concurrence et processus indépendants
Pour pouvoir tirer parti de l’élasticité du Cloud, votre application doit pouvoir se décomposer en un ensemble de plusieurs processus indépendants les uns des autres, certains pouvant s’exécuter en concurrence des autres (ou plutôt, on coopération). De ce fait, la perspective d’un passage à l’échelle horizontale offre la capacité à votre application de supporter une charge plus importante, en ajoutant des processus (workers) supplémentaires pour les tâches qui nécessitent une charge.
Si votre application comprend 2 services métiers Users et Orders, et que le service Orders est 10 fois plus sollicité que Users, la séparation des 2 services en 2 processus distincts (ex : 2 micro-services) permet de de déployer 1 processus Users et pourquoi pas jusqu’à 10 processus Orders. Ces derniers sont concurrents car il s’exécutent dans la même infrastructure (ex : un cluster K8S), néanmoins ils coopèrent car chacun peut recevoir un traitement, si les autres ne le peuvent pas.
9) Démarrages rapides et arrêts gracieux
Dans le Cloud, les processus sont perçus comme volatiles et jetables, ils peuvent être démarrés ou stoppés à tout moment. Les processus doivent donc être prêts à l’emploi rapidement : s’ils sont lancés pour des besoins de montée en charge, il est important d’être opérationnel rapidement pour éviter la saturation des instances en cours (ou le déni si aucune ne fonctionne). En complément, les développeurs doivent prévoir des arrêts gracieux, c’est-à-dire prévoir les cas où l’application est en train d’être déconnectée ou est en train de crasher : il est préférable d’intercepter ces signaux pour que le travail en cours soit renvoyé dans une file de travaux, afin de ne pas corrompre des données.
10) Moins de fossé entre le développement et la production
Il s’agit d’éviter les fossés entre les contextes de développement (lorsqu’on écrit du code et qu’on déploie localement l’application) et de production (lorsque l’application est déployée pour les utilisateurs). Ces disparités peuvent exister lorsque les personnes qui œuvrent dans chaque contexte ne collaborent pas efficacement ou encore lorsqu’elles n’utilisent pas les mêmes outils (si la base de données locale d’un développeur n’est pas la même que celle utilisée en production).
Les applications 12-factor sont conçues pour du déploiement continu, d’où le besoin de réduire au maximum ce fossé entre le développement et la production. Les grandes lignes directrices sont de faire en sorte qu’une modification de code puisse être rapidement envoyée en production, que les personnes impliquées dans le développement le soient aussi dans le déploiement, et enfin que les outils utilisés dans les deux contextes soient le plus proche possible. C’est d’ailleurs très lié aux principes DevOps.
11) Traitement des logs comme des flux
Les logs sont des flux d’événements qui retracent le comportement d’une application. Un événement est une ligne de texte brute et qui est datée. Ces logs sont généralement écrits dans des fichiers sur le disque dur. Le code qui permet cela est généralement embarqué dans l’application. Celle-ci est ainsi responsable de la production et du stockage des logs. Les logs ont pour finalité d’être traités et analysés a posteriori par un service externe. Cependant, l’écriture sur disque ne fait pas de sens car cette ressource est éphémère dans le Cloud, et qu’on a aucune idée d’où nos applications s’exécutent (puisqu’elles peuvent être lancées dans des instances créées dynamiquement par le Cloud Provider).
L’idée est ici de décharger l’application de toute la phase de stockage des logs. L’application doit générer des événements sur sa sortie standard, point. Il revient à l’environnement d’exécution de collecter, assembler et stocker ces logs. On peut imaginer utiliser des services externes tels que Logstash pour router les flux de logs de différentes applications vers différents systèmes de stockage, afin qu’ils soient ensuite traités. Et finalement, cela permet de recentrer le code de l’application sur le coeur de métier.
12) Les processus d’administration
Des opérations de maintenance telles que des migrations de base de données, des scripts de mises à jours, sont parfois nécessaires dans des environnements de production. Ces processus d’administration devraient être des opérations ponctuelles et uniques, et surtout exécutés dans le même environnement d’exécution que celui de l’application. Le code implémentant ces opérations doit être livré avec le code de l’application afin d’éviter les problèmes de synchronisation.
Pour conclure
Les 2 questions qui viennent à l’esprit suite à ça sont :
- Est-ce que tous ces principes doivent être suivis pour que votre application soit façonnée pour le Cloud ?
- Est-ce que ces principes font tous consensus ?
Et bien, pour répondre vite je dirais non et non. Mais c’est la solution facile, je n’aime pas trop ça. En fait, on peut imaginer que beaucoup d’applications Cloud existantes ne respectent pas ces principes et arrivent à tourner malgré tout. Mais est-ce vraiment le seul critère qui compte ? Ces 12 points sont avant tout une base de réflexion : il faut le voir comme un exercice où on fait l’état des lieux sur notre application Cloud et on essaye de comprendre pourquoi on ne respecte pas totalement tel ou tel point. Pour aller plus loin, on retrouve beaucoup de contenus sur le Web qui discute de ces twelve-factor appen proposant des visions et critiques différentes. C’est toujours intéressant d’opposer ces points de vue !
Ressources
Cédric Teyton