=== 12 facteurs Pour la méthode de travail et de développement, nous allons nous baser sur les https://12factor.net/fr/[The Twelve-factor App] - ou plus simplement les *12 facteurs*. L'idée derrière cette méthode, et indépendamment des langages de développement utilisés, consiste à suivre un ensemble de douze concepts, afin de: . *Faciliter la mise en place de phases d'automatisation*; plus concrètement, de faciliter les mises à jour applicatives, simplifier la gestion de l'hôte, diminuer la divergence entre les différents environnements d'exécution et offrir la possibilité d'intégrer le projet dans un processus d'https://en.wikipedia.org/wiki/Continuous_integration[intégration continue] ou link:https://en.wikipedia.org/wiki/Continuous_deployment[déploiement continu] . *Faciliter la mise à pied de nouveaux développeurs ou de personnes souhaitant rejoindre le projet*, dans la mesure où la mise à disposition d'un environnement sera grandement facilitée. . *Minimiser les divergences entre les différents environnemens sur lesquels un projet pourrait être déployé* . *Augmenter l'agilité générale du projet*, en permettant une meilleure évolutivité architecturale et une meilleure mise à l'échelle - _Vous avez 5000 utilisateurs en plus? Ajoutez un serveur et on n'en parle plus ;-)_. En pratique, les points ci-dessus permettront de monter facilement un nouvel environnement - qu'il soit sur la machine du petit nouveau dans l'équipe, sur un serveur Azure/Heroku/Digital Ocean ou votre nouveau Raspberry Pi Zéro caché à la cave - et vous feront gagner un temps précieux. Pour reprendre de manière très brute les différentes idées derrière cette méthode, nous avons: *#1 - Une base de code unique, suivie par un système de contrôle de versions*. Chaque déploiement de l'application se basera sur cette source, afin de minimiser les différences que l'on pourrait trouver entre deux environnements d'un même projet. On utilisera un dépôt Git - Github, Gitlab, Gitea, ... Au choix. image::images/diagrams/12-factors-1.png[align=center] Comme l'explique Eran Messeri footnote:[The DevOps Handbook, Part V, Chapitre 20, Convert Local Discoveries into Global Improvements], ingénieur dans le groupe Google Developer Infrastructure, un des avantages d'utiliser un dépôt unique de sources, est qu'il permet un accès facile et rapide à la forme la plus à jour du code, sans aucun besoin de coordination. Ce dépôt ne sert pas seulement au code source, mais également à d'autres artefacts et formes de connaissance: * Standards de configuration (Chef recipes, Puppet manifests, ...) * Outils de déploiement * Standards de tests, y compris tout ce qui touche à la sécurité * Outils de déploiement de pipeline * Outils d'analyse et de monitoring * Tutoriaux *#2 - Déclarez explicitement les dépendances nécessaires au projet, et les isoler du reste du système lors de leur installation* Chaque installation ou configuration doit toujours être faite de la même manière, et doit pouvoir être répétée quel que soit l'environnement cible. Cela permet d'éviter que l'application n'utilise une dépendance qui soit déjà installée sur un des sytèmes de développement, et qu'elle soit difficile, voire impossible, à répercuter sur un autre environnement. Dans notre cas, cela pourra être fait au travers de https://pypi.org/project/pip/[PIP - Package Installer for Python] ou https://python-poetry.org/[Poetry]. Mais dans tous les cas, chaque application doit disposer d'un environnement sain, qui lui est assigné, et vu le peu de ressources que cela coûte, il ne faut pas s'en priver. Chaque dépendance pouvant être déclarée et épinglée dans un fichier, il suffira de créer un nouvel environment vierge, puis d'utiliser ce fichier comme paramètre pour installer les prérequis au bon fonctionnement de notre application et vérifier que cet environnement est bien reproductible. WARNING: Il est important de bien "épingler" les versions liées aux dépendances de l'application. Cela peut éviter des effets de bord comme une nouvelle version d'une librairie dans laquelle un bug aurait pu avoir été introduit.footnote:[Au conditionnel du futur plus-que-parfait antérieur. Mais ça arrive. Et tout le temps au mauvais moment.] *#3 - Sauver la configuration directement au niveau de l'environnement* Nous voulons éviter d'avoir à recompiler/redéployer l'application parce que: . l'adresse du serveur de messagerie a été modifiée, . un protocole a changé en cours de route . la base de données a été déplacée . ... En pratique, toute information susceptible de modifier un lien applicatif doit se trouver dans un fichier ou dans une variable d'environnement, et doit être facilement modifiable. En allant un pas plus loin, cela permettra de paramétrer facilement un container, en modifiant une variable de configuration qui spécifierait la base de données sur laquelle l'application devra se connecter. Toute clé de configuration (nom du serveur de base de données, adresse d'un service Web externe, clé d'API pour l'interrogation d'une ressource, ...) sera définie directement au niveau de l'hôte - à aucun moment, nous ne devons trouver un mot de passe en clair dans le dépôt source ou une valeur susceptible d'évoluer, écrite en dur dans le code. *#4 - Traiter les ressources externes comme des ressources attachées* Nous parlons de bases de données, de services de mise en cache, d'API externes, ... L'application doit être capable d'effectuer des changements au niveau de ces ressources sans que son code ne soit modifié. Nous parlons alors de *ressources attachées*, dont la présence est nécessaire au bon fonctionnement de l'application, mais pour lesquelles le *type* n'est pas obligatoirement défini. Nous voulons par exemple "une base de données" et "une mémoire cache", et pas "une base MariaDB et une instance Memcached". De cette manière, les ressources peuvent être attachées et détachées d'un déploiement à la volée. Si une base de données ne fonctionne pas correctement (problème matériel?), l'administrateur pourrait simplement restaurer un nouveau serveur à partir d'une précédente sauvegarde, et l'attacher à l'application sans que le code source ne soit modifié. une solution consiste à passer toutes ces informations (nom du serveur et type de base de données, clé d'authentification, ...) directement via des variables d'environnement. *#5 - Séparer proprement les phases de construction, de mise à disposition et d'exécution* . La *construction* (_build_) convertit un code source en un ensemble de fichiers exécutables, associé à une version et à une transaction dans le système de gestion de sources. . La *mise à disposition* (_release_) associe cet ensemble à une configuration prête à être exécutée, . tandis que la phase d'*exécution* (_run_) démarre les processus nécessaires au bon fonctionnement de l'application. Parmi les solutions possibles, nous pourrions nous pourrions nous baser sur les _releases_ de Gitea, sur un serveur d'artefacts ou sur https://fr.wikipedia.org/wiki/Capistrano_(logiciel)[Capistrano]. *#6 - Les processus d'exécution ne doivent rien connaître ou conserver de l'état de l'application* Toute information stockée en mémoire ou sur disque ne doit pas altérer le comportement futur de l'application, par exemple après un redémarrage non souhaité. Pratiquement, si l'application devait rencontrer un problème, nous pourrions la redémarrer sur un autre serveur. Toute information qui aurait été stockée durant l'exécution de l'application sur le premier hôte serait donc perdue. Si une réinitialisation devait être nécessaire, l'application ne devra pas compter sur la présence d'une information au niveau du nouveau système. Il serait également difficile d'appliquer une mise à l'échelle de l'application si une donnée indispensable à son fonctionnement devait se trouver sur une seule machine où elle est exécutée. *#7 - Autoriser la liaison d'un port de l'application à un port du système hôte* Les applications 12-factors sont auto-contenues et peuvent fonctionner en autonomie totale. L'idée est qu'elles puissent être joignables grâce à un mécanisme de ponts, où l'hôte effectue la redirection vers l'un des ports ouverts par l'application, typiquement, en HTTP ou via un autre protocole. image::images/diagrams/12-factors-7.png[align=center] *#8 - Faites confiance aux processus systèmes pour l'exécution de l'application* Comme décrit plus haut, l'application doit utiliser des processus _stateless_ (sans état). Nous pouvons créer et utiliser des processus supplémentaires pour tenir plus facilement une lourde charge, ou dédier des processus particuliers pour certaines tâches: requêtes HTTP _via_ des processus Web; _long-running_ jobs pour des processus asynchrones, ... Si cela existe au niveau du système, ne vous fatiguez pas: utilisez le. *#9 - Améliorer la robustesse de l'application grâce à des arrêts élégants et à des démarrages rapides* Par "arrêt élégant", nous voulons surtout éviter le `kill -9 ` ou tout autre arrêt brutal d'un processus qui nécessiterait une intervention urgente du superviseur. De cette manière, les requêtes en cours pourront se terminer au mieux, tandis que le démarrage rapide de nouveaux processus améliorera la balance d'un processus en cours d'extinction vers des processus tout frais. L'intégration de ces mécanismes dès les premières étapes de développement limitera les perturbations et facilitera la prise en compte d'arrêts inopinés (problème matériel, redémarrage du système hôte, etc.). *#10 - Conserver les différents environnements aussi similaires que possible, et limiter les divergences entre un environnement de développement et de production* L'exemple donné est un développeur qui utilise macOS, NGinx et SQLite, tandis que l'environnement de production tourne sur une CentOS avec Apache2 et PostgreSQL. L'idée derrière ce concept limite les divergences entre environnements, facilite les déploiements et limite la casse et la découverte de modules non compatibles dès les premières phases de développement. Pour vous donner un exemple tout bête, SQLite utilise un https://www.sqlite.org/datatype3.html[mécanisme de stockage dynamique], associée à la valeur plutôt qu'au schéma, _via_ un système d'affinités. Un autre moteur de base de données définira un schéma statique et rigide, où la valeur sera déterminée par son contenant. Un champ `URLField` proposé par Django a une longeur maximale par défaut de https://docs.djangoproject.com/en/3.1/ref/forms/fields/#django.forms.URLField[200 caractères]. Si vous faites vos développements sous SQLite et que vous rencontrez une URL de plus de 200 caractères, votre développement sera passera parfaitement bien, mais plantera en production (ou en _staging_, si vous faites les choses un peu mieux) parce que les données seront tronquées... Conserver des environements similaires limite ce genre de désagréments. *#11 - Gérer les journeaux d'évènements comme des flux* Une application ne doit jamais se soucier de l'endroit où ses évènements seront écrits, mais simplement de les envoyer sur la sortie `stdout`. De cette manière, que nous soyons en développement sur le poste d'un développeur avec une sortie console ou sur une machine de production avec un envoi vers une instance https://www.graylog.org/[Greylog] ou https://sentry.io/welcome/[Sentry], le routage des journaux sera réalisé en fonction de sa nécessité et de sa criticité, et non pas parce que le développeur l'a spécifié en dur dans son code. *#12 - Isoler les tâches administratives du reste de l'application* Evitez qu'une migration ne puisse être démarrée depuis une URL de l'application, ou qu'un envoi massif de notifications ne soit accessible pour n'importe quel utilisateur: les tâches administratives ne doivent être accessibles qu'à un administrateur. Les applications 12facteurs favorisent les langages qui mettent un environnement REPL (pour _Read_, _Eval_, _Print_ et _Loop_) à disposition (au hasard: https://pythonprogramminglanguage.com/repl/[Python] ou https://kotlinlang.org/[Kotlin]), ce qui facilite les étapes de maintenance. === Design for operations through codified non-functional requirements Pour paraphraser une section du DevOps Handbook (Part V, Chapitre 20, Convert Local Discoveries into Global Improvements (page 293-294), une application devient nettement plus maintenable dès lors que l'équipe de développement suit de près les différentes étapes de sa conception, de la demande jusqu'à son aboutissement en production. Au fur et à mesure que le code est délibérément construit pour être maintenable, nous gagnons en rapidité, en qualité et en fiabilité de déploiement et les tâches liées aux opérations en sont facilitées. Ces prérequis sont les suivants: * Activation d'une télémétrie suffisante dans les applications et les environnements. * Conservation précise des dépendances nécessaires * Résilience des services et plantage élégant (i.e. *sans finir sur un SEGFAULT avec l'OS dans les choux et un écran bleu*) * Compatibilité entre les différentes versions (n+1, ...) * Gestion de l'espace de stockage associé à un environnement (pour éviter d'avoir un environnement de production qui fait 157 Tera-octets) * Activation de la recherche dans les logs * Traces des requêtes provenant des utilisateurs, indépendamment des services utilisés * Centralisation de la configuration (*via* ZooKeeper, par exemple)