1132 lines
46 KiB
TeX
1132 lines
46 KiB
TeX
|
||
|
||
|
||
\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 <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}
|