diff --git a/asciidoc-to-tex.tex b/asciidoc-to-tex.tex index fd34f65..c275454 100644 --- a/asciidoc-to-tex.tex +++ b/asciidoc-to-tex.tex @@ -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} diff --git a/chapters/authentication.tex b/chapters/authentication.tex index 76d14b7..32a63bf 100644 --- a/chapters/authentication.tex +++ b/chapters/authentication.tex @@ -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 : diff --git a/chapters/contexts-processors.tex b/chapters/contexts-processors.tex new file mode 100644 index 0000000..360c7f1 --- /dev/null +++ b/chapters/contexts-processors.tex @@ -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} diff --git a/chapters/deployments.tex b/chapters/deployments.tex new file mode 100644 index 0000000..6ba2b04 --- /dev/null +++ b/chapters/deployments.tex @@ -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 d’avoir +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} diff --git a/chapters/heroku.tex b/chapters/heroku.tex new file mode 100644 index 0000000..329dcc6 --- /dev/null +++ b/chapters/heroku.tex @@ -0,0 +1,2 @@ +\chapter{PaaS - Heroku} + diff --git a/chapters/infrastructure.tex b/chapters/infrastructure.tex new file mode 100644 index 0000000..102c8de --- /dev/null +++ b/chapters/infrastructure.tex @@ -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} + diff --git a/chapters/khana.tex b/chapters/khana.tex new file mode 100644 index 0000000..eed1363 --- /dev/null +++ b/chapters/khana.tex @@ -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} \ No newline at end of file diff --git a/chapters/models.tex b/chapters/models.tex index 32bb3f7..a5e1b17 100644 --- a/chapters/models.tex +++ b/chapters/models.tex @@ -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} diff --git a/chapters/resources.tex b/chapters/resources.tex new file mode 100644 index 0000000..95220f5 --- /dev/null +++ b/chapters/resources.tex @@ -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} diff --git a/main.tex b/main.tex index 40d0b21..9f282fb 100644 --- a/main.tex +++ b/main.tex @@ -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} diff --git a/parts/deployment.tex b/parts/deployment.tex new file mode 100644 index 0000000..717432b --- /dev/null +++ b/parts/deployment.tex @@ -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}