\begin{Shaded} \begin{Highlighting}[] \KeywordTok{(}\ExtensionTok{gwift{-}env}\KeywordTok{)} \ExtensionTok{fred@aerys}\NormalTok{:\textasciitilde{}/Sources/gwift$ tree .} \ExtensionTok{.} \NormalTok{├── }\ExtensionTok{gwift} \NormalTok{│   ├── }\ExtensionTok{\_\_init\_\_.py} \NormalTok{│   ├── }\ExtensionTok{asgi.py} \NormalTok{│   ├── }\ExtensionTok{settings.py} \NormalTok{│   ├── }\ExtensionTok{urls.py} \NormalTok{│   ├── }\ExtensionTok{wish} \NormalTok{│   │   ├── }\ExtensionTok{\_\_init\_\_.py} \NormalTok{│   │   ├── }\ExtensionTok{admin.py} \NormalTok{│   │   ├── }\ExtensionTok{apps.py} \NormalTok{│   │   ├── }\ExtensionTok{migrations} \NormalTok{│   │   │   └── }\ExtensionTok{\_\_init\_\_.py} \NormalTok{│   │   ├── }\ExtensionTok{models.py} \NormalTok{│   │   ├── }\ExtensionTok{tests.py} \NormalTok{│   │   └── }\ExtensionTok{views.py} \NormalTok{│   └── }\ExtensionTok{wsgi.py} \NormalTok{├── }\ExtensionTok{Makefile} \NormalTok{├── }\ExtensionTok{manage.py} \NormalTok{├── }\ExtensionTok{README.md} \NormalTok{├── }\ExtensionTok{requirements} \NormalTok{│   ├── }\ExtensionTok{base.txt} \NormalTok{│   ├── }\ExtensionTok{dev.txt} \NormalTok{│   └── }\ExtensionTok{prod.txt} \NormalTok{├── }\ExtensionTok{setup.cfg} \NormalTok{└── }\ExtensionTok{tox.ini} \ExtensionTok{5}\NormalTok{ directories, 22 files} \end{Highlighting} \end{Shaded} \hypertarget{_structure_finale_de_notre_environnement}{% \subsection{Structure finale de notre environnement}\label{_structure_finale_de_notre_environnement}} \begin{Shaded} \begin{Highlighting}[] \KeywordTok{(}\ExtensionTok{gwift{-}env}\KeywordTok{)} \ExtensionTok{fred@aerys}\NormalTok{:\textasciitilde{}/Sources/gwift$ tree .} \ExtensionTok{.} \NormalTok{├── }\ExtensionTok{gwift} \NormalTok{│   ├── }\ExtensionTok{\_\_init\_\_.py} \NormalTok{│   ├── }\ExtensionTok{asgi.py} \NormalTok{│   ├── }\ExtensionTok{settings.py} \NormalTok{│   ├── }\ExtensionTok{urls.py} \NormalTok{│   ├── }\ExtensionTok{wish} \NormalTok{│   │   ├── }\ExtensionTok{\_\_init\_\_.py} \NormalTok{│   │   ├── }\ExtensionTok{admin.py} \NormalTok{│   │   ├── }\ExtensionTok{apps.py} \NormalTok{│   │   ├── }\ExtensionTok{migrations} \NormalTok{│   │   │   └── }\ExtensionTok{\_\_init\_\_.py} \NormalTok{│   │   ├── }\ExtensionTok{models.py} \NormalTok{│   │   ├── }\ExtensionTok{tests.py} \NormalTok{│   │   └── }\ExtensionTok{views.py} \NormalTok{│   └── }\ExtensionTok{wsgi.py} \NormalTok{├── }\ExtensionTok{Makefile} \NormalTok{├── }\ExtensionTok{manage.py} \NormalTok{├── }\ExtensionTok{README.md} \NormalTok{├── }\ExtensionTok{requirements} \NormalTok{│   ├── }\ExtensionTok{base.txt} \NormalTok{│   ├── }\ExtensionTok{dev.txt} \NormalTok{│   └── }\ExtensionTok{prod.txt} \NormalTok{├── }\ExtensionTok{setup.cfg} \NormalTok{└── }\ExtensionTok{tox.ini} \end{Highlighting} \end{Shaded} \hypertarget{_authentification}{% \section{Authentification}\label{_authentification}} \begin{Shaded} \begin{Highlighting}[] \ImportTok{from}\NormalTok{ datetime }\ImportTok{import}\NormalTok{ datetime} \ImportTok{from}\NormalTok{ django.contrib.auth }\ImportTok{import}\NormalTok{ backends, get\_user\_model} \ImportTok{from}\NormalTok{ django.db.models }\ImportTok{import}\NormalTok{ Q} \ImportTok{from}\NormalTok{ accounts.models }\ImportTok{import}\NormalTok{ Token } \NormalTok{UserModel }\OperatorTok{=}\NormalTok{ get\_user\_model()} \KeywordTok{class}\NormalTok{ TokenBackend(backends.ModelBackend):} \KeywordTok{def}\NormalTok{ authenticate(}\VariableTok{self}\NormalTok{, request, username}\OperatorTok{=}\VariableTok{None}\NormalTok{, password}\OperatorTok{=}\VariableTok{None}\NormalTok{, }\OperatorTok{**}\NormalTok{kwargs):} \CommentTok{"""Authentifie l\textquotesingle{}utilisateur sur base d\textquotesingle{}un jeton qu\textquotesingle{}il a reçu.} \CommentTok{ On regarde la date de validité de chaque jeton avant d\textquotesingle{}autoriser l\textquotesingle{}accès.} \CommentTok{ """} \NormalTok{ token }\OperatorTok{=}\NormalTok{ kwargs.get(}\StringTok{"token"}\NormalTok{, }\VariableTok{None}\NormalTok{)} \NormalTok{ current\_token }\OperatorTok{=}\NormalTok{ Token.objects.}\BuiltInTok{filter}\NormalTok{(token}\OperatorTok{=}\NormalTok{token, validity\_date\_\_gte}\OperatorTok{=}\NormalTok{datetime.now()).first()} \ControlFlowTok{if}\NormalTok{ current\_token:} \NormalTok{ user }\OperatorTok{=}\NormalTok{ current\_token.user} \NormalTok{ current\_token.last\_used\_date }\OperatorTok{=}\NormalTok{ datetime.now()} \NormalTok{ current\_token.save()} \ControlFlowTok{return}\NormalTok{ user} \ControlFlowTok{return} \VariableTok{None} \end{Highlighting} \end{Shaded} \begin{itemize} \item Sous-entend qu'on a bien une classe qui permet d'accéder à ces jetons ;-) \end{itemize} \begin{Shaded} \begin{Highlighting}[] \ImportTok{from}\NormalTok{ django.contrib.auth }\ImportTok{import}\NormalTok{ backends, get\_user\_model} \ImportTok{from}\NormalTok{ ldap3 }\ImportTok{import}\NormalTok{ Server, Connection, ALL} \ImportTok{from}\NormalTok{ ldap3.core.exceptions }\ImportTok{import}\NormalTok{ LDAPPasswordIsMandatoryError} \ImportTok{from}\NormalTok{ config }\ImportTok{import}\NormalTok{ settings} \NormalTok{UserModel }\OperatorTok{=}\NormalTok{ get\_user\_model()} \KeywordTok{class}\NormalTok{ LdapBackend(backends.ModelBackend):} \CommentTok{"""Implémentation du backend LDAP pour la connexion des utilisateurs à l\textquotesingle{}Active Directory.} \CommentTok{ """} \KeywordTok{def}\NormalTok{ authenticate(}\VariableTok{self}\NormalTok{, request, username}\OperatorTok{=}\VariableTok{None}\NormalTok{, password}\OperatorTok{=}\VariableTok{None}\NormalTok{, }\OperatorTok{**}\NormalTok{kwargs):} \CommentTok{"""Authentifie l\textquotesingle{}utilisateur au travers du serveur LDAP.} \CommentTok{ """} \NormalTok{ ldap\_server }\OperatorTok{=}\NormalTok{ Server(settings.LDAP\_SERVER, get\_info}\OperatorTok{=}\NormalTok{ALL)} \NormalTok{ ldap\_connection }\OperatorTok{=}\NormalTok{ Connection(ldap\_server, user}\OperatorTok{=}\NormalTok{username, password}\OperatorTok{=}\NormalTok{password)} \ControlFlowTok{try}\NormalTok{:} \ControlFlowTok{if} \KeywordTok{not}\NormalTok{ ldap\_connection.bind():} \ControlFlowTok{raise} \PreprocessorTok{ValueError}\NormalTok{(}\StringTok{"Login ou mot de passe incorrect"}\NormalTok{)} \ControlFlowTok{except}\NormalTok{ (LDAPPasswordIsMandatoryError, }\PreprocessorTok{ValueError}\NormalTok{) }\ImportTok{as}\NormalTok{ ldap\_exception:} \ControlFlowTok{raise}\NormalTok{ ldap\_exception} \NormalTok{ user, \_ }\OperatorTok{=}\NormalTok{ UserModel.objects.get\_or\_create(username}\OperatorTok{=}\NormalTok{username)} \end{Highlighting} \end{Shaded} On peut résumer le mécanisme d'authentification de la manière suivante: \begin{itemize} \item Si vous voulez modifier les informations liées à un utilisateur, orientez-vous vers la modification du modèle. Comme nous le verrons ci-dessous, il existe trois manières de prendre ces modifications en compte. Voir également \href{https://docs.djangoproject.com/en/stable/topics/auth/customizing/}{ici}. \item Si vous souhaitez modifier la manière dont l'utilisateur se connecte, alors vous devrez modifier le \textbf{backend}. \end{itemize} \hypertarget{_modification_du_moduxe8le}{% \subsection{Modification du modèle}\label{_modification_du_moduxe8le}} Dans un premier temps, Django a besoin de manipuler \href{https://docs.djangoproject.com/en/1.9/ref/contrib/auth/\#user-model}{des instances de type \texttt{django.contrib.auth.User}}. Cette classe implémente les champs suivants: \begin{itemize} \item \texttt{username} \item \texttt{first\_name} \item \texttt{last\_name} \item \texttt{email} \item \texttt{password} \item \texttt{date\_joined}. \end{itemize} D'autres champs, comme les groupes auxquels l'utilisateur est associé, ses permissions, savoir s'il est un super-utilisateur, \ldots\hspace{0pt} sont moins pertinents pour le moment. Avec les quelques champs déjà définis ci-dessus, nous avons de quoi identifier correctement nos utilisateurs. Inutile d'implémenter nos propres classes, puisqu'elles existent déjà :-) Si vous souhaitez ajouter un champ, il existe trois manières de faire. \hypertarget{_extension_du_moduxe8le_existant}{% \subsection{Extension du modèle existant}\label{_extension_du_moduxe8le_existant}} Le plus simple consiste à créer une nouvelle classe, et à faire un lien de type \texttt{OneToOne} vers la classe \texttt{django.contrib.auth.User}. De cette manière, on ne modifie rien à la manière dont Django authentife ses utlisateurs: tout ce qu'on fait, c'est un lien vers une table nouvellement créée, comme on l'a déjà vu au point {[}\ldots\hspace{0pt}voir l'héritage de modèle{]}. L'avantage de cette méthode, c'est qu'elle est extrêmement flexible, et qu'on garde les mécanismes Django standard. Le désavantage, c'est que pour avoir toutes les informations de notre utilisateur, on sera obligé d'effectuer une jointure sur le base de données, ce qui pourrait avoir des conséquences sur les performances. \hypertarget{_substitution}{% \subsection{Substitution}\label{_substitution}} Avant de commencer, sachez que cette étape doit être effectuée \textbf{avant la première migration}. Le plus simple sera de définir une nouvelle classe héritant de \texttt{django.contrib.auth.User} et de spécifier la classe à utiliser dans votre fichier de paramètres. Si ce paramètre est modifié après que la première migration ait été effectuée, il ne sera pas pris en compte. Tenez-en compte au moment de modéliser votre application. \begin{Shaded} \begin{Highlighting}[] \NormalTok{AUTH\_USER\_MODEL }\OperatorTok{=} \StringTok{\textquotesingle{}myapp.MyUser\textquotesingle{}} \end{Highlighting} \end{Shaded} Notez bien qu'il ne faut pas spécifier le package \texttt{.models} dans cette injection de dépendances: le schéma à indiquer est bien \texttt{\textless{}nom\ de\ l’application\textgreater{}.\textless{}nom\ de\ la\ classe\textgreater{}}. \hypertarget{_backend}{% \subsubsection{Backend}\label{_backend}} \hypertarget{_templates}{% \subsubsection{Templates}\label{_templates}} Ce qui n'existe pas par contre, ce sont les vues. Django propose donc tout le mécanisme de gestion des utilisateurs, excepté le visuel (hors administration). En premier lieu, ces paramètres sont fixés dans le fichier `settings \textless{}\url{https://docs.djangoproject.com/en/1.8/ref/settings/\#auth\%3E\%60_}. On y trouve par exemple les paramètres suivants: \begin{itemize} \item \texttt{LOGIN\_REDIRECT\_URL}: si vous ne spécifiez pas le paramètre \texttt{next}, l'utilisateur sera automatiquement redirigé vers cette page. \item \texttt{LOGIN\_URL}: l'URL de connexion à utiliser. Par défaut, l'utilisateur doit se rendre sur la page \texttt{/accounts/login}. \end{itemize} \hypertarget{_social_authentification}{% \subsubsection{Social-Authentification}\label{_social_authentification}} Voir ici : \href{https://github.com/omab/python-social-auth}{python social auth} \hypertarget{_un_petit_mot_sur_oauth}{% \subsubsection{Un petit mot sur OAuth}\label{_un_petit_mot_sur_oauth}} OAuth est un standard libre définissant un ensemble de méthodes à implémenter pour l'accès (l'autorisation) à une API. Son fonctionnement se base sur un système de jetons (Tokens), attribués par le possesseur de la ressource à laquelle un utilisateur souhaite accéder. Le client initie la connexion en demandant un jeton au serveur. Ce jeton est ensuite utilisée tout au long de la connexion, pour accéder aux différentes ressources offertes par ce serveur. `wikipedia \textless{}\url{http://en.wikipedia.org/wiki/OAuth\%3E\%60_}. Une introduction à OAuth est \href{http://hueniverse.com/oauth/guide/intro/}{disponible ici}. Elle introduit le protocole comme étant une \texttt{valet\ key}, une clé que l'on donne à la personne qui va garer votre voiture pendant que vous profitez des mondanités. Cette clé donne un accès à votre voiture, tout en bloquant un ensemble de fonctionnalités. Le principe du protocole est semblable en ce sens: vous vous réservez un accès total à une API, tandis que le système de jetons permet d'identifier une personne, tout en lui donnant un accès restreint à votre application. L'utilisation de jetons permet notamment de définir une durée d'utilisation et une portée d'utilisation. L'utilisateur d'un service A peut par exemple autoriser un service B à accéder à des ressources qu'il possède, sans pour autant révéler son nom d'utilisateur ou son mot de passe. L'exemple repris au niveau du \href{http://hueniverse.com/oauth/guide/workflow/}{workflow} est le suivant : un utilisateur(trice), Jane, a uploadé des photos sur le site faji.com (A). Elle souhaite les imprimer au travers du site beppa.com (B). Au moment de la commande, le site beppa.com envoie une demande au site faji.com pour accéder aux ressources partagées par Jane. Pour cela, une nouvelle page s'ouvre pour l'utilisateur, et lui demande d'introduire sa "pièce d'identité". Le site A, ayant reçu une demande de B, mais certifiée par l'utilisateur, ouvre alors les ressources et lui permet d'y accéder. \begin{Shaded} \begin{Highlighting}[] \NormalTok{INSTALLED\_APPS }\OperatorTok{=}\NormalTok{ [} \StringTok{"django.contrib..."} \NormalTok{]} \end{Highlighting} \end{Shaded} peut être splitté en plusieurs parties: \begin{Shaded} \begin{Highlighting}[] \NormalTok{INSTALLED\_APPS }\OperatorTok{=}\NormalTok{ [} \NormalTok{]} \NormalTok{THIRD\_PARTIES }\OperatorTok{=}\NormalTok{ [} \NormalTok{]} \NormalTok{MY\_APPS }\OperatorTok{=}\NormalTok{ [} \hypertarget{_logging_2}{% \subsection{Logging}\label{_logging_2}} \hypertarget{_muxe9thode_de_duxe9ploiement}{% \section{Méthode de déploiement}\label{_muxe9thode_de_duxe9ploiement}} \hypertarget{_duxe9ploiement_sur_debian}{% \section{Déploiement sur Debian}\label{_duxe9ploiement_sur_debian}} \begin{Shaded} \begin{Highlighting}[] \ExtensionTok{apt}\NormalTok{ update} \ExtensionTok{groupadd}\NormalTok{ {-}{-}system webapps } \ExtensionTok{groupadd}\NormalTok{ {-}{-}system gunicorn\_sockets } \ExtensionTok{useradd}\NormalTok{ {-}{-}system {-}{-}gid webapps {-}{-}shell /bin/bash {-}{-}home /home/gwift gwift } \FunctionTok{mkdir}\NormalTok{ {-}p /home/gwift } \FunctionTok{chown}\NormalTok{ gwift:webapps /home/gwift } \end{Highlighting} \end{Shaded} \begin{itemize} \item On ajoute un groupe intitulé \texttt{webapps} \item On crée un groupe pour les communications via sockets \item On crée notre utilisateur applicatif; ses applications seront placées dans le répertoire \texttt{/home/gwift} \item On crée le répertoire home/gwift \item On donne les droits sur le répertoire /home/gwift \end{itemize} \hypertarget{_installation_des_duxe9pendances_systuxe8mes}{% \subsection{Installation des dépendances systèmes}\label{_installation_des_duxe9pendances_systuxe8mes}} La version 3.6 de Python se trouve dans les dépôts officiels de CentOS. Si vous souhaitez utiliser une version ultérieure, il suffit de l'installer en parallèle de la version officiellement supportée par votre distribution. Pour CentOS, vous avez donc deux possibilités : \begin{Shaded} \begin{Highlighting}[] \ExtensionTok{yum}\NormalTok{ install python36 {-}y} \end{Highlighting} \end{Shaded} Ou passer par une installation alternative: \begin{Shaded} \begin{Highlighting}[] \FunctionTok{sudo}\NormalTok{ yum {-}y groupinstall }\StringTok{"Development Tools"} \FunctionTok{sudo}\NormalTok{ yum {-}y install openssl{-}devel bzip2{-}devel libffi{-}devel} \FunctionTok{wget}\NormalTok{ https://www.python.org/ftp/python/3.8.2/Python{-}3.8.2.tgz} \BuiltInTok{cd}\NormalTok{ Python{-}3.8*/} \ExtensionTok{./configure}\NormalTok{ {-}{-}enable{-}optimizations} \FunctionTok{sudo}\NormalTok{ make altinstall } \end{Highlighting} \end{Shaded} \begin{itemize} \item \textbf{Attention !} Le paramètre \texttt{altinstall} est primordial. Sans lui, vous écraserez l'interpréteur initialement supporté par la distribution, et cela pourrait avoir des effets de bord non souhaités. \end{itemize} \hypertarget{_configuration_de_lapplication}{% \subsection{Configuration de l'application}\label{_configuration_de_lapplication}} \begin{Shaded} \begin{Highlighting}[] \VariableTok{SECRET\_KEY=}\OperatorTok{\textless{}}\KeywordTok{set} \ExtensionTok{your}\NormalTok{ secret key here}\OperatorTok{\textgreater{}} \VariableTok{ALLOWED\_HOSTS=}\ExtensionTok{*} \VariableTok{STATIC\_ROOT=}\NormalTok{/var/www/gwift/static} \VariableTok{DATABASE=} \end{Highlighting} \end{Shaded} \begin{itemize} \item La variable \texttt{SECRET\_KEY} est notamment utilisée pour le chiffrement des sessions. \item On fait confiance à django\_environ pour traduire la chaîne de connexion à la base de données. \end{itemize} \hypertarget{_cruxe9ation_des_ruxe9pertoires_de_logs}{% \subsection{Création des répertoires de logs}\label{_cruxe9ation_des_ruxe9pertoires_de_logs}} \begin{verbatim} mkdir -p /var/www/gwift/static \end{verbatim} \hypertarget{_cruxe9ation_du_ruxe9pertoire_pour_le_socket}{% \subsection{Création du répertoire pour le socket}\label{_cruxe9ation_du_ruxe9pertoire_pour_le_socket}} Dans le fichier \texttt{/etc/tmpfiles.d/gwift.conf}: \begin{verbatim} D /var/run/webapps 0775 gwift gunicorn_sockets - \end{verbatim} Suivi de la création par systemd : \begin{verbatim} systemd-tmpfiles --create \end{verbatim} \hypertarget{_gunicorn}{% \subsection{Gunicorn}\label{_gunicorn}} \begin{Shaded} \begin{Highlighting}[] \CommentTok{\#!/bin/bash} \CommentTok{\# defines settings for gunicorn} \VariableTok{NAME=}\StringTok{"gwift"} \VariableTok{DJANGODIR=}\NormalTok{/home/gwift/webapps/gwift} \VariableTok{SOCKFILE=}\NormalTok{/var/run/webapps/gunicorn\_gwift.sock} \VariableTok{USER=}\NormalTok{gwift} \VariableTok{GROUP=}\NormalTok{gunicorn\_sockets} \VariableTok{NUM\_WORKERS=}\NormalTok{5} \VariableTok{DJANGO\_SETTINGS\_MODULE=}\NormalTok{config.settings\_production} \VariableTok{DJANGO\_WSGI\_MODULE=}\NormalTok{config.wsgi} \BuiltInTok{echo} \StringTok{"Starting }\VariableTok{$NAME}\StringTok{ as }\KeywordTok{\textasciigrave{}}\FunctionTok{whoami}\KeywordTok{\textasciigrave{}}\StringTok{"} \BuiltInTok{source}\NormalTok{ /home/gwift/.venvs/gwift/bin/activate} \BuiltInTok{cd} \VariableTok{$DJANGODIR} \BuiltInTok{export} \VariableTok{DJANGO\_SETTINGS\_MODULE=$DJANGO\_SETTINGS\_MODULE} \BuiltInTok{export} \VariableTok{PYTHONPATH=$DJANGODIR}\NormalTok{:}\VariableTok{$PYTHONPATH} \BuiltInTok{exec}\NormalTok{ gunicorn }\VariableTok{$\{DJANGO\_WSGI\_MODULE\}}\NormalTok{:application }\KeywordTok{\textbackslash{}} \ExtensionTok{{-}{-}name} \VariableTok{$NAME} \KeywordTok{\textbackslash{}} \ExtensionTok{{-}{-}workers} \VariableTok{$NUM\_WORKERS} \KeywordTok{\textbackslash{}} \ExtensionTok{{-}{-}user} \VariableTok{$USER} \KeywordTok{\textbackslash{}} \ExtensionTok{{-}{-}bind}\NormalTok{=unix:}\VariableTok{$SOCKFILE} \KeywordTok{\textbackslash{}} \ExtensionTok{{-}{-}log{-}level}\NormalTok{=debug }\KeywordTok{\textbackslash{}} \ExtensionTok{{-}{-}log{-}file}\NormalTok{={-}} \end{Highlighting} \end{Shaded} \hypertarget{_supervision_keep_alive_et_autoreload}{% \subsection{Supervision, keep-alive et autoreload}\label{_supervision_keep_alive_et_autoreload}} Pour la supervision, on passe par Supervisor. Il existe d'autres superviseurs, \begin{Shaded} \begin{Highlighting}[] \ExtensionTok{yum}\NormalTok{ install supervisor {-}y} \end{Highlighting} \end{Shaded} On crée ensuite le fichier \texttt{/etc/supervisord.d/gwift.ini}: \begin{Shaded} \begin{Highlighting}[] \NormalTok{[}\ExtensionTok{program}\NormalTok{:gwift]} \VariableTok{command=}\NormalTok{/home/gwift/bin/start\_gunicorn.sh} \VariableTok{user=}\NormalTok{gwift} \VariableTok{stdout\_logfile=}\NormalTok{/var/log/gwift/gwift.log} \VariableTok{autostart=}\NormalTok{true} \VariableTok{autorestart=}\NormalTok{unexpected} \VariableTok{redirect\_stdout=}\NormalTok{true} \VariableTok{redirect\_stderr=}\NormalTok{true} \end{Highlighting} \end{Shaded} Et on crée les répertoires de logs, on démarre supervisord et on vérifie qu'il tourne correctement: \begin{Shaded} \begin{Highlighting}[] \FunctionTok{mkdir}\NormalTok{ /var/log/gwift} \FunctionTok{chown}\NormalTok{ gwift:nagios /var/log/gwift} \ExtensionTok{systemctl}\NormalTok{ enable supervisord} \ExtensionTok{systemctl}\NormalTok{ start supervisord.service} \ExtensionTok{systemctl}\NormalTok{ status supervisord.service} \NormalTok{● }\ExtensionTok{supervisord.service}\NormalTok{ {-} Process Monitoring and Control Daemon} \ExtensionTok{Loaded}\NormalTok{: loaded (/usr/lib/systemd/system/supervisord.service}\KeywordTok{;} \ExtensionTok{enabled}\KeywordTok{;} \ExtensionTok{vendor}\NormalTok{ preset: disabled)} \ExtensionTok{Active}\NormalTok{: active (running) }\ExtensionTok{since}\NormalTok{ Tue 2019{-}12{-}24 10:08:09 CET}\KeywordTok{;} \ExtensionTok{10s}\NormalTok{ ago} \ExtensionTok{Process}\NormalTok{: 2304 ExecStart=/usr/bin/supervisord {-}c /etc/supervisord.conf (code=exited, status=0/SUCCESS)} \ExtensionTok{Main}\NormalTok{ PID: 2310 (supervisord)} \ExtensionTok{CGroup}\NormalTok{: /system.slice/supervisord.service} \NormalTok{ ├─}\ExtensionTok{2310}\NormalTok{ /usr/bin/python /usr/bin/supervisord {-}c /etc/supervisord.conf} \NormalTok{ ├─}\ExtensionTok{2313}\NormalTok{ /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...} \NormalTok{ ├─}\ExtensionTok{2317}\NormalTok{ /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...} \NormalTok{ ├─}\ExtensionTok{2318}\NormalTok{ /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...} \NormalTok{ ├─}\ExtensionTok{2321}\NormalTok{ /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...} \NormalTok{ ├─}\ExtensionTok{2322}\NormalTok{ /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...} \NormalTok{ └─}\ExtensionTok{2323}\NormalTok{ /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...} \FunctionTok{ls}\NormalTok{ /var/run/webapps/} \end{Highlighting} \end{Shaded} On peut aussi vérifier que l'application est en train de tourner, à l'aide de la commande \texttt{supervisorctl}: \begin{Shaded} \begin{Highlighting}[] \VariableTok{$$}\NormalTok{$ }\ExtensionTok{supervisorctl}\NormalTok{ status gwift} \ExtensionTok{gwift}\NormalTok{ RUNNING pid 31983, uptime 0:01:00} \end{Highlighting} \end{Shaded} Et pour gérer le démarrage ou l'arrêt, on peut passer par les commandes suivantes: \begin{Shaded} \begin{Highlighting}[] \VariableTok{$$}\NormalTok{$ }\ExtensionTok{supervisorctl}\NormalTok{ stop gwift} \ExtensionTok{gwift}\NormalTok{: stopped} \ExtensionTok{root@ks3353535}\NormalTok{:/etc/supervisor/conf.d\# supervisorctl start gwift} \ExtensionTok{gwift}\NormalTok{: started} \ExtensionTok{root@ks3353535}\NormalTok{:/etc/supervisor/conf.d\# supervisorctl restart gwift} \ExtensionTok{gwift}\NormalTok{: stopped} \ExtensionTok{gwift}\NormalTok{: started} \end{Highlighting} \end{Shaded} \hypertarget{_configuration_du_firewall_et_ouverture_des_ports}{% \subsection{Configuration du firewall et ouverture des ports}\label{_configuration_du_firewall_et_ouverture_des_ports}} \begin{verbatim} et 443 (HTTPS). \end{verbatim} \begin{verbatim} firewall-cmd --permanent --zone=public --add-service=http firewall-cmd --permanent --zone=public --add-service=https firewall-cmd --reload \end{verbatim} \begin{itemize} \item On ouvre le port 80, uniquement pour autoriser une connexion HTTP, mais qui sera immédiatement redirigée vers HTTPS \item Et le port 443 (forcément). \end{itemize} \hypertarget{_installation_dnginx}{% \subsection{Installation d'Nginx}\label{_installation_dnginx}} \begin{verbatim} yum install nginx -y usermod -a -G gunicorn_sockets nginx \end{verbatim} On configure ensuite le fichier \texttt{/etc/nginx/conf.d/gwift.conf}: \begin{verbatim} upstream gwift_app { server unix:/var/run/webapps/gunicorn_gwift.sock fail_timeout=0; } server { listen 80; server_name ; root /var/www/gwift; error_log /var/log/nginx/gwift_error.log; access_log /var/log/nginx/gwift_access.log; client_max_body_size 4G; keepalive_timeout 5; gzip on; gzip_comp_level 7; gzip_proxied any; gzip_types gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/xml; location /static/ { access_log off; expires 30d; add_header Pragma public; add_header Cache-Control "public"; add_header Vary "Accept-Encoding"; try_files $uri $uri/ =404; } location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://gwift_app; } } \end{verbatim} \begin{itemize} \item Ce répertoire sera complété par la commande \texttt{collectstatic} que l'on verra plus tard. L'objectif est que les fichiers ne demandant aucune intelligence soit directement servis par Nginx. Cela évite d'avoir un processus Python (relativement lent) qui doive être instancié pour servir un simple fichier statique. \item Afin d'éviter que Django ne reçoive uniquement des requêtes provenant de 127.0.0.1 \end{itemize} \hypertarget{_mise_uxe0_jour}{% \subsection{Mise à jour}\label{_mise_uxe0_jour}} Script de mise à jour. \begin{Shaded} \begin{Highlighting}[] \FunctionTok{su}\NormalTok{ {-} }\OperatorTok{\textless{}}\NormalTok{user}\OperatorTok{\textgreater{}} \BuiltInTok{source}\NormalTok{ \textasciitilde{}/.venvs/}\OperatorTok{\textless{}}\NormalTok{app}\OperatorTok{\textgreater{}}\NormalTok{/bin/activate} \BuiltInTok{cd}\NormalTok{ \textasciitilde{}/webapps/}\OperatorTok{\textless{}}\NormalTok{app}\OperatorTok{\textgreater{}} \FunctionTok{git}\NormalTok{ fetch} \FunctionTok{git}\NormalTok{ checkout vX.Y.Z} \ExtensionTok{pip}\NormalTok{ install {-}U requirements/prod.txt} \ExtensionTok{python}\NormalTok{ manage.py migrate} \ExtensionTok{python}\NormalTok{ manage.py collectstatic} \BuiltInTok{kill}\NormalTok{ {-}HUP }\KeywordTok{\textasciigrave{}}\FunctionTok{ps}\NormalTok{ {-}C gunicorn fch {-}o pid }\KeywordTok{|} \FunctionTok{head}\NormalTok{ {-}n 1}\KeywordTok{\textasciigrave{}} \end{Highlighting} \end{Shaded} \begin{itemize} \item \url{https://stackoverflow.com/questions/26902930/how-do-i-restart-gunicorn-hup-i-dont-know-masterpid-or-location-of-pid-file} \end{itemize} \hypertarget{_configuration_des_sauvegardes}{% \subsection{Configuration des sauvegardes}\label{_configuration_des_sauvegardes}} Les sauvegardes ont été configurées avec borg: \texttt{yum\ install\ borgbackup}. C'est l'utilisateur gwift qui s'en occupe. \begin{verbatim} mkdir -p /home/gwift/borg-backups/ cd /home/gwift/borg-backups/ borg init gwift.borg -e=none borg create gwift.borg::{now} ~/bin ~/webapps \end{verbatim} Et dans le fichier crontab : \begin{verbatim} 0 23 * * * /home/gwift/bin/backup.sh \end{verbatim} \hypertarget{_rotation_des_jounaux}{% \subsection{Rotation des jounaux}\label{_rotation_des_jounaux}} \begin{Shaded} \begin{Highlighting}[] \ExtensionTok{/var/log/gwift/*}\NormalTok{ \{} \ExtensionTok{weekly} \ExtensionTok{rotate}\NormalTok{ 3} \FunctionTok{size}\NormalTok{ 10M} \ExtensionTok{compress} \ExtensionTok{delaycompress} \NormalTok{\}} \end{Highlighting} \end{Shaded} Puis on démarre logrotate avec \# logrotate -d /etc/logrotate.d/gwift pour vérifier que cela fonctionne correctement. \hypertarget{_ansible}{% \subsection{Ansible}\label{_ansible}} TODO \hypertarget{_duxe9ploiement_sur_heroku}{% \section{Déploiement sur Heroku}\label{_duxe9ploiement_sur_heroku}} \href{https://www.heroku.com}{Heroku} est une \emph{Plateform As A Service} paas, où vous choisissez le \emph{service} dont vous avez besoin (une base de données, un service de cache, un service applicatif, \ldots\hspace{0pt}), vous lui envoyer les paramètres nécessaires et le tout démarre gentiment sans que vous ne deviez superviser l'hôte. Ce mode démarrage ressemble énormément aux 12 facteurs dont nous avons déjà parlé plus tôt - raison de plus pour que notre application soit directement prête à y être déployée, d'autant plus qu'il ne sera pas possible de modifier un fichier une fois qu'elle aura démarré: si vous souhaitez modifier un paramètre, cela reviendra à couper l'actuelle et envoyer de nouveaux paramètres et recommencer le déploiement depuis le début. \begin{figure} \centering \includegraphics{images/deployment/heroku.png} \caption{Invest in apps, not ops. Heroku handles the hard stuff --- patching and upgrading, 24/7 ops and security, build systems, failovers, and more --- so your developers can stay focused on building great apps.} \end{figure} Pour un projet de type "hobby" et pour l'exemple de déploiement ci-dessous, il est tout à fait possible de s'en sortir sans dépenser un kopek, afin de tester nos quelques idées ou mettre rapidement un \emph{Most Valuable Product} en place. La seule contrainte consistera à pouvoir héberger des fichiers envoyés par vos utilisateurs - ceci pourra être fait en configurant un \emph{bucket compatible S3}, par exemple chez Amazon, Scaleway ou OVH. Le fonctionnement est relativement simple: pour chaque application, Heroku crée un dépôt Git qui lui est associé. Il suffit donc d'envoyer les sources de votre application vers ce dépôt pour qu'Heroku les interprête comme étant une nouvelle version, déploie les nouvelles fonctionnalités - sous réserve que tous les tests passent correctement - et les mettent à disposition. Dans un fonctionnement plutôt manuel, chaque déploiement est initialisé par le développeur ou par un membre de l'équipe. Dans une version plus automatisée, chacun de ces déploiements peut être placé en fin de \emph{pipeline}, lorsque tous les tests unitaires et d'intégration auront été réalisés. Au travers de la commande \texttt{heroku\ create}, vous associez donc une nouvelle référence à votre code source, comme le montre le contenu du fichier \texttt{.git/config} ci-dessous: \begin{Shaded} \begin{Highlighting}[] \NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ create} \ExtensionTok{Creating}\NormalTok{ app... done, ⬢ young{-}temple{-}86098} \ExtensionTok{https}\NormalTok{://young{-}temple{-}86098.herokuapp.com/ }\KeywordTok{|} \ExtensionTok{https}\NormalTok{://git.heroku.com/young{-}temple{-}86098.git} \NormalTok{$ }\FunctionTok{cat}\NormalTok{ .git/config} \NormalTok{[}\ExtensionTok{core}\NormalTok{]} \ExtensionTok{repositoryformatversion}\NormalTok{ = 0} \ExtensionTok{filemode}\NormalTok{ = false} \ExtensionTok{bare}\NormalTok{ = false} \ExtensionTok{logallrefupdates}\NormalTok{ = true} \ExtensionTok{symlinks}\NormalTok{ = false} \ExtensionTok{ignorecase}\NormalTok{ = true} \NormalTok{[}\ExtensionTok{remote} \StringTok{"heroku"}\NormalTok{]} \ExtensionTok{url}\NormalTok{ = https://git.heroku.com/still{-}thicket{-}66406.git} \ExtensionTok{fetch}\NormalTok{ = +refs/heads/*:refs/remotes/heroku/*} \end{Highlighting} \end{Shaded} IMPORTANT: \begin{verbatim} Pour définir de quel type d'application il s'agit, Heroku nécessite un minimum de configuration. Celle-ci se limite aux deux fichiers suivants: * Déclarer un fichier `Procfile` qui va simplement décrire le fichier à passer au protocole WSGI * Déclarer un fichier `requirements.txt` (qui va éventuellement chercher ses propres dépendances dans un sous-répertoire, avec l'option `-r`) \end{verbatim} Après ce paramétrage, il suffit de pousser les changements vers ce nouveau dépôt grâce à la commande \texttt{git\ push\ heroku\ master}. Heroku propose des espaces de déploiements, mais pas d'espace de stockage. Il est possible d'y envoyer des fichiers utilisateurs (typiquement, des media personnalisés), mais ceux-ci seront perdus lors du redémarrage du container. Il est donc primordial de configurer correctement l'hébergement des fichiers média, de préférences sur un stockage compatible S3. s3 Prêt à vous lancer ? Commencez par créer un compte: \url{https://signup.heroku.com/python}. \hypertarget{_configuration_du_compte_heroku}{% \subsection{Configuration du compte Heroku}\label{_configuration_du_compte_heroku}} + Récupération des valeurs d'environnement pour les réutiliser ci-dessous. Vous aurez peut-être besoin d'un coup de pouce pour démarrer votre première application; heureusement, la documentation est super bien faite: \begin{figure} \centering \includegraphics{images/deployment/heroku-new-app.png} \caption{Heroku: Commencer à travailler avec un langage} \end{figure} Installez ensuite la CLI (\emph{Command Line Interface}) en suivant \href{https://devcenter.heroku.com/articles/heroku-cli}{la documentation suivante}. Au besoin, cette CLI existe pour: \begin{enumerate} \def\labelenumi{\arabic{enumi}.} \item macOS, \emph{via} `brew ` \item Windows, grâce à un \href{https://cli-assets.heroku.com/heroku-x64.exe}{binaire x64} (la version 32 bits existe aussi, mais il est peu probable que vous en ayez besoin) \item GNU/Linux, via un script Shell \texttt{curl\ https://cli-assets.heroku.com/install.sh\ \textbar{}\ sh} ou sur \href{https://snapcraft.io/heroku}{SnapCraft}. \end{enumerate} Une fois installée, connectez-vous: \begin{Shaded} \begin{Highlighting}[] \NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ login} \end{Highlighting} \end{Shaded} Et créer votre application: \begin{Shaded} \begin{Highlighting}[] \NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ create} \ExtensionTok{Creating}\NormalTok{ app... done, ⬢ young{-}temple{-}86098} \ExtensionTok{https}\NormalTok{://young{-}temple{-}86098.herokuapp.com/ }\KeywordTok{|} \ExtensionTok{https}\NormalTok{://git.heroku.com/young{-}temple{-}86098.git} \end{Highlighting} \end{Shaded} \begin{figure} \centering \includegraphics{images/deployment/heroku-app-created.png} \caption{Notre application est à présent configurée!} \end{figure} Ajoutons lui une base de données, que nous sauvegarderons à intervalle régulier: \begin{Shaded} \begin{Highlighting}[] \NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ addons:create heroku{-}postgresql:hobby{-}dev} \ExtensionTok{Creating}\NormalTok{ heroku{-}postgresql:hobby{-}dev on ⬢ still{-}thicket{-}66406... free} \ExtensionTok{Database}\NormalTok{ has been created and is available} \NormalTok{ ! }\ExtensionTok{This}\NormalTok{ database is empty. If upgrading, you can transfer} \NormalTok{ ! }\ExtensionTok{data}\NormalTok{ from another database with pg:copy} \ExtensionTok{Created}\NormalTok{ postgresql{-}clear{-}39693 as DATABASE\_URL} \ExtensionTok{Use}\NormalTok{ heroku addons:docs heroku{-}postgresql to view documentation} \NormalTok{$ }\ExtensionTok{heroku}\NormalTok{ pg:backups schedule {-}{-}at }\StringTok{\textquotesingle{}14:00 Europe/Brussels\textquotesingle{}}\NormalTok{ DATABASE\_URL} \ExtensionTok{Scheduling}\NormalTok{ automatic daily backups of postgresql{-}clear{-}39693 at 14:00 Europe/Brussels... done} \end{Highlighting} \end{Shaded} TODO: voir comment récupérer le backup de la db :-p \begin{Shaded} \begin{Highlighting}[] \CommentTok{\# Copié/collé de https://cookiecutter{-}django.readthedocs.io/en/latest/deployment{-}on{-}heroku.html} \ExtensionTok{heroku}\NormalTok{ create {-}{-}buildpack https://github.com/heroku/heroku{-}buildpack{-}python} \ExtensionTok{heroku}\NormalTok{ addons:create heroku{-}redis:hobby{-}dev} \ExtensionTok{heroku}\NormalTok{ addons:create mailgun:starter} \ExtensionTok{heroku}\NormalTok{ config:set PYTHONHASHSEED=random} \ExtensionTok{heroku}\NormalTok{ config:set WEB\_CONCURRENCY=4} \ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_DEBUG=False} \ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_SETTINGS\_MODULE=config.settings.production} \ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_SECRET\_KEY=}\StringTok{"}\VariableTok{$(}\ExtensionTok{openssl}\NormalTok{ rand {-}base64 64}\VariableTok{)}\StringTok{"} \CommentTok{\# Generating a 32 character{-}long random string without any of the visually similar characters "IOl01":} \ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_ADMIN\_URL=}\StringTok{"}\VariableTok{$(}\ExtensionTok{openssl}\NormalTok{ rand {-}base64 4096 }\KeywordTok{|} \FunctionTok{tr}\NormalTok{ {-}dc }\StringTok{\textquotesingle{}A{-}HJ{-}NP{-}Za{-}km{-}z2{-}9\textquotesingle{}} \KeywordTok{|} \FunctionTok{head}\NormalTok{ {-}c 32}\VariableTok{)}\StringTok{/"} \CommentTok{\# Set this to your Heroku app url, e.g. \textquotesingle{}bionic{-}beaver{-}28392.herokuapp.com\textquotesingle{}} \ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_ALLOWED\_HOSTS=} \CommentTok{\# Assign with AWS\_ACCESS\_KEY\_ID} \ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_AWS\_ACCESS\_KEY\_ID=} \CommentTok{\# Assign with AWS\_SECRET\_ACCESS\_KEY} \ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_AWS\_SECRET\_ACCESS\_KEY=} \CommentTok{\# Assign with AWS\_STORAGE\_BUCKET\_NAME} \ExtensionTok{heroku}\NormalTok{ config:set DJANGO\_AWS\_STORAGE\_BUCKET\_NAME=} \FunctionTok{git}\NormalTok{ push heroku master} \ExtensionTok{heroku}\NormalTok{ run python manage.py createsuperuser} \ExtensionTok{heroku}\NormalTok{ run python manage.py check {-}{-}deploy} \ExtensionTok{heroku}\NormalTok{ open} \end{Highlighting} \end{Shaded} \hypertarget{_configuration}{% \subsection{Configuration}\label{_configuration}} Pour qu'Heroku comprenne le type d'application à démarrer, ainsi que les commandes à exécuter pour que tout fonctionne correctement. Pour un projet Django, cela comprend, à placer à la racine de votre projet: \begin{enumerate} \def\labelenumi{\arabic{enumi}.} \item Un fichier \texttt{requirements.txt} (qui peut éventuellement faire appel à un autre fichier, \textbf{via} l'argument \texttt{-r}) \item Un fichier \texttt{Procfile} ({[}sans extension{]}(\url{https://devcenter.heroku.com/articles/procfile)}!), qui expliquera la commande pour le protocole WSGI. \end{enumerate} Dans notre exemple: \begin{verbatim} # requirements.txt django==3.2.8 gunicorn boto3 django-storages \end{verbatim} \begin{Shaded} \begin{Highlighting}[] \CommentTok{\# Procfile} \ExtensionTok{release}\NormalTok{: python3 manage.py migrate} \ExtensionTok{web}\NormalTok{: gunicorn gwift.wsgi} \end{Highlighting} \end{Shaded} \hypertarget{_huxe9bergement_s3}{% \subsection{Hébergement S3}\label{_huxe9bergement_s3}} Pour cette partie, nous allons nous baser sur l'\href{https://www.scaleway.com/en/object-storage/}{Object Storage de Scaleway}. Ils offrent 75GB de stockage et de transfert par mois, ce qui va nous laisser suffisament d'espace pour jouer un peu 😉. \includegraphics{images/deployment/scaleway-object-storage-bucket.png} L'idée est qu'au moment de la construction des fichiers statiques, Django aille simplement les héberger sur un espace de stockage compatible S3. La complexité va être de configurer correctement les différents points de terminaison. Pour héberger nos fichiers sur notre \textbf{bucket} S3, il va falloir suivre et appliquer quelques étapes dans l'ordre: \begin{enumerate} \def\labelenumi{\arabic{enumi}.} \item Configurer un bucket compatible S3 - je parlais de Scaleway, mais il y en a - \textbf{littéralement} - des dizaines. \item Ajouter la librairie \texttt{boto3}, qui s'occupera de "parler" avec ce type de protocole \item Ajouter la librairie \texttt{django-storage}, qui va elle s'occuper de faire le câblage entre le fournisseur (\textbf{via} \texttt{boto3}) et Django, qui s'attend à ce qu'on lui donne un moteur de gestion \textbf{via} la clé {[}\texttt{DJANGO\_STATICFILES\_STORAGE}{]}(\url{https://docs.djangoproject.com/en/3.2/ref/settings/\#std:setting-STATICFILES_STORAGE}). \end{enumerate} La première étape consiste à se rendre dans {[}la console Scaleway{]}(\url{https://console.scaleway.com/project/credentials}), pour gérer ses identifiants et créer un jeton. \includegraphics{images/deployment/scaleway-api-key.png} Selon la documentation de \href{https://django-storages.readthedocs.io/en/latest/backends/amazon-S3.html\#settings}{django-storages}, de \href{https://boto3.amazonaws.com/v1/documentation/api/latest/index.html}{boto3} et de \href{https://www.scaleway.com/en/docs/tutorials/deploy-saas-application/}{Scaleway}, vous aurez besoin des clés suivantes au niveau du fichier \texttt{settings.py}: \begin{Shaded} \begin{Highlighting}[] \NormalTok{AWS\_ACCESS\_KEY\_ID }\OperatorTok{=}\NormalTok{ os.getenv(}\StringTok{\textquotesingle{}ACCESS\_KEY\_ID\textquotesingle{}}\NormalTok{)} \NormalTok{AWS\_SECRET\_ACCESS\_KEY }\OperatorTok{=}\NormalTok{ os.getenv(}\StringTok{\textquotesingle{}SECRET\_ACCESS\_KEY\textquotesingle{}}\NormalTok{)} \NormalTok{AWS\_STORAGE\_BUCKET\_NAME }\OperatorTok{=}\NormalTok{ os.getenv(}\StringTok{\textquotesingle{}AWS\_STORAGE\_BUCKET\_NAME\textquotesingle{}}\NormalTok{)} \NormalTok{AWS\_S3\_REGION\_NAME }\OperatorTok{=}\NormalTok{ os.getenv(}\StringTok{\textquotesingle{}AWS\_S3\_REGION\_NAME\textquotesingle{}}\NormalTok{)} \NormalTok{AWS\_DEFAULT\_ACL }\OperatorTok{=} \StringTok{\textquotesingle{}public{-}read\textquotesingle{}} \NormalTok{AWS\_LOCATION }\OperatorTok{=} \StringTok{\textquotesingle{}static\textquotesingle{}} \NormalTok{AWS\_S3\_SIGNATURE\_VERSION }\OperatorTok{=} \StringTok{\textquotesingle{}s3v4\textquotesingle{}} \NormalTok{AWS\_S3\_HOST }\OperatorTok{=} \StringTok{\textquotesingle{}s3.}\SpecialCharTok{\%s}\StringTok{.scw.cloud\textquotesingle{}} \OperatorTok{\%}\NormalTok{ (AWS\_S3\_REGION\_NAME,)} \NormalTok{AWS\_S3\_ENDPOINT\_URL }\OperatorTok{=} \StringTok{\textquotesingle{}https://}\SpecialCharTok{\%s}\StringTok{\textquotesingle{}} \OperatorTok{\%}\NormalTok{ (AWS\_S3\_HOST, )} \NormalTok{DEFAULT\_FILE\_STORAGE }\OperatorTok{=} \StringTok{\textquotesingle{}storages.backends.s3boto3.S3Boto3Storage\textquotesingle{}} \NormalTok{STATICFILES\_STORAGE }\OperatorTok{=} \StringTok{\textquotesingle{}storages.backends.s3boto3.S3ManifestStaticStorage\textquotesingle{}} \NormalTok{STATIC\_URL }\OperatorTok{=} \StringTok{\textquotesingle{}}\SpecialCharTok{\%s}\StringTok{/}\SpecialCharTok{\%s}\StringTok{/\textquotesingle{}} \OperatorTok{\%}\NormalTok{ (AWS\_S3\_ENDPOINT\_URL, AWS\_LOCATION)} \CommentTok{\# General optimization for faster delivery} \NormalTok{AWS\_IS\_GZIPPED }\OperatorTok{=} \VariableTok{True} \NormalTok{AWS\_S3\_OBJECT\_PARAMETERS }\OperatorTok{=}\NormalTok{ \{} \StringTok{\textquotesingle{}CacheControl\textquotesingle{}}\NormalTok{: }\StringTok{\textquotesingle{}max{-}age=86400\textquotesingle{}}\NormalTok{,} \NormalTok{\}} \end{Highlighting} \end{Shaded} Configurez-les dans la console d'administration d'Heroku: \includegraphics{images/deployment/heroku-vars-reveal.png} Lors de la publication, vous devriez à présent avoir la sortie suivante, qui sera confirmée par le \textbf{bucket}: \begin{Shaded} \begin{Highlighting}[] \ExtensionTok{remote}\NormalTok{: {-}{-}{-}{-}{-}}\OperatorTok{\textgreater{}}\NormalTok{ $ python manage.py collectstatic {-}{-}noinput} \ExtensionTok{remote}\NormalTok{: 128 static files copied, 156 post{-}processed.} \end{Highlighting} \end{Shaded} \includegraphics{images/deployment/gwift-cloud-s3.png} Sources complémentaires: \begin{itemize} \item {[}How to store Django static and media files on S3 in production{]}(\url{https://coderbook.com/@marcus/how-to-store-django-static-and-media-files-on-s3-in-production/}) \item {[}Using Django and Boto3{]}(\url{https://www.simplecto.com/using-django-and-boto3-with-scaleway-object-storage/}) \end{itemize} \hypertarget{_autres_outils}{% \section{Autres outils}\label{_autres_outils}} \hypertarget{_arborescences}{% \subsection{Arborescences}\label{_arborescences}} \begin{Shaded} \begin{Highlighting}[] \end{Highlighting} \end{Shaded} \begin{Shaded} \begin{Highlighting}[] \CommentTok{\# \textless{}app\textgreater{}/management/commands/rebuild.py} \CommentTok{"""This command manages Closure Tables implementation} \CommentTok{It adds new levels and cleans links between entities.} \CommentTok{This way, it\textquotesingle{}s relatively easy to fetch an entire tree with just one tiny request.} \CommentTok{"""} \ImportTok{from}\NormalTok{ django.core.management.base }\ImportTok{import}\NormalTok{ BaseCommand} \ImportTok{from}\NormalTok{ rps.structure.models }\ImportTok{import}\NormalTok{ Entity, EntityTreePath} \KeywordTok{class}\NormalTok{ Command(BaseCommand):} \KeywordTok{def}\NormalTok{ handle(}\VariableTok{self}\NormalTok{, }\OperatorTok{*}\NormalTok{args, }\OperatorTok{**}\NormalTok{options):} \NormalTok{ entities }\OperatorTok{=}\NormalTok{ Entity.objects.}\BuiltInTok{all}\NormalTok{()} \ControlFlowTok{for}\NormalTok{ entity }\KeywordTok{in}\NormalTok{ entities:} \NormalTok{ breadcrumb }\OperatorTok{=}\NormalTok{ [node }\ControlFlowTok{for}\NormalTok{ node }\KeywordTok{in}\NormalTok{ entity.breadcrumb()]} \NormalTok{ tree }\OperatorTok{=} \BuiltInTok{set}\NormalTok{(EntityTreePath.objects.}\BuiltInTok{filter}\NormalTok{(descendant}\OperatorTok{=}\NormalTok{entity))} \ControlFlowTok{for}\NormalTok{ idx, node }\KeywordTok{in} \BuiltInTok{enumerate}\NormalTok{(breadcrumb):} \NormalTok{ tree\_path, \_ }\OperatorTok{=}\NormalTok{ EntityTreePath.objects.get\_or\_create(} \NormalTok{ ancestor}\OperatorTok{=}\NormalTok{node, descendant}\OperatorTok{=}\NormalTok{entity, weight}\OperatorTok{=}\NormalTok{idx }\OperatorTok{+} \DecValTok{1} \NormalTok{ )} \ControlFlowTok{if}\NormalTok{ tree\_path }\KeywordTok{in}\NormalTok{ tree:} \NormalTok{ tree.remove(tree\_path)} \ControlFlowTok{for}\NormalTok{ tree\_path }\KeywordTok{in}\NormalTok{ tree:} \NormalTok{ tree\_path.delete()} \end{Highlighting} \end{Shaded} Don't make me think, or why I switched from JS SPAs to Ruby On Rails \url{https://news.ycombinator.com/item?id=30206989\&utm_term=comment}