12 Factor App
Qu’est ce que le « 12 Factor App » ?
Le « 12 Factor app » est un manifeste qui propose 12 bonnes pratiques concernant le développement d’applications web. Ce manifeste, écrit par Adam Wiggins (co-fondateur d’Heroku), est né de ses observations et de son expérience dans le développement et le déploiement d’applications web.
Ce manifeste s’applique à tous les langages et toutes les plateformes, c’est pourquoi il se contente de décrire les décisions de conception de haut niveau sans donner de détail sur l’implémentation.
Dans ce post, nous allons parcourir ensemble ces 12 facteurs, en extraire le concept et tenter de l’appliquer au monde PHP.
L’application « 12 Factor »
I Base de code
Une base de code suivie avec un système de contrôle de version, plusieurs déploiements
Ce premier facteur nous dit que tout le code doit se trouver à un seul emplacement centralisé et versionné. On y apprend également qu’une base de code est à l’origine de plusieurs déploiements, et qu’un déploiement ne doit pas être lié à plusieurs base de code. Dans le cas où du code serait commun à plusieurs applications, alors celui-ci doit être factorisé puis extrait sous forme de librairie.
En utilisant cette approche, nous avons une base de code qui ne contient que le code de l’application, et l’utilisation d’un gestionnaire de dépendances permet de récupérer les bibliothèques supplémentaires nécessaires au fonctionnement de l’application.
La solution évidente pour mettre en oeuvre ce facteur, est l’utilisation d’un système de contrôle de version (VCS) pour gérer les modifications apportées au référentiel central.
II Dépendances
Déclarez explicitement et isolez les dépendances
Les applications web modernes sont rarement conçues comme des monolithes à usage unique. Elles sont au contraire conçues pour tirer partie de librairies (généralement externes).
Cependant l’utilisation de ces librairies externes peut introduire des problèmes :
- Si nous ajoutons la librairie au projet, la base de code de ce dernier va considérablement augmenter et rendre compliquée la mise à jour d’une de ces librairies sans affecter notre application principale.
- Si nous n’ajoutons pas la librairie au projet, c’est le déploiement du code qui devient difficile. Où trouver la librairie ? Quelle version utiliser ?
Nous pouvons résoudre ces problèmes en utilisant un outil de gestion de dépendances. Les gestionnaires de dépendances visent à rendre la composition d’application beaucoup plus simple en récupérant et gérant les librairies pour nous. Il suffit de lister les dépendances ainsi que les versions que requiert notre application et l’outil de gestion fait le reste pour nous.
Dans le monde PHP, le gestionnaire de dépendances le plus utilisé est composer, fortement inspiré de npm pour node et bundler pour ruby, il installe les dépendances requise dans un dossier vendor. Il crée également dans ce dossier vendor des fichiers d’autoload permettant de faire un mapping dépendance-namespace et ainsi les rendre disponibles dans l’application. La définition de ces dépendances se fait dans le fichier composer.json et les versions réellement installées des librairies sont trackées dans le composer.lock afin d’installer exactement les mêmes versions sur tous les déploiements.
III Configuration
Stockez la configuration dans l’environnement
Les applications web nécessitent de la configuration. Que ce soit pour spécifier l’emplacement des ressources attachées (base de données, api, …), le réglage des préférences de l’application ou l’environnement cible (dev, prod, …).
Pour configurer notre application, une approche souvent utilisée est le fichier de configuration; un script simple qui définit certaines variables. Mais cette approche n’est pas idéale pour plusieurs raisons :
- Généralement, nous voulons utiliser des paramètres basés sur l’environnement. Nous ne souhaitons pas que les serveurs de développement accèdent aux bases de données de production. En utilisant l’approche du fichier de configuration, nous aurions besoin de plusieurs fichiers ainsi que d’un moyen de choisir le fichier à utiliser.
- Les fichiers de configurations contiennent des informations sensibles que l’on ne souhaite pas ajouter à la base de code.
Une meilleure approche consiste à utiliser des variables d’environnement configurées au niveau du serveur web ou du système. Ces variables sont donc spécifiques au serveur sur lequel l’application s’exécute.
En PHP, les valeurs de ces variables d’environnement peuvent être récupérées au runtime par la fonction getenv. Le framework Symfony va plus loin en transformant toutes les variables d’environnements commençant par SYMFONY en paramètre, utilisable dans la configuration de l’application.
IV Services externes
Traitez les services externes comme des ressources attachées
L’application ne fait pas de distinction entre les services locaux et les services tiers. Le remplacement d’une base de données PostgreSQL locale par une autre gérée chez un tiers doit pouvoir se faire sans modifications dans le code de l’application, seul la configuration doit changer.
En conséquence, le remplacement des services externes devient trivial. Cela implique également que l’application doive interagir avec ces services externes de la même façon, que l’on soit en environnement de dev ou de prod.
Prenons l’exemple d’une application Symfony (Standard Edition) avec Doctrine, les informations de connexion à la base de données sont récupérées depuis la configuration de l’application. Le switch d’une base de données locale à une base de données distante se fait par simple changement des informations de connexion à celle-ci et le code permettant cette communication n’a pas besoin de subir de modifications.
V Assemblez (build), Publiez (releases), Exécutez (run)
Séparez strictement les étapes de build et d’exécution
Le processus de déploiement d’une application est divisé en trois étapes distinctes :
- L’étape d’assemblage (build), est une étape de transformation qui convertit une base de code en un ensemble exécutable.
- L’étape de publication (release), contient à la fois la construction et la configuration et est prête pour une exécution immédiate dans l’environnement d’exécution.
- L’étape d’exécution (run), exécute l’application dans un environnement d’exécution, en lançant un ensemble de processus de l’application dans une version donnée.
Cela peut sembler compliqué, mais l’idée clé est simple : nous devons séparer le processus de déploiement en deux phases distinctes. La première phase est de transformer la base de code en un livrable (build). La deuxième phase consiste à mettre à jour l’environnement pour que le nouveau livrable soit utilisé (release + run).
Nous faisons cela pour trois raisons :
- Les modifications en environnement de production sont impossibles. Toutes modification doit être faite dans la base de code, qui sera ensuite transformée en livrable puis exécutée.
- Une simplification de l’action de rollback (retour à une version antérieure) de l’application en cas de problème.
- La mise en place de ce workflow permet d’automatiser les déploiements, ce qui les rend moins coûteux et en augmente la fréquence. Qui dit déploiements plus fréquents dit également boucle de feedback plus courte, permettant le développement d’un meilleur logiciel et moins de stress lors des déploiements.
VI Processus
Exécutez l’application comme un ou plusieurs processus sans état
L’application doit être conçue pour que chaque requête puisse être traitée par des processus différents, non liés et non communicants. Ce modèle de conception « sans état et sans partage » (stateless) est indispensable à la scalabilité horizontale.
Considérons le modèle client-serveur. Dans ce modèle, nous avons des serveurs qui gèrent les demandes des clients. Si nous voulons augmenter le nombre de requêtes que le système peut gérer, nous avons deux options :
- Augmenter la puissance de traitement des serveurs, augmentant ainsi le nombre de demandes que chacun peut traiter. On parle alors de scalabilité verticale.
- Ajouter plus de serveurs. On parle alors de scalabilité horizontale.
La scalabilité verticale est plus simple à court terme, mais devient rapidement coûteuse et affiche des rendements décroissants en termes de performance. La scalabilité horizontale, d’autre part, est assez linéaire en termes de coût vs performance et permet d’être tolérant aux pannes.
VII Associations de ports
Exportez les services via des associations de ports
Dans certains cas, les applications web sont exécutées dans un serveur web. C’est par exemple le cas des applications web PHP fonctionnant à l’intérieur d’Apache, ou des applications web Java fonctionnant à l’intérieur de Tomcat.
Les applications « 12 factor » sont auto-suffisantes, et ne se basent pas sur l’injection au moment de l’exécution d’un serveur web dans l’environnement. C’est l’application web qui expose HTTP comme un service en l’associant à un port et écoute les requêtes qui arrivent sur ce port.
On peut par exemple citer les librairies de serveur web Thin pour Ruby, Tornado pour Python ou encore Jetty pour Java.
Côté PHP, on a depuis la version 5.4.0 la possibilité de lancer un serveur web depuis PHP en CLI. Cependant la documentation PHP nous met en garde concernant ce serveur web intégré.
Avertissement
Ce serveur web a été prévu pour aider dans le développement des applications. Il peut également être utile pour les tests, et pour les démonstrations d’applications qui sont exécutées dans des environnements contrôlés. Mais par contre, il n’a pas été conçu pour être un serveur web complet. Aussi, il ne devrait pas être utilisé dans un réseau public.
Vous l’aurez compris, PHP ne permet pas de satisfaire ce point en conservant un mode de fonctionnement intégré au serveur web et non l’inverse.
VIII Concurrence
Grossissez à l’aide du modèle de processus
Les processus de l’application sont considérés comme des élèves modèle. Cela signifie que, pour mettre à l’échelle notre application, nous créons plus de processus afin de tenir la charge. Nous utilisons également différent types de processus pour différents types de tâches.
L’avantage considérable de la concurrence sont les performances versus le coût. La scalabilité par la parallélisation, donc horizontale, permet une croissance quasi-linéaire en termes de coûts et de demandes traitées (à condition que l’application soit conçue pour cette stratégie de croissance).
Un autre avantage évident est la tolérance aux pannes. Une défaillance de processus ne paralysera pas l’application.
Enfin, la parallélisation offre une application plus rapide et réactive. La possibilité de différer le traitement du thread principal signifie que le serveur web n’a pas besoin d’attendre que les actions de fond se terminent avant d’envoyer la réponse HTTP au client.
IX Jetable
Maximisez la robustesse avec des démarrages rapides et des arrêts gracieux (graceful)
Les processus sont dit jetables, c’est à dire qu’ils peuvent être démarrés ou stoppés rapidement à n’importe quel instant. Cela permet d’ajouter des processus très rapidement en cas d’augmentation de la charge sur l’application et au contraire d’en supprimer quand la charge diminue.
Trois points sont particulièrement importants ici :
- Minimiser le temps de démarrage des processus. Idéalement, un processus prend quelques secondes entre le moment où une commande le lance et celui où il est en marche et prêt à recevoir des requêtes. Un court temps de démarrage rend plus agile les processus de release et de scalabilité horizontale.
- Éteindre gracieusement les processus lorsqu’ils reçoivent un signal SIGTERM. Pour un processus web, s’éteindre en douceur se fait en arrêtant d’écouter sur le port de service (refusant, par la même occasion, toute nouvelle requête), en permettant à la requête courante de se terminer, et en quittant ensuite. Pour un processus de worker, s’éteindre gracieusement est réalisé en renvoyant le travail en cours dans la file de travaux ; par exemple un NACK en RabbitMQ.
- Robustesse face aux morts subites, dans le cas d’une panne du hardware. Bien que ce soit bien moins courant qu’un arrêt gracieux, cela peut arriver malgré tout. L’approche recommandée est l’utilisation d’un broker de message tel que RabbitMQ, capable de renvoyer les tâches dans la file lorsqu’un client se déconnecte ou ne répond plus.
X Parité dev/prod
Gardez le développement, la validation et la production aussi proches que possible
Les applications « 12 factor » sont conçues pour le déploiement continu et la conservation d’un fossé étroit entre les différents environnements.
Le déploiement continu permet de :
- Réduire le delta temporel : un développeur peut écrire du code et le déployer quelques heures ou même juste quelques minutes plus tard, là où il devait attendre plusieurs jours ou semaines avant de pouvoir mettre en production son développement.
- Améliorer le workflow : les personnes qui écrivent le code sont impliquées dans son déploiement ; ce qui les sensiblise également à la surveillance du comportement en production de son code.
L’autre point à ne pas négliger concerne les outils.
Si l’environnement de production utilise PHP 5.6 et MySQL sur Linux, l’environnement de développements doit utiliser les mêmes outils. Si le développeur utilise PHP 7.1 et Sqlite sur OS X, il augmente la probabilité d’avoir des comportement non prévus en production.
XI Logs
Traitez les logs comme des flux d’événements
Les logs sont des flux d’événements, ordonnés dans le temps, collectés à travers les flux de sortie de tous les processus et services externes qui tournent. Les logs, dans leur forme brute, sont au format texte avec un événement par ligne, n’ont pas de début ou de fin fixe, mais se remplissent en continu tant que l’application est en marche.
Une application « 12 factor » ne s’inquiète jamais du routage ou du stockage de ses flux de sortie. Elle ne devrait pas tenter d’écrire ou de gérer les fichiers de logs. À la place, chaque processus qui tourne écrit ses flux d’événements, sans tampon, vers stdout, la sortie standard.
Par déploiement, les flux de chaque processus seront capturés par leur environnement d’exécution, assemblés avec les autres flux de l’application, et routés vers une ou plusieurs destinations pour un visionnage et un archivage de longue durée. Le lieu d’archivage n’est pas visible et ne peut être configuré par l’application : ils sont complètements gérés par l’environnement d’exécution.
XII Processus d’administration
Lancez les processus d’administration et de maintenance comme des one-off-processes
La formation de processus est la liste des processus qui sont utilisés pour le fonctionnement normal de l’application (comme gérer les requêtes web) lorsqu’elle tourne. Les développeurs vont souvent vouloir effectuer des tâches occasionnelles d’administration ou de maintenance, comme :
- Lancer les migrations de base de données.
- Lancer une console (également appelée terminal REPL).
- Exécuter des scripts ponctuels inclus dans le dépôt de code.
Les processus ponctuels d’administration devraient être lancés dans un environnement identique à ceux des processus standards de l’application. Ils s’exécutent sur une release, en utilisant la même base de code et configuration que tout processus qui tourne pour cette release. Le code d’administration doit être livré avec le code de l’application afin d’éviter les problèmes de synchronisation.
En PHP, il est possible d’exéctuer un script ponctuel grâce à l’exécutable php suivi du chemin vers le fichier contenant le script (par ex. php scripts/fix_bad_records.php). Le composant console de Symfony facilite la création et le test de tâches d’administrations.