Integrate Debian, forms, ...
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Fred Pauchet 2022-04-26 22:26:00 +02:00
parent b154a4b302
commit 373a39a22b
11 changed files with 694 additions and 875 deletions

View File

@ -327,485 +327,18 @@ peut être splitté en plusieurs parties:
\NormalTok{MY\_APPS }\OperatorTok{=}\NormalTok{ [}
\NormalTok{]}
\end{Highlighting}
\end{Shaded}
\hypertarget{_context_processors}{%
\section{\texorpdfstring{\emph{Context
Processors}}{Context Processors}}\label{_context_processors}}
Mise en pratique: un \emph{context processor} sert \emph{grosso-modo} à
peupler l'ensemble des données transmises des vues aux templates avec
des données communes. Un context processor est un peu l'équivalent d'un
middleware, mais entre les données et les templates, là où le middleware
va s'occuper des données relatives aux réponses et requêtes elles-mêmes.
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# core/context\_processors.py}
\ImportTok{import}\NormalTok{ subprocess}
\KeywordTok{def}\NormalTok{ git\_describe(request) }\OperatorTok{{-}\textgreater{}} \BuiltInTok{str}\NormalTok{:}
\ControlFlowTok{return}\NormalTok{ \{}
\StringTok{"git\_describe"}\NormalTok{: subprocess.check\_output(}
\NormalTok{ [}\StringTok{"git"}\NormalTok{, }\StringTok{"describe"}\NormalTok{, }\StringTok{"{-}{-}always"}\NormalTok{]}
\NormalTok{ ).strip(),}
\StringTok{"git\_date"}\NormalTok{: subprocess.check\_output(}
\NormalTok{ [}\StringTok{"git"}\NormalTok{, }\StringTok{"show"}\NormalTok{, }\StringTok{"{-}s"}\NormalTok{, }\VerbatimStringTok{r"{-}{-}format=}\SpecialCharTok{\%c}\VerbatimStringTok{d"}\NormalTok{, }\VerbatimStringTok{r"{-}{-}date=format:}\SpecialCharTok{\%d}\VerbatimStringTok{{-}\%m{-}\%Y"}\NormalTok{]}
\NormalTok{ ),}
\NormalTok{ \}}
\end{Highlighting}
\end{Shaded}
Ceci aura pour effet d'ajouter les deux variables \texttt{git\_describe}
et \texttt{git\_date} dans tous les contextes de tous les templates de
l'application.
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{TEMPLATES }\OperatorTok{=}\NormalTok{ [}
\NormalTok{ \{}
\StringTok{\textquotesingle{}BACKEND\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}django.template.backends.django.DjangoTemplates\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}DIRS\textquotesingle{}}\NormalTok{: [os.path.join(BASE\_DIR, }\StringTok{"templates"}\NormalTok{),],}
\StringTok{\textquotesingle{}APP\_DIRS\textquotesingle{}}\NormalTok{: }\VariableTok{True}\NormalTok{,}
\StringTok{\textquotesingle{}OPTIONS\textquotesingle{}}\NormalTok{: \{}
\StringTok{\textquotesingle{}context\_processors\textquotesingle{}}\NormalTok{: [}
\StringTok{\textquotesingle{}django.template.context\_processors.debug\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}django.template.context\_processors.request\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}django.contrib.auth.context\_processors.auth\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}django.contrib.messages.context\_processors.messages\textquotesingle{}}\NormalTok{,}
\StringTok{"core.context\_processors.git\_describe"}
\NormalTok{ ],}
\NormalTok{ \},}
\NormalTok{ \},}
\NormalTok{]}
\end{Highlighting}
\end{Shaded}
\begin{quote}
To be effective, a software system must be deployable. The higher the
cost of deployements, the less useful the system is. A goal of a
software architecture, then, should be to make a system that can be
easily deployed with a single action. Unfortunately, deployment strategy
is seldom considered during initial development. This leads to
architectures that may be make the system easy to develop, but leave it
very difficult to deploy.
--- Robert C. Martin Clean Architecture
\end{quote}
Il y a une raison très simple à aborder le déploiement dès maintenant: à
trop attendre et à peaufiner son développement en local, on en oublie
que sa finalité sera de se retrouver exposé et accessible depuis un
serveur. Il est du coup probable d'oublier une partie des désidérata, de
zapper une fonctionnalité essentielle ou simplement de passer énormément
de temps à adapter les sources pour qu'elles puissent être mises à
disposition sur un environnement en particulier, une fois que leur
développement aura été finalisé, testé et validé. Un bon déploiement ne
doit pas dépendre de dizaines de petits scripts éparpillés sur le
disque. L'objectif est qu'il soit rapide et fiable. Ceci peut être
atteint au travers d'un partitionnement correct, incluant le fait que le
composant principal s'assure que chaque sous-composant est correctement
démarré intégré et supervisé.
Aborder le déploiement dès le début permet également de rédiger dès le
début les procédures d'installation, de mises à jour et de sauvegardes.
A la fin de chaque intervalle de développement, les fonctionnalités
auront dû avoir été intégrées, testées, fonctionnelles et un code
propre, démontrable dans un environnement similaire à un environnement
de production, et créées à partir d'un tronc commun au développement
cite:{[}devops\_handbook{]}.
Déploier une nouvelle version sera aussi simple que de récupérer la
dernière archive depuis le dépôt, la placer dans le bon répertoire,
appliquer des actions spécifiques (et souvent identiques entre deux
versions), puis redémarrer les services adéquats, et la procédure
complète se résumera à quelques lignes d'un script bash.
\begin{quote}
Because value is created only when our services are running into
production, we must ensure that we are not only delivering fast flow,
but that our deployments can also be performed without causing chaos and
disruptions such as service outages, service impairments, or security or
compliance failures.
--- DevOps Handbook Introduction
\end{quote}
Le serveur que django met à notre disposition \emph{via} la commande
\texttt{runserver} est extrêmement pratique, mais il est uniquement
prévu pour la phase développement: en production, il est inutile de
passer par du code Python pour charger des fichiers statiques (feuilles
de style, fichiers JavaScript, images, \ldots\hspace{0pt}). De même,
Django propose par défaut une base de données SQLite, qui fonctionne
parfaitement dès lors que l'on connait ses limites et que l'on se limite
à un utilisateur à la fois. En production, il est légitime que la base
de donnée soit capable de supporter plusieurs utilisateurs et connexions
simultanés. En restant avec les paramètres par défaut, il est plus que
probable que vous rencontriez rapidement des erreurs de verrou parce
qu'un autre processus a déjà pris la main pour écrire ses données. En
bref, vous avez quelque chose qui fonctionne, qui répond à un besoin,
mais qui va attirer la grogne de ses utilisateurs pour des problèmes de
latences, pour des erreurs de verrou ou simplement parce que le serveur
répondra trop lentement.
L'objectif de cette partie est de parcourir les différentes possibilités
qui s'offrent à nous en termes de déploiement, tout en faisant en sorte
que le code soit le moins couplé possible à sa destination de
production. L'objectif est donc de faire en sorte qu'une même
application puisse être hébergées par plusieurs hôtes sans avoir à subir
de modifications. Nous vous renvoyons vers les 12-facteurs dont nous
avons déjà parlé et qui vous énormément nous aider, puisque ce sont des
variables d'environnement qui vont réellement piloter le câblage entre
l'application, ses composants et son hébergeur.
RedHat proposait récemment un article intitulé \emph{*What Is IaaS*},
qui présentait les principales différences entre types d'hébergement.
\begin{figure}
\centering
\includegraphics{images/deployment/iaas_focus-paas-saas-diagram.png}
\caption{L'infrastructure en tant que service, cc. \emph{RedHat Cloud
Computing}}
\end{figure}
Ainsi, on trouve:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Le déploiment \emph{on-premises} ou \emph{on-site}
\item
Les \emph{Infrastructures as a service} ou \emph{IaaSIaaS}
\item
Les \emph{Platforms as a service} ou \emph{PaaSPaaS}
\item
Les \emph{Softwares as a service} ou \emph{SaaSSaaS}, ce dernier point
nous concernant moins, puisque c'est nous qui développons le logiciel.
\end{enumerate}
Dans cette partie, nous aborderons les points suivants:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Définir l'infrastructure et les composants nécessaires à notre
application
\item
Configurer l'hôte qui hébergera l'application et y déployer notre
application: dans une machine physique, virtuelle ou dans un
container. Nous aborderons aussi les déploiements via Ansible et Salt.
A ce stade, nous aurons déjà une application disponible.
\item
Configurer les outils nécessaires à la bonne exécution de ce code et
de ses fonctionnalités: les différentes méthodes de supervision de
l'application, comment analyser les fichiers de logs, comment
intercepter correctement une erreur si elle se présente et comment
remonter correctement l'information.
\end{enumerate}
\hypertarget{_infrastructure_composants}{%
\section{Infrastructure \&
composants}\label{_infrastructure_composants}}
Pour une mise ne production, le standard \emph{de facto} est le suivant:
\begin{itemize}
\item
Nginx comme reverse proxy
\item
HAProxy pour la distribution de charge
\item
Gunicorn ou Uvicorn comme serveur d'application
\item
Supervisor pour le monitoring
\item
PostgreSQL ou MySQL/MariaDB comme bases de données.
\item
Celery et RabbitMQ pour l'exécution de tâches asynchrones
\item
Redis / Memcache pour la mise à en cache (et pour les sessions ? A
vérifier).
\item
Sentry, pour le suivi des bugs
\end{itemize}
Si nous schématisons l'infrastructure et le chemin parcouru par une
requête, nous pourrions arriver à la synthèse suivante:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
L'utilisateur fait une requête via son navigateur (Firefox ou Chrome)
\item
Le navigateur envoie une requête http, sa version, un verbe (GET,
POST, \ldots\hspace{0pt}), un port et éventuellement du contenu
\item
Le firewall du serveur (Debian GNU/Linux, CentOS, \ldots\hspace{0pt})
vérifie si la requête peut être prise en compte
\item
La requête est transmise à l'application qui écoute sur le port
(probablement 80 ou 443; et \emph{a priori} Nginx)
\item
Elle est ensuite transmise par socket et est prise en compte par un
des \emph{workers} (= un processus Python) instancié par Gunicorn. Si
l'un de ces travailleurs venait à planter, il serait automatiquement
réinstancié par Supervisord.
\item
Qui la transmet ensuite à l'un de ses \emph{workers} (= un processus
Python).
\item
Après exécution, une réponse est renvoyée à l'utilisateur.
\end{enumerate}
\includegraphics{images/diagrams/architecture.png}
\hypertarget{_reverse_proxy}{%
\subsection{Reverse proxy}\label{_reverse_proxy}}
Le principe du \textbf{proxy inverse} est de pouvoir rediriger du trafic
entrant vers une application hébergée sur le système. Il serait tout à
fait possible de rendre notre application directement accessible depuis
l'extérieur, mais le proxy a aussi l'intérêt de pouvoir élever la
sécurité du serveur (SSL) et décharger le serveur applicatif grâce à un
mécanisme de cache ou en compressant certains résultats \footnote{\url{https://fr.wikipedia.org/wiki/Proxy_inverse}}
\hypertarget{_load_balancer}{%
\subsection{Load balancer}\label{_load_balancer}}
\hypertarget{_workers}{%
\subsection{Workers}\label{_workers}}
\hypertarget{_supervision_des_processus}{%
\subsection{Supervision des
processus}\label{_supervision_des_processus}}
\hypertarget{_base_de_donnuxe9es_2}{%
\subsection{Base de données}\label{_base_de_donnuxe9es_2}}
\hypertarget{_tuxe2ches_asynchrones}{%
\subsection{Tâches asynchrones}\label{_tuxe2ches_asynchrones}}
\hypertarget{_mise_en_cache}{%
\subsection{Mise en cache}\label{_mise_en_cache}}
\hypertarget{_code_source}{%
\section{Code source}\label{_code_source}}
Au niveau logiciel (la partie mise en subrillance ci-dessus), la requête
arrive dans les mains du processus Python, qui doit encore
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
effectuer le routage des données,
\item
trouver la bonne fonction à exécuter,
\item
récupérer les données depuis la base de données,
\item
effectuer le rendu ou la conversion des données,
\item
et renvoyer une réponse à l'utilisateur.
\end{enumerate}
Comme nous l'avons vu dans la première partie, Django est un framework
complet, intégrant tous les mécanismes nécessaires à la bonne évolution
d'une application. Il est possible de démarrer petit, et de suivre
l'évolution des besoins en fonction de la charge estimée ou ressentie,
d'ajouter un mécanisme de mise en cache, des logiciels de suivi,
\ldots\hspace{0pt}
\hypertarget{_outils_de_supervision_et_de_mise_uxe0_disposition}{%
\section{Outils de supervision et de mise à
disposition}\label{_outils_de_supervision_et_de_mise_uxe0_disposition}}
\hypertarget{_logs}{%
\subsection{Logs}\label{_logs}}
\hypertarget{_logging}{%
\section{Logging}\label{_logging}}
La structure des niveaux de journaux est essentielle.
\begin{quote}
When deciding whether a message should be ERROR or WARN, imagine being
woken up at 4 a.m. Low printer toner is not an ERROR.
--- Dan North former ToughtWorks consultant
\end{quote}
\begin{itemize}
\item
\textbf{DEBUG}: Il s'agit des informations qui concernent tout ce qui
peut se passer durant l'exécution de l'application. Généralement, ce
niveau est désactivé pour une application qui passe en production,
sauf s'il est nécessaire d'isoler un comportement en particulier,
auquel cas il suffit de le réactiver temporairement.
\item
\textbf{INFO}: Enregistre les actions pilotées par un utilisateur -
Démarrage de la transaction de paiement, \ldots\hspace{0pt}
\item
\textbf{WARN}: Regroupe les informations qui pourraient
potentiellement devenir des erreurs.
\item
\textbf{ERROR}: Indique les informations internes - Erreur lors de
l'appel d'une API, erreur interne, \ldots\hspace{0pt}
\item
\textbf{FATAL} (ou \textbf{EXCEPTION}): \ldots\hspace{0pt}
généralement suivie d'une terminaison du programme ;-) - Bind raté
d'un socket, etc.
\end{itemize}
La configuration des \emph{loggers} est relativement simple, un peu plus
complexe si nous nous penchons dessus, et franchement complète si nous
creusons encore. Il est ainsi possible de définir des formattages,
gestionnaires (\emph{handlers}) et loggers distincts, en fonction de nos
applications.
Sauf que comme nous l'avons vu avec les 12 facteurs, nous devons traiter
les informations de notre application comme un flux d'évènements. Il
n'est donc pas réellement nécessaire de chipoter la configuration,
puisque la seule classe qui va réellement nous intéresser concerne les
\texttt{StreamHandler}. La configuration que nous allons utiliser est
celle-ci:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Formattage: à définir - mais la variante suivante est complète,
lisible et pratique:
\texttt{\{levelname\}\ \{asctime\}\ \{module\}\ \{process:d\}\ \{thread:d\}\ \{message\}}
\item
Handler: juste un, qui définit un \texttt{StreamHandler}
\item
Logger: pour celui-ci, nous avons besoin d'un niveau (\texttt{level})
et de savoir s'il faut propager les informations vers les
sous-paquets, auquel cas il nous suffira de fixer la valeur de
\texttt{propagate} à \texttt{True}.
\end{enumerate}
\begin{Shaded}
\begin{Highlighting}[]
\NormalTok{LOGGING }\OperatorTok{=}\NormalTok{ \{}
\StringTok{\textquotesingle{}version\textquotesingle{}}\NormalTok{: }\DecValTok{1}\NormalTok{,}
\StringTok{\textquotesingle{}disable\_existing\_loggers\textquotesingle{}}\NormalTok{: }\VariableTok{False}\NormalTok{,}
\StringTok{\textquotesingle{}formatters\textquotesingle{}}\NormalTok{: \{}
\StringTok{\textquotesingle{}verbose\textquotesingle{}}\NormalTok{: \{}
\StringTok{\textquotesingle{}format\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}}\SpecialCharTok{\{levelname\}}\StringTok{ }\SpecialCharTok{\{asctime\}}\StringTok{ }\SpecialCharTok{\{module\}}\StringTok{ }\SpecialCharTok{\{process:d\}}\StringTok{ }\SpecialCharTok{\{thread:d\}}\StringTok{ }\SpecialCharTok{\{message\}}\StringTok{\textquotesingle{}}\NormalTok{,}
\NormalTok{ \},}
\StringTok{\textquotesingle{}simple\textquotesingle{}}\NormalTok{: \{}
\StringTok{\textquotesingle{}format\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}}\SpecialCharTok{\{levelname\}}\StringTok{ }\SpecialCharTok{\{asctime\}}\StringTok{ }\SpecialCharTok{\{module\}}\StringTok{ }\SpecialCharTok{\{message\}}\StringTok{\textquotesingle{}}\NormalTok{,}
\NormalTok{ \},}
\NormalTok{ \},}
\StringTok{\textquotesingle{}handlers\textquotesingle{}}\NormalTok{: \{}
\StringTok{\textquotesingle{}console\textquotesingle{}}\NormalTok{: \{}
\StringTok{\textquotesingle{}level\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}DEBUG\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}class\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}logging.StreamHandler\textquotesingle{}}\NormalTok{,}
\StringTok{\textquotesingle{}formatter\textquotesingle{}}\NormalTok{: }\StringTok{"verbose"}
\NormalTok{ \}}
\NormalTok{ \},}
\StringTok{\textquotesingle{}loggers\textquotesingle{}}\NormalTok{: \{}
\StringTok{\textquotesingle{}khana\textquotesingle{}}\NormalTok{: \{}
\StringTok{\textquotesingle{}handlers\textquotesingle{}}\NormalTok{: [}\StringTok{\textquotesingle{}console\textquotesingle{}}\NormalTok{],}
\StringTok{\textquotesingle{}level\textquotesingle{}}\NormalTok{: env(}\StringTok{"LOG\_LEVEL"}\NormalTok{, default}\OperatorTok{=}\StringTok{"DEBUG"}\NormalTok{),}
\StringTok{\textquotesingle{}propagate\textquotesingle{}}\NormalTok{: }\VariableTok{True}\NormalTok{,}
\NormalTok{ \},}
\NormalTok{ \}}
\NormalTok{\}}
\end{Highlighting}
\end{Shaded}
Pour utiliser nos loggers, il suffit de copier le petit bout de code
suivant:
\begin{Shaded}
\begin{Highlighting}[]
\ImportTok{import}\NormalTok{ logging}
\NormalTok{logger }\OperatorTok{=}\NormalTok{ logging.getLogger(}\VariableTok{\_\_name\_\_}\NormalTok{)}
\NormalTok{logger.debug(}\StringTok{\textquotesingle{}helloworld\textquotesingle{}}\NormalTok{)}
\end{Highlighting}
\end{Shaded}
\href{https://docs.djangoproject.com/en/stable/topics/logging/\#examples}{Par
exemples}.
\hypertarget{_logging_2}{%
\subsection{Logging}\label{_logging_2}}
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Sentry via sentry\_sdk
\item
Nagios
\item
LibreNMS
\item
Zabbix
\end{enumerate}
Il existe également \href{https://munin-monitoring.org}{Munin},
\href{https://www.elastic.co}{Logstash, ElasticSearch et Kibana
(ELK-Stack)} ou \href{https://www.fluentd.org}{Fluentd}.
\hypertarget{_muxe9thode_de_duxe9ploiement}{%
\section{Méthode de déploiement}\label{_muxe9thode_de_duxe9ploiement}}
Nous allons détailler ci-dessous trois méthodes de déploiement:
\begin{itemize}
\item
Sur une machine hôte, en embarquant tous les composants sur un même
serveur. Ce ne sera pas idéal, puisqu'il ne sera pas possible de
configurer un \emph{load balancer}, de routeur plusieurs basées de
données, mais ce sera le premier cas de figure.
\item
Dans des containers, avec Docker-Compose.
\item
Sur une \textbf{Plateforme en tant que Service} (ou plus simplement,
\textbf{PaaSPaaS}), pour faire abstraction de toute la couche de
configuration du serveur.
\end{itemize}
\hypertarget{_duxe9ploiement_sur_debian}{%
\section{Déploiement sur Debian}\label{_duxe9ploiement_sur_debian}}
La première étape pour la configuration de notre hôte consiste à définir
les utilisateurs et groupes de droits. Il est faut absolument éviter de
faire tourner une application en tant qu'utilisateur \textbf{root}, car
la moindre faille pourrait avoir des conséquences catastrophiques.
Une fois que ces utilisateurs seront configurés, nous pourrons passer à
l'étape de configuration, qui consistera à:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Déployer les sources
\item
Démarrer un serveur implémentant une interface WSGI (\textbf{Web
Server Gateway Interface}), qui sera chargé de créer autant de petits
lutins travailleurs que nous le désirerons.
\item
Démarrer un superviseur, qui se chargera de veiller à la bonne santé
de nos petits travailleurs, et en créer de nouveaux s'il le juge
nécessaire
\item
Configurer un proxy inverse, qui s'occupera d'envoyer les requêtes
d'un utilisateur externe à la machine hôte vers notre serveur
applicatif, qui la communiquera à l'un des travailleurs.
\end{enumerate}
La machine hôte peut être louée chez Digital Ocean, Scaleway, OVH,
Vultr, \ldots\hspace{0pt} Il existe des dizaines d'hébergements typés
VPS (\textbf{Virtual Private Server}). A vous de choisir celui qui vous
convient \footnote{Personnellement, j'ai un petit faible pour Hetzner
Cloud}.
\begin{Shaded}
\begin{Highlighting}[]
@ -870,136 +403,7 @@ Ou passer par une installation alternative:
distribution, et cela pourrait avoir des effets de bord non souhaités.
\end{itemize}
\hypertarget{_installation_de_la_base_de_donnuxe9es}{%
\subsection{Installation de la base de
données}\label{_installation_de_la_base_de_donnuxe9es}}
On l'a déjà vu, Django se base sur un pattern type
\href{https://www.martinfowler.com/eaaCatalog/activeRecord.html}{ActiveRecords}
pour la gestion de la persistance des données et supporte les principaux
moteurs de bases de données connus:
\begin{itemize}
\item
SQLite (en natif, mais Django 3.0 exige une version du moteur
supérieure ou égale à la 3.8)
\item
MariaDB (en natif depuis Django 3.0),
\item
PostgreSQL au travers de psycopg2 (en natif aussi),
\item
Microsoft SQLServer grâce aux drivers {[}\ldots\hspace{0pt}à
compléter{]}
\item
Oracle via
\href{https://oracle.github.io/python-cx_Oracle/}{cx\_Oracle}.
\end{itemize}
Chaque pilote doit être utilisé précautionneusement ! Chaque version de
Django n'est pas toujours compatible avec chacune des versions des
pilotes, et chaque moteur de base de données nécessite parfois une
version spécifique du pilote. Par ce fait, vous serez parfois bloqué sur
une version de Django, simplement parce que votre serveur de base de
données se trouvera dans une version spécifique (eg. Django 2.3 à cause
d'un Oracle 12.1).
Ci-dessous, quelques procédures d'installation pour mettre un serveur à
disposition. Les deux plus simples seront MariaDB et PostgreSQL, qu'on
couvrira ci-dessous. Oracle et Microsoft SQLServer se trouveront en
annexes.
\hypertarget{_postgresql}{%
\subsubsection{PostgreSQL}\label{_postgresql}}
On commence par installer PostgreSQL.
Par exemple, dans le cas de debian, on exécute la commande suivante:
\begin{Shaded}
\begin{Highlighting}[]
\VariableTok{$$}\NormalTok{$ }\ExtensionTok{aptitude}\NormalTok{ install postgresql postgresql{-}contrib}
\end{Highlighting}
\end{Shaded}
Ensuite, on crée un utilisateur pour la DB:
\begin{Shaded}
\begin{Highlighting}[]
\VariableTok{$$}\NormalTok{$ }\FunctionTok{su}\NormalTok{ {-} postgres}
\ExtensionTok{postgres@gwift}\NormalTok{:\textasciitilde{}$ createuser {-}{-}interactive {-}P}
\ExtensionTok{Enter}\NormalTok{ name of role to add: gwift\_user}
\ExtensionTok{Enter}\NormalTok{ password for new role:}
\ExtensionTok{Enter}\NormalTok{ it again:}
\ExtensionTok{Shall}\NormalTok{ the new role be a superuser? (y/n) }\ExtensionTok{n}
\ExtensionTok{Shall}\NormalTok{ the new role be allowed to create databases? (y/n) }\ExtensionTok{n}
\ExtensionTok{Shall}\NormalTok{ the new role be allowed to create more new roles? (y/n) }\ExtensionTok{n}
\ExtensionTok{postgres@gwift}\NormalTok{:\textasciitilde{}$}
\end{Highlighting}
\end{Shaded}
Finalement, on peut créer la DB:
\begin{Shaded}
\begin{Highlighting}[]
\ExtensionTok{postgres@gwift}\NormalTok{:\textasciitilde{}$ createdb {-}{-}owner gwift\_user gwift}
\ExtensionTok{postgres@gwift}\NormalTok{:\textasciitilde{}$ exit}
\BuiltInTok{logout}
\VariableTok{$$}\NormalTok{$}
\end{Highlighting}
\end{Shaded}
penser à inclure un bidule pour les backups.
\hypertarget{_mariadb}{%
\subsubsection{MariaDB}\label{_mariadb}}
Idem, installation, configuration, backup, tout ça. A copier de
grimboite, je suis sûr d'avoir des notes là-dessus.
\hypertarget{_microsoft_sql_server}{%
\subsubsection{Microsoft SQL Server}\label{_microsoft_sql_server}}
\hypertarget{_oracle}{%
\subsubsection{Oracle}\label{_oracle}}
\hypertarget{_pruxe9paration_de_lenvironnement_utilisateur}{%
\subsection{Préparation de l'environnement
utilisateur}\label{_pruxe9paration_de_lenvironnement_utilisateur}}
\begin{Shaded}
\begin{Highlighting}[]
\FunctionTok{su}\NormalTok{ {-} gwift}
\FunctionTok{cp}\NormalTok{ /etc/skel/.bashrc .}
\FunctionTok{cp}\NormalTok{ /etc/skel/.bash\_profile .}
\FunctionTok{ssh{-}keygen}
\FunctionTok{mkdir}\NormalTok{ bin}
\FunctionTok{mkdir}\NormalTok{ .venvs}
\FunctionTok{mkdir}\NormalTok{ webapps}
\ExtensionTok{python3.6}\NormalTok{ {-}m venv .venvs/gwift}
\BuiltInTok{source}\NormalTok{ .venvs/gwift/bin/activate}
\BuiltInTok{cd}\NormalTok{ /home/gwift/webapps}
\FunctionTok{git}\NormalTok{ clone ...}
\end{Highlighting}
\end{Shaded}
La clé SSH doit ensuite être renseignée au niveau du dépôt, afin de
pouvoir y accéder.
A ce stade, on devrait déjà avoir quelque chose de fonctionnel en
démarrant les commandes suivantes:
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# en tant qu\textquotesingle{}utilisateur \textquotesingle{}gwift\textquotesingle{}}
\BuiltInTok{source}\NormalTok{ .venvs/gwift/bin/activate}
\ExtensionTok{pip}\NormalTok{ install {-}U pip}
\ExtensionTok{pip}\NormalTok{ install {-}r requirements/base.txt}
\ExtensionTok{pip}\NormalTok{ install gunicorn}
\BuiltInTok{cd}\NormalTok{ webapps/gwift}
\ExtensionTok{gunicorn}\NormalTok{ config.wsgi:application {-}{-}bind localhost:3000 {-}{-}settings=config.settings\_production}
\end{Highlighting}
\end{Shaded}
\hypertarget{_configuration_de_lapplication}{%
\subsection{Configuration de
@ -3268,266 +2672,6 @@ code.
\hypertarget{_quelques_liens_utiles}{%
\subsection{Quelques liens utiles}\label{_quelques_liens_utiles}}
\begin{itemize}
\item
`Django factory boy
\textless{}\url{https://github.com/rbarrois/django-factory_boy/tree/v1.0.0\%3E\%60_}
\end{itemize}
\hypertarget{_refactoring}{%
\section{Refactoring}\label{_refactoring}}
On constate que plusieurs classes possèdent les mêmes propriétés
\texttt{created\_at} et \texttt{updated\_at}, initialisées aux mêmes
valeurs. Pour gagner en cohérence, nous allons créer une classe dans
laquelle nous définirons ces deux champs, et nous ferons en sorte que
les classes \texttt{Wishlist}, \texttt{Item} et \texttt{Part} en
héritent. Django gère trois sortes d'héritage:
\begin{itemize}
\item
L'héritage par classe abstraite
\item
L'héritage classique
\item
L'héritage par classe proxy.
\end{itemize}
\hypertarget{_classe_abstraite}{%
\subsection{Classe abstraite}\label{_classe_abstraite}}
L'héritage par classe abstraite consiste à déterminer une classe mère
qui ne sera jamais instanciée. C'est utile pour définir des champs qui
se répèteront dans plusieurs autres classes et surtout pour respecter le
principe de DRY. Comme la classe mère ne sera jamais instanciée, ces
champs seront en fait dupliqués physiquement, et traduits en SQL, dans
chacune des classes filles.
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# wish/models.py}
\KeywordTok{class}\NormalTok{ AbstractModel(models.Model):}
\KeywordTok{class}\NormalTok{ Meta:}
\NormalTok{ abstract }\OperatorTok{=} \VariableTok{True}
\NormalTok{ created\_at }\OperatorTok{=}\NormalTok{ models.DateTimeField(auto\_now\_add}\OperatorTok{=}\VariableTok{True}\NormalTok{)}
\NormalTok{ updated\_at }\OperatorTok{=}\NormalTok{ models.DateTimeField(auto\_now}\OperatorTok{=}\VariableTok{True}\NormalTok{)}
\KeywordTok{class}\NormalTok{ Wishlist(AbstractModel):}
\ControlFlowTok{pass}
\KeywordTok{class}\NormalTok{ Item(AbstractModel):}
\ControlFlowTok{pass}
\KeywordTok{class}\NormalTok{ Part(AbstractModel):}
\ControlFlowTok{pass}
\end{Highlighting}
\end{Shaded}
En traduisant ceci en SQL, on aura en fait trois tables, chacune
reprenant les champs \texttt{created\_at} et \texttt{updated\_at}, ainsi
que son propre identifiant:
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{{-}{-}$ python manage.py sql wish}
\ControlFlowTok{BEGIN}\NormalTok{;}
\KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_wishlist"}\NormalTok{ (}
\OtherTok{"id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY}\NormalTok{ AUTOINCREMENT,}
\OtherTok{"created\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}\NormalTok{,}
\OtherTok{"updated\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}
\NormalTok{)}
\NormalTok{;}
\KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_item"}\NormalTok{ (}
\OtherTok{"id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY}\NormalTok{ AUTOINCREMENT,}
\OtherTok{"created\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}\NormalTok{,}
\OtherTok{"updated\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}
\NormalTok{)}
\NormalTok{;}
\KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_part"}\NormalTok{ (}
\OtherTok{"id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY}\NormalTok{ AUTOINCREMENT,}
\OtherTok{"created\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}\NormalTok{,}
\OtherTok{"updated\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}
\NormalTok{)}
\NormalTok{;}
\KeywordTok{COMMIT}\NormalTok{;}
\end{Highlighting}
\end{Shaded}
\hypertarget{_huxe9ritage_classique}{%
\subsection{Héritage classique}\label{_huxe9ritage_classique}}
L'héritage classique est généralement déconseillé, car il peut
introduire très rapidement un problème de performances: en reprenant
l'exemple introduit avec l'héritage par classe abstraite, et en omettant
l'attribut \texttt{abstract\ =\ True}, on se retrouvera en fait avec
quatre tables SQL:
\begin{itemize}
\item
Une table \texttt{AbstractModel}, qui reprend les deux champs
\texttt{created\_at} et \texttt{updated\_at}
\item
Une table \texttt{Wishlist}
\item
Une table \texttt{Item}
\item
Une table \texttt{Part}.
\end{itemize}
A nouveau, en analysant la sortie SQL de cette modélisation, on obtient
ceci:
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{{-}{-}$ python manage.py sql wish}
\ControlFlowTok{BEGIN}\NormalTok{;}
\KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_abstractmodel"}\NormalTok{ (}
\OtherTok{"id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY}\NormalTok{ AUTOINCREMENT,}
\OtherTok{"created\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}\NormalTok{,}
\OtherTok{"updated\_at"}\NormalTok{ datetime }\KeywordTok{NOT} \KeywordTok{NULL}
\NormalTok{)}
\NormalTok{;}
\KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_wishlist"}\NormalTok{ (}
\OtherTok{"abstractmodel\_ptr\_id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY} \KeywordTok{REFERENCES} \OtherTok{"wish\_abstractmodel"}\NormalTok{ (}\OtherTok{"id"}\NormalTok{)}
\NormalTok{)}
\NormalTok{;}
\KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_item"}\NormalTok{ (}
\OtherTok{"abstractmodel\_ptr\_id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY} \KeywordTok{REFERENCES} \OtherTok{"wish\_abstractmodel"}\NormalTok{ (}\OtherTok{"id"}\NormalTok{)}
\NormalTok{)}
\NormalTok{;}
\KeywordTok{CREATE} \KeywordTok{TABLE} \OtherTok{"wish\_part"}\NormalTok{ (}
\OtherTok{"abstractmodel\_ptr\_id"} \DataTypeTok{integer} \KeywordTok{NOT} \KeywordTok{NULL} \KeywordTok{PRIMARY} \KeywordTok{KEY} \KeywordTok{REFERENCES} \OtherTok{"wish\_abstractmodel"}\NormalTok{ (}\OtherTok{"id"}\NormalTok{)}
\NormalTok{)}
\NormalTok{;}
\KeywordTok{COMMIT}\NormalTok{;}
\end{Highlighting}
\end{Shaded}
Le problème est que les identifiants seront définis et incrémentés au
niveau de la table mère. Pour obtenir les informations héritées, nous
seront obligés de faire une jointure. En gros, impossible d'obtenir les
données complètes pour l'une des classes de notre travail de base sans
effectuer un \textbf{join} sur la classe mère.
Dans ce sens, cela va encore\ldots\hspace{0pt} Mais imaginez que vous
définissiez une classe \texttt{Wishlist}, de laquelle héritent les
classes \texttt{ChristmasWishlist} et \texttt{EasterWishlist}: pour
obtenir la liste complètes des listes de souhaits, il vous faudra faire
une jointure \textbf{externe} sur chacune des tables possibles, avant
même d'avoir commencé à remplir vos données. Il est parfois nécessaire
de passer par cette modélisation, mais en étant conscient des risques
inhérents.
\hypertarget{_classe_proxy}{%
\subsection{Classe proxy}\label{_classe_proxy}}
Lorsqu'on définit une classe de type \textbf{proxy}, on fait en sorte
que cette nouvelle classe ne définisse aucun nouveau champ sur la classe
mère. Cela ne change dès lors rien à la traduction du modèle de données
en SQL, puisque la classe mère sera traduite par une table, et la classe
fille ira récupérer les mêmes informations dans la même table: elle ne
fera qu'ajouter ou modifier un comportement dynamiquement, sans ajouter
d'emplacements de stockage supplémentaires.
Nous pourrions ainsi définir les classes suivantes:
\begin{Shaded}
\begin{Highlighting}[]
\CommentTok{\# wish/models.py}
\KeywordTok{class}\NormalTok{ Wishlist(models.Model):}
\NormalTok{ name }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{255}\NormalTok{)}
\NormalTok{ description }\OperatorTok{=}\NormalTok{ models.CharField(max\_length}\OperatorTok{=}\DecValTok{2000}\NormalTok{)}
\NormalTok{ expiration\_date }\OperatorTok{=}\NormalTok{ models.DateField()}
\AttributeTok{@staticmethod}
\KeywordTok{def}\NormalTok{ create(}\VariableTok{self}\NormalTok{, name, description, expiration\_date}\OperatorTok{=}\VariableTok{None}\NormalTok{):}
\NormalTok{ wishlist }\OperatorTok{=}\NormalTok{ Wishlist()}
\NormalTok{ wishlist.name }\OperatorTok{=}\NormalTok{ name}
\NormalTok{ wishlist.description }\OperatorTok{=}\NormalTok{ description}
\NormalTok{ wishlist.expiration\_date }\OperatorTok{=}\NormalTok{ expiration\_date}
\NormalTok{ wishlist.save()}
\ControlFlowTok{return}\NormalTok{ wishlist}
\KeywordTok{class}\NormalTok{ ChristmasWishlist(Wishlist):}
\KeywordTok{class}\NormalTok{ Meta:}
\NormalTok{ proxy }\OperatorTok{=} \VariableTok{True}
\AttributeTok{@staticmethod}
\KeywordTok{def}\NormalTok{ create(}\VariableTok{self}\NormalTok{, name, description):}
\NormalTok{ christmas }\OperatorTok{=}\NormalTok{ datetime(current\_year, }\DecValTok{12}\NormalTok{, }\DecValTok{31}\NormalTok{)}
\NormalTok{ w }\OperatorTok{=}\NormalTok{ Wishlist.create(name, description, christmas)}
\NormalTok{ w.save()}
\KeywordTok{class}\NormalTok{ EasterWishlist(Wishlist):}
\KeywordTok{class}\NormalTok{ Meta:}
\NormalTok{ proxy }\OperatorTok{=} \VariableTok{True}
\AttributeTok{@staticmethod}
\KeywordTok{def}\NormalTok{ create(}\VariableTok{self}\NormalTok{, name, description):}
\NormalTok{ expiration\_date }\OperatorTok{=}\NormalTok{ datetime(current\_year, }\DecValTok{4}\NormalTok{, }\DecValTok{1}\NormalTok{)}
\NormalTok{ w }\OperatorTok{=}\NormalTok{ Wishlist.create(name, description, expiration\_date)}
\NormalTok{ w.save()}
\end{Highlighting}
\end{Shaded}
Gestion des utilisateurs
Dans les spécifications, nous souhaitions pouvoir associer un
utilisateur à une liste (\textbf{le propriétaire}) et un utilisateur à
une part (\textbf{le donateur}). Par défaut, Django offre une gestion
simplifiée des utilisateurs (pas de connexion LDAP, pas de double
authentification, \ldots\hspace{0pt}): juste un utilisateur et un mot de
passe. Pour y accéder, un paramètre par défaut est défini dans votre
fichier de settings: \texttt{AUTH\_USER\_MODEL}.
\hypertarget{_khana}{%
\section{Khana}\label{_khana}}
Khana est une application de suivi d'apprentissage pour des élèves ou
étudiants. Nous voulons pouvoir:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Lister les élèves
\item
Faire des listes de présence pour les élèves
\item
Pouvoir planifier ses cours
\item
Pouvoir suivre l'apprentissage des élèves, les liens qu'ils ont entre
les éléments à apprendre:
\item
pour écrire une phrase, il faut pouvoir écrire des mots, connaître la
grammaire, et connaître la conjugaison
\item
pour écrire des mots, il faut savoir écrire des lettres
\item
\ldots\hspace{0pt}
\end{enumerate}
Plusieurs professeurs s'occupent d'une même classe; il faut pouvoir
écrire des notes, envoyer des messages aux autres professeurs, etc.
Il faut également pouvoir définir des dates de contrôle, voir combien de
semaines il reste pour s'assurer d'avoir vu toute la matiètre.
Et pouvoir encoder les points des contrôles.
\begin{figure}
\centering
\includegraphics{images/django/django-project-vs-apps-khana.png}
\caption{Khana}
\end{figure}
\end{document}

View File

@ -5,6 +5,13 @@ Ce dont nous n'avons pas parlé cependant, c'est la manière dont l'utilisateur
La \href{https://docs.djangoproject.com/en/stable/topics/auth/}{documentation} est très complète, nous allons essayer de la simplifier au maximum.
Accrochez-vous, le sujet peut être complexe.
\section{Utilisateurs}
Dans les spécifications, nous souhaitions pouvoir associer un utilisateur à une liste (\textbf{le propriétaire}) et un utilisateur à une part (\textbf{le donateur}).
Par défaut, Django offre une gestion simplifiée des utilisateurs (pas de connexion LDAP, pas de double
authentification, ...: juste un utilisateur et un mot de passe.
Pour y accéder, un paramètre par défaut est défini dans votre fichier de settings: \texttt{AUTH\_USER\_MODEL}.
\section{Mécanisme d'authentification}
On peut schématiser le flux d'authentification de la manière suivante :

View File

@ -0,0 +1,48 @@
\chapter{Context Processors}
Mise en pratique: un \emph{context processor} sert \emph{grosso-modo} à
peupler l'ensemble des données transmises des vues aux templates avec
des données communes. Un context processor est un peu l'équivalent d'un
middleware, mais entre les données et les templates, là où le middleware
va s'occuper des données relatives aux réponses et requêtes elles-mêmes.
\begin{minted}{python}
# core/context_processors.py
import subprocess
def git_describe(request) -> str:
return {
"git_describe": subprocess.check_output(
["git", "describe", "--always"]
).strip(),
"git_date": subprocess.check_output(
["git", "show", "-s", r"--format=%cd", r"--date=format:%d-%m-%Y"]
),
}
\end{minted}
Ceci aura pour effet d'ajouter les deux variables \texttt{git\_describe}
et \texttt{git\_date} dans tous les contextes de tous les templates de
l'application.
\begin{minted}{python}
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, "templates"),],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
"core.context_processors.git_describe"
],
},
},
]
\end{minted}

119
chapters/deployments.tex Normal file
View File

@ -0,0 +1,119 @@
\chapter{Debian}
La première étape pour la configuration de notre hôte consiste à définir les utilisateurs et groupes de droits. Il est faut absolument éviter de faire tourner une application en tant qu'utilisateur \textbf{root}, car la moindre faille pourrait avoir des conséquences catastrophiques.
Une fois que ces utilisateurs seront configurés, nous pourrons passer à l'étape de configuration, qui consistera à:
\begin{enumerate}
\item
Déployer les sources
\item
Démarrer un serveur implémentant une interface WSGI (\textbf{Web Server Gateway Interface}), qui sera chargé de créer autant de petits lutins travailleurs que nous le désirerons.
\item
Démarrer un superviseur, qui se chargera de veiller à la bonne santé de nos petits travailleurs, et en créer de nouveaux s'il le juge nécessaire
\item
Configurer un proxy inverse, qui s'occupera d'envoyer les requêtes d'un utilisateur externe à la machine hôte vers notre serveur applicatif, qui la communiquera à l'un des travailleurs.
\end{enumerate}
La machine hôte peut être louée chez Digital Ocean, Scaleway, OVH, Vultr, ... Il existe des dizaines d'hébergements typés VPS (\textbf{Virtual Private Server}). A vous de choisir celui qui vous convient \footnote{Personnellement, j'ai un petit faible pour Hetzner Cloud}.
\section{Dépendances systèmes}
\section{Base de données}
On l'a déjà vu, Django se base sur un pattern type
\href{https://www.martinfowler.com/eaaCatalog/activeRecord.html}{ActiveRecords}
pour la gestion de la persistance des données et supporte les principaux
moteurs de bases de données connus:
\begin{itemize}
\item
SQLite (en natif, mais Django 3.0 exige une version du moteur
supérieure ou égale à la 3.8)
\item
MariaDB (en natif depuis Django 3.0),
\item
PostgreSQL au travers de psycopg2 (en natif aussi),
\item
Microsoft SQLServer grâce aux drivers {[}\ldots\hspace{0pt}à
compléter{]}
\item
Oracle via
\href{https://oracle.github.io/python-cx_Oracle/}{cx\_Oracle}.
\end{itemize}
Chaque pilote doit être utilisé précautionneusement ! Chaque version de Django n'est pas toujours compatible avec chacune des versions des pilotes, et chaque moteur de base de données nécessite parfois une
version spécifique du pilote. Par ce fait, vous serez parfois bloqué sur une version de Django, simplement parce que votre serveur de base de données se trouvera dans une version spécifique (eg. Django 2.3 à cause
d'un Oracle 12.1).
Ci-dessous, quelques procédures d'installation pour mettre un serveur à disposition. Les deux plus simples seront MariaDB et PostgreSQL, qu'on couvrira ci-dessous. Oracle et Microsoft SQLServer se trouveront en
annexes.
\subsection{PostgreSQL}
On commence par installer PostgreSQL.
Dans le cas de debian, on exécute la commande suivante:
\begin{verbatim}
# aptitude install postgresql postgresql-contrib
\end{verbatim}
Ensuite, on crée un utilisateur pour la DB:
\begin{verbatim}
# su - postgres
postgres@gwift:~$ createuser --interactive -P
Enter name of role to add: gwift_user
Enter password for new role:
Enter it again:
Shall the new role be a superuser? (y/n) n
Shall the new role be allowed to create databases? (y/n) n
Shall the new role be allowed to create more new roles? (y/n) n
postgres@gwift:~$
\end{verbatim}
Finalement, on peut créer la DB:
\begin{verbatim}
postgres@gwift:~$ createdb --owner gwift_user gwift
postgres@gwift:~$ exit
logout
\end{verbatim}
\subsection{MariaDB}
Idem, installation, configuration, backup, tout ça. A copier de grimboite, je suis sûr davoir
des notes là-dessus.
\section{Environment utilisateur}
\begin{verbatim}
su - gwift
cp /etc/skel/.bashrc .
cp /etc/skel/.bash_profile .
ssh-keygen
mkdir bin
mkdir .venvs
mkdir webapps
python3.6 -m venv .venvs/gwift
source .venvs/gwift/bin/activate
cd /home/gwift/webapps
git clone ...
\end{verbatim}
La clé SSH doit ensuite être renseignée au niveau du dépôt, afin de pouvoir y accéder.
A ce stade, on devrait déjà avoir quelque chose de fonctionnel en démarrant les commandes
suivantes:
\begin{verbatim}
# en tant qu'utilisateur 'gwift'
source .venvs/gwift/bin/activate
pip install -U pip
pip install -r requirements/base.txt
pip install gunicorn
cd webapps/gwift
gunicorn config.wsgi:application --bind localhost:3000 --settings
=config.settings_production
\end{verbatim}

2
chapters/heroku.tex Normal file
View File

@ -0,0 +1,2 @@
\chapter{PaaS - Heroku}

173
chapters/infrastructure.tex Normal file
View File

@ -0,0 +1,173 @@
\chapter{Infrastructure et composants}
Pour une mise ne production, le standard \emph{de facto} est le suivant:
\begin{itemize}
\item
Nginx comme reverse proxy
\item
HAProxy pour la distribution de charge
\item
Gunicorn ou Uvicorn comme serveur d'application
\item
Supervisor pour le monitoring
\item
PostgreSQL ou MySQL/MariaDB comme bases de données.
\item
Celery et RabbitMQ pour l'exécution de tâches asynchrones
\item
Redis / Memcache pour la mise à en cache (et pour les sessions ? A
vérifier).
\item
Sentry, pour le suivi des bugs
\end{itemize}
Si nous schématisons l'infrastructure et le chemin parcouru par une requête, nous pourrions arriver à la synthèse suivante:
\begin{enumerate}
\item
L'utilisateur fait une requête via son navigateur (Firefox ou Chrome)
\item
Le navigateur envoie une requête http, sa version, un verbe (GET,
POST, \ldots\hspace{0pt}), un port et éventuellement du contenu
\item
Le firewall du serveur (Debian GNU/Linux, CentOS, ...
vérifie si la requête peut être prise en compte
\item
La requête est transmise à l'application qui écoute sur le port
(probablement 80 ou 443; et \emph{a priori} Nginx)
\item
Elle est ensuite transmise par socket et est prise en compte par un
des \emph{workers} (= un processus Python) instancié par Gunicorn. Si
l'un de ces travailleurs venait à planter, il serait automatiquement
réinstancié par Supervisord.
\item
Qui la transmet ensuite à l'un de ses \emph{workers} (= un processus
Python).
\item
Après exécution, une réponse est renvoyée à l'utilisateur.
\end{enumerate}
\includegraphics{images/diagrams/architecture.png}
\section{Reverse proxy}
Le principe du \textbf{proxy inverse} est de pouvoir rediriger du trafic entrant vers une application hébergée sur le système. Il serait tout à fait possible de rendre notre application directement accessible depuis l'extérieur, mais le proxy a aussi l'intérêt de pouvoir élever la sécurité du serveur (SSL) et décharger le serveur applicatif grâce à un mécanisme de cache ou en compressant certains résultats \footnote{\url{https://fr.wikipedia.org/wiki/Proxy_inverse}}
\section{Load balancer}
\section{Workers}
\section{Supervision des processus}
\section{Bases de données}
\section{Tâches asynchrones}
\section{Mise en cache}
\section{Niveau application}
Au niveau logiciel (la partie mise en subrillance ci-dessus), la requête arrive dans les mains du processus Python, qui doit encore
\begin{enumerate}
\item
effectuer le routage des données,
\item
trouver la bonne fonction à exécuter,
\item
récupérer les données depuis la base de données,
\item
effectuer le rendu ou la conversion des données,
\item
et renvoyer une réponse à l'utilisateur.
\end{enumerate}
Comme nous l'avons vu dans la première partie, Django est un framework complet, intégrant tous les mécanismes nécessaires à la bonne évolution d'une application. Il est possible de démarrer petit, et de suivre l'évolution des besoins en fonction de la charge estimée ou ressentie, d'ajouter un mécanisme de mise en cache, des logiciels de suivi, ...
\section{Supervision globale}
\subsection{Journaux}
La structure des niveaux de journaux est essentielle.
\begin{quote}
When deciding whether a message should be ERROR or WARN, imagine being woken up at 4 a.m.
Low printer toner is not an ERROR.
--- Dan North former ToughtWorks consultant
\end{quote}
\begin{itemize}
\item
\textbf{DEBUG}: Il s'agit des informations qui concernent tout ce qui peut se passer durant l'exécution de l'application. Généralement, ce niveau est désactivé pour une application qui passe en production,
sauf s'il est nécessaire d'isoler un comportement en particulier, auquel cas il suffit de le réactiver temporairement.
\item
\textbf{INFO}: Enregistre les actions pilotées par un utilisateur - Démarrage de la transaction de paiement, ...
\item
\textbf{WARN}: Regroupe les informations qui pourraient potentiellement devenir des erreurs.
\item
\textbf{ERROR}: Indique les informations internes - Erreur lors de l'appel d'une API, erreur interne, ...
\item
\textbf{FATAL} (ou \textbf{EXCEPTION}): ... généralement suivie d'une terminaison du programme - Bind raté
d'un socket, etc.
\end{itemize}
La configuration des \emph{loggers} est relativement simple, un peu plus complexe si nous nous penchons dessus, et franchement complète si nous creusons encore. Il est ainsi possible de définir des formattages,
gestionnaires (\emph{handlers}) et loggers distincts, en fonction de nos applications.
Sauf que comme nous l'avons vu avec les 12 facteurs, nous devons traiter les informations de notre application comme un flux d'évènements. Il n'est donc pas réellement nécessaire de chipoter la configuration,
puisque la seule classe qui va réellement nous intéresser concerne les \texttt{StreamHandler}. La configuration que nous allons utiliser est celle-ci:
\begin{enumerate}
\item
Formattage: à définir - mais la variante suivante est complète, lisible et pratique:
\texttt{\{levelname\}\ \{asctime\}\ \{module\}\ \{process:d\}\ \{thread:d\}\ \{message\}}
\item
Handler: juste un, qui définit un \texttt{StreamHandler}
\item
Logger: pour celui-ci, nous avons besoin d'un niveau (\texttt{level})
et de savoir s'il faut propager les informations vers les
sous-paquets, auquel cas il nous suffira de fixer la valeur de
\texttt{propagate} à \texttt{True}.
\end{enumerate}
Pour utiliser nos loggers, il suffit de copier le petit bout de code suivant:
\begin{minted}{python}
import logging
logger = logging.getLogger(__name__)
logger.debug('helloworld')
\end{minted}
\href{https://docs.djangoproject.com/en/stable/topics/logging/\#examples}{Par
exemples}.
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Sentry via sentry\_sdk
\item
Nagios
\item
LibreNMS
\item
Zabbix
\end{enumerate}
Il existe également \href{https://munin-monitoring.org}{Munin},
\href{https://www.elastic.co}{Logstash, ElasticSearch et Kibana
(ELK-Stack)} ou \href{https://www.fluentd.org}{Fluentd}.
\subsection{Zabbix, Nagios, ...}
\subsection{Sentry}
\subsection{Greylog}

35
chapters/khana.tex Normal file
View File

@ -0,0 +1,35 @@
\chapter{Khana}
Khana est une application de suivi d'apprentissage pour des élèves ou étudiants.
Nous voulons pouvoir:
\begin{enumerate}
\item
Lister les élèves
\item
Faire des listes de présence pour les élèves
\item
Pouvoir planifier ses cours
\item
Pouvoir suivre l'apprentissage des élèves, les liens qu'ils ont entre
les éléments à apprendre:
\item
pour écrire une phrase, il faut pouvoir écrire des mots, connaître la
grammaire, et connaître la conjugaison
\item
pour écrire des mots, il faut savoir écrire des lettres
\item
\ldots\hspace{0pt}
\end{enumerate}
Plusieurs professeurs s'occupent d'une même classe; il faut pouvoir écrire des notes, envoyer des messages aux autres professeurs, etc.
Il faut également pouvoir définir des dates de contrôle, voir combien de semaines il reste pour s'assurer d'avoir vu toute la matiètre.
Et pouvoir encoder les points des contrôles.
\begin{figure}
\centering
\includegraphics{images/django/django-project-vs-apps-khana.png}
\caption{Khana}
\end{figure}

View File

@ -559,6 +559,199 @@ Par défaut, le gestionnaire est accessible au travers de la propriété
filtres bien spécifiques.
\end{enumerate}
\section{Refactoring et héritages}
On constate que plusieurs classes possèdent les mêmes propriétés \texttt{created\_at} et \texttt{updated\_at}, initialisées aux mêmes valeurs.
Pour gagner en cohérence, nous allons créer une classe dans laquelle nous définirons ces deux champs, et nous ferons en sorte que les classes \texttt{Wishlist}, \texttt{Item} et \texttt{Part} en
héritent.
Django gère trois sortes d'héritage:
\begin{itemize}
\item
L'héritage par classe abstraite
\item
L'héritage classique
\item
L'héritage par classe proxy.
\end{itemize}
\subsection{Classes abstraites}
L'héritage par classe abstraite consiste à déterminer une classe mère
qui ne sera jamais instanciée. C'est utile pour définir des champs qui
se répèteront dans plusieurs autres classes et surtout pour respecter le
principe de DRY. Comme la classe mère ne sera jamais instanciée, ces
champs seront en fait dupliqués physiquement, et traduits en SQL, dans
chacune des classes filles.
\begin{minted}{python}
# wish/models.py
class AbstractModel(models.Model):
class Meta:
abstract = True
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Wishlist(AbstractModel):
pass
class Item(AbstractModel):
pass
class Part(AbstractModel):
pass
\end{minted}
En traduisant ceci en SQL, on aura en fait trois tables, chacune
reprenant les champs \texttt{created\_at} et \texttt{updated\_at}, ainsi
que son propre identifiant:
\begin{minted}{sql}
--$ python manage.py sql wish
BEGIN;
CREATE TABLE "wish_wishlist" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"created_at" datetime NOT NULL,
"updated_at" datetime NOT NULL
)
;
CREATE TABLE "wish_item" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"created_at" datetime NOT NULL,
"updated_at" datetime NOT NULL
)
;
CREATE TABLE "wish_part" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"created_at" datetime NOT NULL,
"updated_at" datetime NOT NULL
)
;
COMMIT;
\end{minted}
\subsection{Héritage classique}
L'héritage classique est généralement déconseillé, car il peut
introduire très rapidement un problème de performances: en reprenant
l'exemple introduit avec l'héritage par classe abstraite, et en omettant
l'attribut \texttt{abstract\ =\ True}, on se retrouvera en fait avec
quatre tables SQL:
\begin{itemize}
\item
Une table \texttt{AbstractModel}, qui reprend les deux champs
\texttt{created\_at} et \texttt{updated\_at}
\item
Une table \texttt{Wishlist}
\item
Une table \texttt{Item}
\item
Une table \texttt{Part}.
\end{itemize}
A nouveau, en analysant la sortie SQL de cette modélisation, on obtient
ceci:
\begin{minted}{sql}
--$ python manage.py sql wish
BEGIN;
CREATE TABLE "wish_abstractmodel" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"created_at" datetime NOT NULL,
"updated_at" datetime NOT NULL
)
;
CREATE TABLE "wish_wishlist" (
"abstractmodel_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES
"wish_abstractmodel" ("id")
)
;
CREATE TABLE "wish_item" (
"abstractmodel_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES
"wish_abstractmodel" ("id")
)
;
CREATE TABLE "wish_part" (
"abstractmodel_ptr_id" integer NOT NULL PRIMARY KEY REFERENCES
"wish_abstractmodel" ("id")
)
;
COMMIT;
\end{minted}
Le problème est que les identifiants seront définis et incrémentés au
niveau de la table mère. Pour obtenir les informations héritées, nous
seront obligés de faire une jointure. En gros, impossible d'obtenir les
données complètes pour l'une des classes de notre travail de base sans
effectuer un \textbf{join} sur la classe mère.
Dans ce sens, cela va encore\ldots\hspace{0pt} Mais imaginez que vous
définissiez une classe \texttt{Wishlist}, de laquelle héritent les
classes \texttt{ChristmasWishlist} et \texttt{EasterWishlist}: pour
obtenir la liste complètes des listes de souhaits, il vous faudra faire
une jointure \textbf{externe} sur chacune des tables possibles, avant
même d'avoir commencé à remplir vos données. Il est parfois nécessaire
de passer par cette modélisation, mais en étant conscient des risques
inhérents.
\subsection{Classes Proxy}
Lorsqu'on définit une classe de type \textbf{proxy}, on fait en sorte
que cette nouvelle classe ne définisse aucun nouveau champ sur la classe
mère. Cela ne change dès lors rien à la traduction du modèle de données
en SQL, puisque la classe mère sera traduite par une table, et la classe
fille ira récupérer les mêmes informations dans la même table: elle ne
fera qu'ajouter ou modifier un comportement dynamiquement, sans ajouter
d'emplacements de stockage supplémentaires.
Nous pourrions ainsi définir les classes suivantes:
\begin{minted}{python}
# wish/models.py
class Wishlist(models.Model):
name = models.CharField(max_length=255)
description = models.CharField(max_length=2000)
expiration_date = models.DateField()
@staticmethod
def create(self, name, description, expiration_date=None):
wishlist = Wishlist()
wishlist.name = name
wishlist.description = description
wishlist.expiration_date = expiration_date
wishlist.save()
return wishlist
class ChristmasWishlist(Wishlist):
class Meta:
proxy = True
@staticmethod
def create(self, name, description):
christmas = datetime(current_year, 12, 31)
w = Wishlist.create(name, description, christmas)
w.save()
class EasterWishlist(Wishlist):
class Meta:
proxy = True
@staticmethod
def create(self, name, description):
expiration_date = datetime(current_year, 4, 1)
w = Wishlist.create(name, description, expiration_date)
w.save()
\end{minted}
\section{Conclusions}

9
chapters/resources.tex Normal file
View File

@ -0,0 +1,9 @@
\chapter{Annexes}
\section{Liens et librairies utiles}
\begin{itemize}
\item
`Django factory boy
\textless{}\url{https://github.com/rbarrois/django-factory_boy/tree/v1.0.0\%3E\%60_}
\end{itemize}

View File

@ -60,28 +60,17 @@
\include{chapters/migrations.tex}
\include{chapters/administration.tex}
\include{chapters/forms.tex}
\chapter{Processus d'authentification}
\include{chapters/authentication.tex}
\chapter{Context Processors}
\chapter{Conclusions}
\part{Méthodes de déploiement}
\include{parts/deployment.tex}
\chapter{Infrastructure et composants}
\chapter{Code source}
\chapter{Outils de supervision et de mise à disposition}
\chapter{Journaux}
\chapter{Méthodes}
\chapter{On-premise: Debian}
\chapter{PaaS: Heroku}
\include{chapters/infrastructure.tex}
\include{chapters/deployments.tex}
\include{chapters/heroku.tex}
\chapter{Outils complémentaires}
@ -109,9 +98,7 @@
\chapter{Gwift}
\chapter{Khana}
\chapter{Refactoring}
\include{chapters/khana.tex}
\chapter{Conclusions}
@ -126,6 +113,8 @@
\include{glossary.tex}
\include{resources.tex}
\include{chapters/thanks.tex}
\end{document}

100
parts/deployment.tex Normal file
View File

@ -0,0 +1,100 @@
\part{Déploiement}
\begin{quote}
To be effective, a software system must be deployable. The higher the
cost of deployements, the less useful the system is. A goal of a
software architecture, then, should be to make a system that can be
easily deployed with a single action. Unfortunately, deployment strategy
is seldom considered during initial development. This leads to
architectures that may be make the system easy to develop, but leave it
very difficult to deploy.
--- Robert C. Martin Clean Architecture
\end{quote}
Il y a une raison très simple à aborder le déploiement dès maintenant: à trop attendre et à peaufiner son développement en local, on en oublie que sa finalité sera de se retrouver exposé et accessible depuis un
serveur.
Il est du coup probable d'oublier une partie des désidérata, de zapper une fonctionnalité essentielle ou simplement de passer énormément de temps à adapter les sources pour qu'elles puissent être mises à
disposition sur un environnement en particulier, une fois que leur développement aura été finalisé, testé et validé.
Un bon déploiement ne doit pas dépendre de dizaines de petits scripts éparpillés sur le disque. L'objectif est qu'il soit rapide et fiable. Ceci peut être atteint au travers d'un partitionnement correct, incluant le fait que le composant principal s'assure que chaque sous-composant est correctement démarré intégré et supervisé.
Aborder le déploiement dès le début permet également de rédiger dès le début les procédures d'installation, de mises à jour et de sauvegardes. A la fin de chaque intervalle de développement, les fonctionnalités auront dû avoir été intégrées, testées, fonctionnelles et un code propre, démontrable dans un environnement similaire à un environnement de production, et créées à partir d'un tronc commun au développement
\cite{devops_handbook}.
Déploier une nouvelle version sera aussi simple que de récupérer la dernière archive depuis le dépôt, la placer dans le bon répertoire, appliquer des actions spécifiques (et souvent identiques entre deux
versions), puis redémarrer les services adéquats, et la procédure complète se résumera à quelques lignes d'un script bash.
\begin{quote}
Because value is created only when our services are running into production, we must ensure that we are not only delivering fast flow, but that our deployments can also be performed without causing chaos and
disruptions such as service outages, service impairments, or security or compliance failures.
--- DevOps Handbook Introduction
\end{quote}
Le serveur que django met à notre disposition \emph{via} la commande \texttt{runserver} est extrêmement pratique, mais il est uniquement prévu pour la phase développement: en production, il est inutile de
passer par du code Python pour charger des fichiers statiques (feuilles de style, fichiers JavaScript, images, \ldots\hspace{0pt}). De même, Django propose par défaut une base de données SQLite, qui fonctionne
parfaitement dès lors que l'on connait ses limites et que l'on se limite à un utilisateur à la fois. En production, il est légitime que la base de donnée soit capable de supporter plusieurs utilisateurs et connexions simultanés. En restant avec les paramètres par défaut, il est plus que probable que vous rencontriez rapidement des erreurs de verrou parce qu'un autre processus a déjà pris la main pour écrire ses données. En bref, vous avez quelque chose qui fonctionne, qui répond à un besoin, mais qui va attirer la grogne de ses utilisateurs pour des problèmes de latences, pour des erreurs de verrou ou simplement parce que le serveur répondra trop lentement.
L'objectif de cette partie est de parcourir les différentes possibilités qui s'offrent à nous en termes de déploiement, tout en faisant en sorte que le code soit le moins couplé possible à sa destination de
production. L'objectif est donc de faire en sorte qu'une même application puisse être hébergées par plusieurs hôtes sans avoir à subir de modifications. Nous vous renvoyons vers les 12-facteurs dont nous
avons déjà parlé et qui vous énormément nous aider, puisque ce sont des variables d'environnement qui vont réellement piloter le câblage entre l'application, ses composants et son hébergeur.
RedHat proposait récemment un article intitulé \emph{*What Is IaaS*}, qui présentait les principales différences entre types d'hébergement.
\begin{figure}
\centering
\includegraphics{images/deployment/iaas_focus-paas-saas-diagram.png}
\caption{L'infrastructure en tant que service, cc. \emph{RedHat Cloud
Computing}}
\end{figure}
Ainsi, on trouve:
\begin{enumerate}
\def\labelenumi{\arabic{enumi}.}
\item
Le déploiment \emph{on-premises} ou \emph{on-site}
\item
Les \emph{Infrastructures as a service} ou \emph{IaaSIaaS}
\item
Les \emph{Platforms as a service} ou \emph{PaaSPaaS}
\item
Les \emph{Softwares as a service} ou \emph{SaaSSaaS}, ce dernier point
nous concernant moins, puisque c'est nous qui développons le logiciel.
\end{enumerate}
Dans cette partie, nous aborderons les points suivants:
\begin{enumerate}
\item
Définir l'infrastructure et les composants nécessaires à notre
application
\item
Configurer l'hôte qui hébergera l'application et y déployer notre
application: dans une machine physique, virtuelle ou dans un
container. Nous aborderons aussi les déploiements via Ansible et Salt.
A ce stade, nous aurons déjà une application disponible.
\item
Configurer les outils nécessaires à la bonne exécution de ce code et
de ses fonctionnalités: les différentes méthodes de supervision de
l'application, comment analyser les fichiers de logs, comment
intercepter correctement une erreur si elle se présente et comment
remonter correctement l'information.
\end{enumerate}
Nous allons détailler ci-dessous trois méthodes de déploiement:
\begin{itemize}
\item
Sur une machine hôte, en embarquant tous les composants sur un même
serveur. Ce ne sera pas idéal, puisqu'il ne sera pas possible de
configurer un \emph{load balancer}, de routeur plusieurs basées de
données, mais ce sera le premier cas de figure.
\item
Dans des containers, avec Docker-Compose.
\item
Sur une \textbf{Plateforme en tant que Service} (ou plus simplement,
\textbf{PaaSPaaS}), pour faire abstraction de toute la couche de
configuration du serveur.
\end{itemize}