gwift-book/chapters/maintenability.tex

282 lines
22 KiB
TeX
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

\chapter{Fiabilité, évolutivité et maintenabilité}
\begin{quote}
The primary cost of maintenance is in spelunking and risk\cite[p. 139]{clean_architecture}
\end{quote}
Que l'utilisateur soit humain, bot automatique ou client Web, la finalité d'un développement est de fournir des applications résilientes, pouvant être mises à l'échelle et maintenables \cite[p. 6]{data_intensive} :
\begin{enumerate}
\item
\textbf{La résilience} consiste à ce que l'application continue à fonctionner \textit{correctement}, c'est-à-dire à ce qu'elle fournisse un service correct au niveau de performance désiré, même quand les choses se passent mal.
Ceci signifie qu'un système a la capacité:
\begin{enumerate}
\item
D'anticiper certains types d'erreurs (ou en tout cas, de les gérer de manière propre), par exemple en proposant une solution de repli (\textit{fallback}) \footnote{Lors d'un incident AWS, Netlix proposait des recommandations statiques et ne se basait plus sur des recommandations personnalisées. Ceci leur a permis de tenir six heures avant de décréter être impacté. \cite{devops_handbook}}
\item
De se rendre rapidement indisponibles les composants qui posent problème, de manière à ce que ceux-ci n'entraînent pas tout le système avec eux (\textit{fail fast})
\item
De retirer certaines fonctionnalités non-critiques lorsqu'elles répondent plus lentement ou lorsqu'elles présentent un impact sur le reste de l'application (\textit{feature removal}).
\end{enumerate}
\item
\textbf{La mise à échelle} consiste à autoriser le système à \textit{grandir} - soit par le trafic pouvant être pris en charge, soit par son volume de données, soit par sa complexité.
\item
\textbf{La maintenabilité} consiste à faire en sorte que toute intervention puisse être réalisée de manière productive: au fil du temps, il est probable que plusieurs personnes se succèdent à travailler sur l'évolution d'une application, qu'ils travaillent sur sa conception ou sur son exploitation.
\end{enumerate}
Une manière de développer de telles applications consiste à suivre la méthodologie des \textbf{12 facteurs} \footnote{\url{https://12factor.net/fr/}}.
Il s'agit d'une méthodologie consistant à suivre douze principes, qui permettent de:
\begin{enumerate}
\item
\textbf{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 qui héberge l'application ou les services, diminuer la divergence entre les différents environnements d'exécution et offrir la possibilité d'intégrer le projet dans un processus d'intégration continue \footnote{\url{https://en.wikipedia.org/wiki/Continuous_integration}} ou déploiement continu \footnote{\url{https://en.wikipedia.org/wiki/Continuous_deployment}}.
\item
\textbf{Faciliter l'intégration de nouveaux développeurs dans l'équipe ou de personnes souhaitant rejoindre le projet}, dans la mesure où la construction d'un nouvel environnement sera grandement facilitée.
\item
\textbf{Minimiser les divergences entre les différents environnemens} sur lesquels un projet pourrait être déployé, pour éviter de découvrir un bogue sur l'environnement de production qui serait impossible à reproduire ailleurs, simplement parce qu'un des composants varierait
\item
\textbf{Augmenter l'agilité générale du projet}, en permettant une meilleure évolutivité architecturale et une meilleure mise à l'échelle.
\end{enumerate}
En pratique, les points ci-dessus permettront de gagner un temps précieux à la construction et à la maintenance de n'importe quel environnement - qu'il soit sur la machine du petit nouveau dans l'équipe, sur un serveur Azure/Heroku/Digital Ocean ou sur votre nouveau Raspberry Pi Zéro planqué à la cave.
Pour reprendre plus spécifiquement les différentes idées derrière cette méthode, nous trouvons des conseils concernant:
\begin{enumerate}
\item
Une base de code unique, suivie par un contrôle de versions
\item
Une déclaration explicite et une isolation des dépendances
\item
Configuration applicative
\item
Ressources externes
\item
La séparation des phases de constructions
\item
La mémoire des processus d'exécution
\item
La liaison des ports
\item
Une connaissance et une confiance dans les processus systèmes
\item
La possibilité d'arrêter élégamment l'application, tout en réduisant au minimum son temps de démarrage
\item
La similarité des environnements
\item
La gestion des journaux et des flux d'évènements
\item
L'isolation des tâches administratives
\end{enumerate}
\section{Une base de code unique, suivie par un contrôle de versions}
Chaque déploiement de l'application, et quel que soit son environnement cible, se basera sur une source unique, afin de minimiser les différences que l'on pourrait trouver entre deux déploiements d'un même projet.
\begin{figure}[H]
\centering
\scalebox{1.0}{\includegraphics[max size={\textwidth}{\textheight}]{images/diagrams/12factors-codebase-deploys.png}}
\end{figure}
Git est reconnu dans l'industrie comme standard des systèmes de contrôles de versions, malgré une courbe d'apprentissage assez ardue.
Comme dépôt, nous pourrons par exemple utiliser GitHub, Gitea ou Gitlab, suivant que vous ayez besoin d'une plateforme centralisée, propriétaire, payante ou auto-hébergée. \index{Git} \index{Github} \index{Gitlab} \index{Gitea}
En résumé:
\begin{quote}
\textit{Il y a seulement une base de code par application, mais il y aura plusieurs déploiements de lapplication. Un déploiement est une instance en fonctionnement de lapplication. Cest, par exemple, le site en production, ou bien un ou plusieurs sites de validation. En plus de cela, chaque développeur a une copie de lapplication qui fonctionne dans son environnement local de développement, ce qui compte également comme un déploiement}. \footnote{\url{https://12factor.net/fr/codebase}}
\end{quote}
Comme l'explique Eran Messeri, ingénieur dans le groupe Google Developer Infrastructure: "\textit{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}. \cite[pp. 288-298]{devops_handbook}.
Ce dépôt n'est pas uniquement destiné à hébergé le code source, mais également à d'autres artefacts et autres formes de connaissance, comme les standards de configuration (Chef recipes, Puppet manifests, \ldots), outils de déploiement, standards de tests (y compris ce qui touche à la sécurité), outils d'analyse et de monitoring ou tutoriaux.
\section{Déclaration explicite et isolation des dépendances}
Chaque installation ou configuration doit être toujours réalisée de la même manière, et doit pouvoir être répétée quel que soit l'environnement cible.
Ceci permet d'éviter que l'application n'utilise une dépendance qui ne soit installée que sur l'un des sytèmes de développement, et qu'elle soit difficile, voire impossible, à réinstaller sur un autre environnement.
Chaque dépendance devra être déclarée dans un fichier présent au niveau de la base de code.
Lors de la création d'un nouvel environnement vierge, il suffira d'utiliser ce fichier comme paramètre afin d'installer les prérequis au bon fonctionnement de notre application, afin d'assurer une reproductibilité quasi parfaite de l'environnement d'exécution.
Il est important de bien "épingler" les versions liées aux dépendances de l'application.
Ceci peut éviter des effets de bord comme une nouvelle version d'une librairie dans laquelle un bug aurait pu avoir été introduit.\footnote{Le paquet PyLint dépend par exemple d'Astroid; \href{https://github.com/PyCQA/pylint-django/issues/343}{en janvier 2022}, ce dernier a été mis à jour sans respecter le principe de versions sémantiques et introduisant une régression. PyLint spécifiait que sa dépendance avec Astroid devait respecter une version ~2.9. Lors de sa mise à jour en 2.9.1, Astroid a introduit un changement majeur, qui faisait planter Pylint. L'épinglage explicite aurait pu éviter ceci.}.
Dans le cas de Python, la déclaration explicite et l'épinglage pourront notamment être réalisés au travers de \href{https://pypi.org/project/pip/}{PIP} ou \href{https://python-poetry.org/}{Poetry}.
La majorité des langages modernes proposent des mécanismes similaires (\href{https://rubygems.org/}{Gem} pour Ruby, \href{https://www.npmjs.com/}{NPM} pour NodeJS, \ldots)
\section{Configuration applicative}
Il faut éviter d'avoir à recompiler/redéployer l'application simplement parce
que:
\begin{enumerate}
\item l'adresse du serveur de messagerie a été modifiée,
\item un protocole a changé en cours de route
\item la base de données a été déplacée
\item \ldots
\end{enumerate}
En pratique, toute information susceptible d'évoluer ou de changer (un seuil, une ressource externe, un couple utilisateur/mot de passe, \ldots) doit se trouver dans un fichier ou dans une variable d'environnement, et doit être facilement modifiable.
Ceci permet de paramétrer facilement un environnement (par exemple, un container), simplement 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, \ldots) 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. \footnote{Ainsi, nous pourrions faire une \href{https://github.com/search?q=filenamefilename:.env DB_USERNAME&type=Code}{recherche sur Github} pour retrouver certaines variables d'environnement qui auraient été laissées en dur dans le code source de certains projets. Le \href{https://github.com/techgaun/github-dorks}{dépôt suivant} liste quelques idées de variables à rechercher\ldots}.
Au moment de développer une nouvelle fonctionnalité, réfléchissez si
l'un des paramètres utilisés risquerait de subir une modification ou s'il concerne un principe de sécurité: ce composant peut concerner une nouvelle chaîne de connexion, un point de terminaison nécessaire à télécharger des données officielles ou un chemin vers un répertoire partagé pour y déposer un fichier.
Le risque est de se retrouver avec une liste colossale de paramètres; pensez à leur assigner une variable par défaut.
Par exemple, Gitea expose la liste suivante de paramètres \footnote{\url{https://docs.gitea.io/en-us/config-cheat-sheet/}}; il serait impossible de tous les configurer un à un avant de pouvoir démarrer une instance.
\section{Ressources externes}
Nous parlons de bases de données, de services de mise en cache, d'API externes, \ldots 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 \textbf{ressources attachées}, dont la présence est nécessaire au bon fonctionnement de l'application, mais pour lesquelles le \textbf{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, \ldots directement \emph{via} des variables d'environnement.
\begin{figure}[H]
\centering
\scalebox{1.0}{\includegraphics[max size={\textwidth}{\textheight}]{images/12factors/attached-resources.png}}
\end{figure}
Nous serons ravis de pouvoir simplement modifier une chaîne \texttt{sqlite:////tmp/my-tmp-sqlite.db} en \texttt{psql://user:pass@127.0.0.1:8458/db} lorsque ce sera nécessaire, sans avoir à recompiler ou redéployer les modifications.
Ces ressources sont donc spécifiés grâce à des variables d'environnement, et chacune d'entre elles dispose également d'un \textbf{type}, afin de profiter d'une correspondance dynamique entre un moteur d'exécution et une information de configuration.
\section{Séparation des phases de construction}
\begin{enumerate}
\item
La \textbf{construction} (\emph{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.
\item
La \textbf{mise à disposition} (\emph{release}) associe cet ensemble à
une configuration prête à être exécutée,
\item
tandis que la phase d'\textbf{exécution} (\emph{run}) démarre les
processus nécessaires au bon fonctionnement de l'application.
\end{enumerate}
\begin{figure}[H]
\centering
\scalebox{1.0}{\includegraphics[max size={\textwidth}{\textheight}]{images/12factors/release.png}}
\end{figure}
Parmi les solutions possibles, nous pourrions nous pourrions nous baser
sur les \emph{releases} de Gitea, sur un serveur d'artefacts (\href{https://fr.wikipedia.org/wiki/Capistrano_(logiciel)}{Capistrano}), voire directement au niveau de forge logicielle (Gitea, Github, Gitlab, \ldots).
\section{Mémoire des processus d'exécution}
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é.
Dit autrement, l'exécution de l'application ne doit pas dépendre de la présence d'une information stockée en mémoire ou sur disque.
Pratiquement, si l'application devait rencontrer un problème, il est nécessaire qu'elle puisse redémarrer rapidement, éventuellement en étant déployée sur un autre serveur - par exemple suite à un problème matériel.
Toute information stockée physiquement sur le premier hôte durant son exécution sur le premier hôte, puisqu'elle pourra avoir été entretemps perdue.
Lors d'une initialisation ou réinitialisation, la solution consiste donc à jouer sur les variables d'environnement (cf. \#3) et sur les informations que l'on pourra trouver au niveau des ressources attachées (cf \#4), et faire en sorte que les informations et données primordiales puissent être récupérées ou reconstruites.
Il serait également difficile d'appliquer une mise à l'échelle de l'application, en ajoutant un nouveau serveur d'exécution, si une donnée indispensable à son fonctionnement devait se trouver sur la seule machine où elle est actuellement exécutée.
\section{Liaison des ports}
Les applications 12-factors sont auto-contenues et peuvent fonctionner
en autonomie totale. Elles doivent être joignables grâce à un mécanisme
de ponts, où l'hôte qui s'occupe de l'exécution effectue lui-même la
redirection vers l'un des ports ouverts par l'application, typiquement,
en HTTP ou via un autre protocole.
\begin{figure}[H]
\centering
\scalebox{1.0}{\includegraphics[max size={\textwidth}{\textheight}]{images/diagrams/12-factors-7.png}}
\end{figure}
L'application fonctionne de manière autonome et expose un port (ici, le 8000).
Le serveur (= l'hôte) choisit d'appliquer une correspondance entre "son" port 443 et le port offert par l'application (8000).
\section{Connaissance et confiance des processus systèmes}
Comme décrit plus haut (cf. \#6), l'application doit utiliser des processus \emph{stateless} (sans état). Nous pouvons créer et utiliser des processus supplémentaires pour tenir plus facilement une lourde charge, ou dédier des particuliers pour certaines tâches: requêtes HTTP \emph{via} des processus Web; \emph{long-running} jobs pour des processus asynchrones, \ldots
Si cela existe sur l'hôte hébergeant l'application, ne vous fatiguez pas: utilisez le.
\begin{figure}[H]
\centering
\scalebox{1.0}{\includegraphics[max size={\textwidth}{\textheight}]{images/12factors/process-types.png}}
\end{figure}
\section{Arrêts élégants et démarrages rapides}
Par "arrêt élégant", nous voulons surtout éviter le fameux
\texttt{kill\ -9\ \textless{}pid\textgreater{}} (ou équivalent), ou tout autre arrêt brutal d'un processus qui nécessiterait une intervention urgente du
superviseur.
En prévoyant une manière élégante d'envoyer un signal de terminaison,
\begin{enumerate}
\item Les requêtes en cours peuvent se terminer au mieux,
\item Le démarrage rapide de nouveaux processus améliorera la balance d'un processus en cours d'extinction vers des processus tout frais, en autorisant l'exécution parallèle d'anciens et de nouveaux "types" de processus
\end{enumerate}
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.).
\begin{figure}[H]
\centering
\scalebox{1.0}{\includegraphics[max size={\textwidth}{\textheight}]{images/12factors/process-type-chronology.png}}
\end{figure}
\section{Similarité des environnements}
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.
Faire en sorte que tous les environnements soient les plus similaires possibles limite les divergences entre environnements, facilite les déploiements et limite la casse et la découverte de modules non compatibles, au plus proche de la phase de développement, selon le principe de la corde d'Andon \cite[p. 140]{devops_handbook} \index{Andon} \footnote{Pour donner un exemple tout bête, SQLite utilise un
mécanisme de stockage dynamique \footnote{\url{https://www.sqlite.org/datatype3.html}}, associée à la valeur plutôt qu'au schéma, \emph{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 \texttt{URLField} proposé par Django a une longeur maximale par défaut de 200 caractères \footnote{\url{https://docs.djangoproject.com/en/3.1/ref/forms/fields/\#django.forms.URLField}}.
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 \emph{staging}, si vous faites les choses un peu mieux) parce que les données seront tronquées, et que cela ne plaira pas à la base de données.
Conserver des environements similaires limite ce genre de désagréments.}
Ceci permet également de proposer à nos utilisateurs un bac à sable dans lequel ils pourront explorer et réaliser des expérimentations en toute sécurité, sans quel cela n'ait d'impact sur un \textit{réel} environnement de production, où les conséquences pourraient être beaucoup plus graves. \cite[p. 9]{data_design}
\section{Journaux de flux évènementiels}
Une application ne doit jamais se soucier de l'endroit où les évènements qui la concerne seront écrits, mais se doit simplement de les envoyer sur la sortie \texttt{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 \href{https://www.graylog.org/}{Greylog} ou \href{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.
Cette phase est critique, dans la mesure où les journaux d'exécution sont la seule manière pour une application de communiquer son état vers l'extérieur: recevoir une erreur interne de serveur est une chose; pouvoir obtenir un minimum d'informations, voire un contexte de plantage complet en est une autre.
La différence entre ces deux points vous fera, au mieux, gagner plusieurs heures sur l'identification ou la résolution d'un problème.
\section{Isolation des tâches administratives}
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 \emph{Read}, \emph{Eval}, \emph{Print} et \emph{Loop}) \index{REPL} à disposition (au hasard: \href{https://pythonprogramminglanguage.com/repl/}{Python} ou Kotlin \footnote{\url{https://kotlinlang.org/}}), ce qui facilite les étapes de maintenance.
\section{Conclusions}
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. \cite[pp. 293-294]{devops_handbook}.
Au fur et à mesure que le code est délibérément construit pour être maintenable, l'équipe gagne en rapidité, en qualité et en fiabilité de déploiement, ce qui facilite les tâches opérationnelles:
\begin{enumerate}
\item
Activation d'une télémétrie suffisante dans les applications et les environnements
\item
Conservation précise des dépendances nécessaires
\item
Résilience des services et plantage élégant (i.e. \textbf{sans finir un SEGFAULT avec l'OS dans les choux et un écran bleu})
\item
Compatibilité entre les différentes versions (n+1, \ldots)
\item
Gestion de l'espace de stockage associé à un environnement (pour éviter d'avoir un environnement de production qui fait 157 Tera-octets)
\item
Activation de la recherche dans les logs
\item
Traces des requêtes provenant des utilisateurs, indépendamment des services utilisés
\item
Centralisation de la configuration (\textbf{via} ZooKeeper, par exemple)
\end{enumerate}