Réécriture du fichier maintenability

This commit is contained in:
Gregory Trullemans 2023-04-21 11:17:58 +02:00
parent 6e23a9d7e4
commit 1461ee2fa1
1 changed files with 51 additions and 104 deletions

View File

@ -6,9 +6,7 @@
--- Robert C. Martin
\end{quote}
Pour la méthode de travail et de développement, nous allons nous baser
sur les \href{https://12factor.net/fr/}{The Twelve-factor App} - ou plus
simplement les \textbf{12 facteurs}.
Pour la méthode de travail et de développement, nous allons nous baser sur les \href{https://12factor.net/fr/}{The Twelve-factor App} - ou plus simplement les \textbf{12 facteurs}.
Suivre ces concepts permet de :
\begin{enumerate}
\item
@ -46,18 +44,13 @@ Ce cépôt n'est pas uniquement destiné à hébergé le code source, mais égal
\section{Déclaration explicite et isolation des dépendances}
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.
Ceci permet d'éviter que l'application n'utilise une dépendance qui ne 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 le cas de Python, cela pourra être fait au travers de
\href{https://pypi.org/project/pip/}{PIP - Package Installer for Python}
ou \href{https://python-poetry.org/}{Poetry}.
La majorité des langages moderners proposent des mécanismes similaires (\href{https://rubygems.org/}{Gem} pour Ruby, \href{https://www.npmjs.com/}{NPM} pour NodeJS, ...)
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.
Ceci permet d'éviter que l'application n'utilise une dépendance qui ne 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 le cas de Python, cela pourra être fait au travers de \href{https://pypi.org/project/pip/}{PIP - Package Installer for Python} ou \href{https://python-poetry.org/}{Poetry}.
La majorité des langages moderners proposent des mécanismes similaires (\href{https://rubygems.org/}{Gem} pour Ruby, \href{https://www.npmjs.com/}{NPM} pour NodeJS, \ldots)
Dans tous les cas, chaque application doit disposer d'un environnement sain, qui lui est assigné. Vu le peu de ressources que cela coûte, il ne faut pas s'en priver.
Dans tous les cas, chaque application doit disposer d'un environnement sain, qui lui est assigné.
Vu le peu de ressources que cela coûte, il ne faut pas s'en priver.
Chaque dépendance devra déclarer et épingler dans un fichier la version nécessaire.
Lors de la création d'un nouvel environnement vierge, il suffira d'utiliser ce
@ -65,17 +58,17 @@ fichier comme paramètre afin d'installer les prérequis au bon
fonctionnement de notre application.
Ceci autorise une reproductibilité quasi parfaite de l'environnement.
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. Parce qu'il arrive que ce genre de problème apparaisse, et
lorsque ce sera le cas, ce sera systématiquement au mauvais moment \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.}
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.
Parce qu'il arrive que ce genre de problème apparaisse, et lorsque ce sera le cas, ce sera systématiquement au mauvais moment \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.}
\section{Configuration applicative}
Il faut éviter d'avoir à recompiler/redéployer l'application simplement parce
que:
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
@ -83,67 +76,47 @@ que:
\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, ...) doit se trouver dans un fichier ou dans une variable d'environnement, et doit être facilement modifiable.
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, ...) 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...}.
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.
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 \href{https://docs.gitea.io/en-us/config-cheat-sheet/}{la liste suivante de paramètres}; 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, ... 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 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.
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 \emph{via} des
variables d'environnement.
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.
\includegraphics{images/12factors/attached-resources.png}
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.
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.
\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}
\includegraphics{images/12factors/release.png}
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, ...).
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}
@ -158,11 +131,8 @@ Il serait également difficile d'appliquer une mise à l'échelle de l'applicati
\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.
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.
\includegraphics{images/diagrams/12-factors-7.png}
@ -171,16 +141,15 @@ Le serveur (= l'hôte) choisit d'appliquer une correspondance entre "son" port 4
\section{Connaissance et confiance des processys 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, ...
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.
\includegraphics{images/12factors/process-types.png}
\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.
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}
@ -188,25 +157,19 @@ En prévoyant une manière élégante d'envoyer un signal de terminaison,
\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.).
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.).
\includegraphics{images/12factors/process-type-chronology.png}
\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
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
\href{https://www.sqlite.org/datatype3.html}{mécanisme de stockage dynamique}, 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.
\href{https://www.sqlite.org/datatype3.html}{mécanisme de stockage dynamique}, 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 \href{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 \emph{staging}, si vous faites les choses peu mieux) parce que les données seront tronquées, et que cela ne plaira pas à la base de données.
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 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.}
\section{Journaux de flux évènementiels}
@ -219,38 +182,22 @@ La différence entre ces deux points vous fera, au mieux, gagner plusieurs heure
\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.
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 \href{https://kotlinlang.org/}{Kotlin}), 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.
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
sur un SEGFAULT avec l'OS dans les choux et un écran bleu})
\item
Compatibilité entre les différentes versions (n+1, \ldots\hspace{0pt})
\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)
\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 sur un SEGFAULT avec l'OS dans les choux et un écran bleu})
\item Compatibilité entre les différentes versions (n+1, \ldots\hspace{0pt})
\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}