Rework deployment
continuous-integration/drone/push Build is passing Details

This commit is contained in:
Fred Pauchet 2020-11-24 21:40:34 +01:00
parent 3c5055a46e
commit cf1a7f2c1a
4 changed files with 126 additions and 85211 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,70 +1,86 @@
= Déploiement
On va déjà parler de déploiement. Le serveur que django met à notre disposition est prévu uniquement pour le développement: inutile de passer par du code Python pour charger des fichiers statiques (feuilles de style, fichiers JavaScript, images, ...). De même, la base de donnée doit supporter plus qu'un seul utilisateur: SQLite fonctionne très bien dès lors qu'on se limite à un seul utilisateur... Sur une application Web, il est plus que probable que vous rencontriez rapidement des erreurs de base de données verrouillée pour écriture par un autre processus. Il est donc plus que bénéfique de passer sur quelque chose de plus solide. https://docs.djangoproject.com/fr/3.0/howto/deployment/[Déploiement].
On va déjà parler de déploiement.
Le serveur que django met à notre disposition _via_ la commande `runserver` est prévu uniquement pour le développement: en production, il est inutile de passer par du code Python pour charger des fichiers statiques (feuilles de style, fichiers JavaScript, images, ...).
De même, la base de donnée doit être capable de supporter plusieurs utilisateurs et connexions simultanément: SQLite fonctionne très bien dès lors qu'on se limite à un seul utilisateur... Mais sur une application Web, il est plus que probable que vous rencontriez rapidement des erreurs de verrou parce qu'un autre processus a déjà pris la main pour écrire ses données.
Il est donc plus que bénéfique de passer sur quelque chose de plus solide.
Si vous avez suivi les étapes jusqu'ici, vous devriez à peine disposer d'un espace de travail proprement configuré, d'un modèle relativement basique et d'une configuration avec une base de données simpliste. En bref, vous avez quelque chose qui fonctionne, mais qui ressemble de très loin à ce que vous souhaitez au final.
Si vous avez suivi les étapes jusqu'ici, vous devriez disposer d'un espace de travail proprement configuré, d'un modèle relativement basique et d'une configuration avec une base de données type SQLite.
En bref, vous avez quelque chose qui fonctionne, mais qui ressemble de très loin à ce dont vous aurez besoin au final.
Il y a une raison très simple à aborder le déploiement dès maintenant: à trop attendre et à peaufiner son développement en local, on en oublie que sa finalité sera de se retrouver exposé sur un serveur. On risque d'avoir oublié une partie des désidérata, d'avoir zappé une fonctionnalité essentielle ou simplement de passer énormément de temps à adapter les sources pour qu'elles fonctionnent sur un environnement en particulier.
Il y a une raison très simple à aborder le déploiement dès maintenant: à trop attendre et à peaufiner son développement en local, on en oublie que sa finalité sera de se retrouver exposé sur un serveur.
Il est du coup probable d'oublier une partie des désidérata, de zapper une fonctionnalité essentielle ou simplement de passer énormément de temps à adapter les sources pour qu'elles fonctionnent sur un environnement en particulier.
Aborder le déploiement maintenant permet également de rédiger dès le début les procédures d'installation, de mise à jour et de sauvegardes. Déploier une nouvelle version sera aussi simple que de récupérer la dernière archive depuis le dépôt, la placer dans le bon répertoire, appliquer des actions spécifiques (et souvent identiques entre deux versions), puis redémarrer les services adéquats.
Aborder le déploiement dès le début permet également de rédiger dès le début les procédures d'installation, de mises à jour et de sauvegardes.
Déploier une nouvelle version sera aussi simple que de récupérer la dernière archive depuis le dépôt, la placer dans le bon répertoire, appliquer des actions spécifiques (et souvent identiques entre deux versions), puis redémarrer les services adéquats, et la procédure complète se résumera à quelques lignes d'un script bash.
Dans cette partie, on abordera les points suivants:
Dans cette partie, nous aborderons les points suivants:
* La définition de l'infrastructure nécessaire à notre application
* La configuration de l'hôte, qui hébergera l'application: dans une machine physique, virtuelle ou dans un container. On abordera aussi rapidement les déploiements via Ansible, Chef, Puppet ou Salt.
* Les différentes méthodes de supervision de l'application: comment analyser les fichiers de logs et comment intercepter correctement une erreur si elle se présente et comment remonter l'information.
* La définition de l'infrastructure nécessaire à notre application,
* La configuration de l'hôte, qui hébergera l'application: dans une machine physique, virtuelle ou dans un container. Nous aborderons aussi les déploiements via Ansible et Salt.
* Les différentes méthodes de supervision de l'application: comment analyser les fichiers de logs, comment intercepter correctement une erreur si elle se présente et comment remonter l'information.
* Une partie sur la sécurité et la sécurisation de l'hôte.
Si on schématise l'infrastructure et le chemin parcouru par une éventuelle requête, on devrait arriver à quelque chose de synthéthique:
Si on schématise l'infrastructure et le chemin parcouru par une éventuelle requête, nous devrions arriver à quelque chose de synthéthique:
* Au niveau de l'infrastructure,
. l'utilisateur fait une requête via son navigateur (Firefox ou Chrome)
* Au niveau de l'infrastructure,
. l'utilisateur fait une requête via son navigateur (Firefox ou Chrome)
. le navigateur envoie une requête http, sa version, un verbe (GET, POST, ...), un port et éventuellement du contenu
. le firewall du serveur (Debian GNU/Linux, CentOS, ...) vérifie si la requête peut être prise en compte
. le firewall du serveur (Debian GNU/Linux, CentOS, ...) vérifie si la requête peut être prise en compte
. la requête est transmise à l'application qui écoute sur le port (probablement 80 ou 443; et _a priori_ Nginx)
. elle est ensuite transmise par socket et est prise en compte par Gunicorn
. qui la transmet ensuite à l'un de ses _workers_ (= un processus Python)
. après exécution, une réponse est renvoyée à l'utilisateur.
. après exécution, une réponse est renvoyée à l'utilisateur.
image::images/diagrams/architecture.png[]
* Au niveau logiciel (la partie mise en subrillance ci-dessus), la requête arrive dans les mains du processus Python, qui doit encore
. effectuer le routage des données,
. trouver la bonne fonction à exécuter,
. récupérer les données depuis la base de données,
. effectuer le rendu ou la conversion des données,
* Au niveau logiciel (la partie mise en subrillance ci-dessus), la requête arrive dans les mains du processus Python, qui doit encore
. effectuer le routage des données,
. trouver la bonne fonction à exécuter,
. récupérer les données depuis la base de données,
. effectuer le rendu ou la conversion des données,
. et renvoyer une réponse à l'utilisateur.
image::images/diagrams/django-process.png[]
Comme nous l'avons vu dans la première partie, Django est un framework complet, intégrant tous les mécanismes nécessaires à la bonne évolution d'une application.
Il est possible de démarrer petit, et de suivre l'évolution des besoins en fonction de la charge estimée ou ressentie, d'ajouter un mécanisme de mise en cache, des logiciels de suivi, ...
== Définition de l'infrastructure
Comme on l'a vu dans la première partie, Django est un framework complet, intégrant tous les mécanismes nécessaires à la bonne évolution d'une application. On peut ainsi commencer petit, et suivre l'évolution des besoins en fonction de la charge estimée ou ressentie, ajouter un mécanisme de mise en cache, des logiciels de suivi, ...
Pour une mise ne production, le standard *de facto* est le suivant:
Pour une mise ne production, le standard _de facto_ est le suivant:
* Nginx comme reverse proxy
* Gunicorn ou Uvicorn comme serveur d'application
* Supervisorctl pour le monitoring
* PostgreSQL ou MariaDB comme base de données.
* Celery et RabbitMQ pour l'exécution de tâches asynchrones
* Redis / Memcache pour la mise à en cache (et pour les sessions ? A vérifier).
En mode _containers_, on passera plutôt par Docker et Traefik.
C'est celle-ci que nous allons décrire ci-dessous.
Nous allons détailler ci-dessous trois méthodes de déploiement:
=== Configuration et sécurisation de la machine hôte
* Sur une machine hôte, en embarquant tous les composants sur un même serveur. Ce ne sera pas idéal, puisqu'il ne sera pas possible de configurer un _load balancer_, de routeur plusieurs basées de données, mais ce sera le premier cas de figure.
* Dans des containers, avec Docker-Compose.
* Sur une *Plateforme en tant que Service* (ou plus simplement, *PaaS*), pour faire abstraction de toute la couche de configuration du serveur.
Supervisor, nginx, gunicorn, utilisateurs, groupes, ...
== Sur une machine hôte
[plantuml]
--
entity Nginx
entity "Gunicorn (sockets/HTTP)" as gunicorn
database PGSQL
--
La première étape pour la configuration de notre hôte consiste à définir les utilisateurs et groupes de droits. Il est faut absolument éviter de faire tourner une application en tant qu'utilisateur *root*, car la moindre faille pourrait avoir des conséquences catastrophiques.
Aussi : Docker, Heroku, Digital Ocean, Scaleway, OVH, Ansible, Puppet, Chef, ... Bref, sur Debian et CentOS pour avoir un panel assez large. On oublie Windows: rien que Gunicorn et Nginx n'y tournent pas.
Une fois que ces utilisateurs seront configurés, nous pourrons passer à l'étape de configuration, qui consistera à:
1. Déployer les sources
2. Démarrer un serveur implémentant une interface WSGI (**Web Server Gateway Interface**), qui sera chargé de créer autant de [.line-through]#petits lutins# travailleurs que nous le désirerons.
3. Démarrer un superviseur, qui se chargera de veiller à la bonne santé de nos petits travailleurs, et en créer de nouveaux s'il le juge nécessaire
4. Configurer un proxy inverse, qui s'occupera d'envoyer les requêtes d'un utilisateur externe à la machine hôte vers notre serveur applicatif, qui la communiquera à l'un des travailleurs.
La machine hôte peut être louée chez Digital Ocean, Scaleway, OVH, Vultr, ... Il existe des dizaines d'hébergements typés VPS (**Virtual Private Server**). A vous de choisir celui qui vous convient footnote:[Personnellement, j'ai un petit faible pour Hetzner Cloud].
include::centos+debian.adoc[]
== Via Ansible & Salt
== Sur Heroku
== Docker-Compose
=== Mise à jour
@ -76,7 +92,7 @@ Script de mise à jour.
su - <user>
source ~/.venvs/<app>/bin/activate
cd ~/webapps/<app>
git fetch
git fetch
git checkout vX.Y.Z
pip install -U requirements/prod.txt
python manage.py migrate
@ -99,8 +115,7 @@ include::infrastructure.adoc[]
include::database.adoc[]
include::centos+debian.adoc[]
== Ressources
https://zestedesavoir.com/tutoriels/2213/deployer-une-application-django-en-production/
* https://zestedesavoir.com/tutoriels/2213/deployer-une-application-django-en-production/
* https://docs.djangoproject.com/fr/3.0/howto/deployment/[Déploiement].

View File

@ -1,4 +1,4 @@
== Déploiement sur CentOS
=== Déploiement sur CentOS
[source,bash]
----
@ -16,17 +16,17 @@ chown gwift:webapps /home/gwift <5>
<5> On pourrait sans doute fusioner les étapes 3, 4 et 5.
=== Installation des dépendances systèmes
==== Installation des dépendances systèmes
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 :
Pour CentOS, vous avez donc deux possibilités :
[source,bash]
----
yum install python36 -y
# CentOS 7 ne dispose que de la version 3.7 d'SQLite. On a besoin d'une version 3.8 au minimum:
wget https://kojipkgs.fedoraproject.org//packages/sqlite/3.8.11/1.fc21/x86_64/sqlite-devel-3.8.11-1.fc21.x86_64.rpm
wget https://kojipkgs.fedoraproject.org//packages/sqlite/3.8.11/1.fc21/x86_64/sqlite-3.8.11-1.fc21.x86_64.rpm
@ -47,7 +47,7 @@ sudo make altinstall <1>
----
<1> *Attention !* Le paramètre `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.
=== Préparation de l'environnement utilisateur
==== Préparation de l'environnement utilisateur
[source,bash]
----
@ -64,14 +64,14 @@ cd /home/gwift/webapps
git clone ...
----
La clé SSH doit ensuite être renseignée au niveau du dépôt, afin de pouvoir y accéder.
La clé SSH doit ensuite être renseignée au niveau du dépôt, afin de pouvoir y accéder.
A ce stade, on devrait déjà avoir quelque chose de fonctionnel en démarrant les commandes suivantes:
[source,bash]
----
# en tant qu'utilisateur 'gwift'
source .venvs/gwift/bin/activate
pip install -U pip
pip install -r requirements/base.txt
@ -80,7 +80,7 @@ cd webapps/gwift
gunicorn config.wsgi:application --bind localhost:3000 --settings=config.settings_production
----
=== Configuration de l'application
==== Configuration de l'application
[source,bash]
----
@ -92,14 +92,14 @@ DATABASE= <2>
<1> La variable `SECRET_KEY` est notamment utilisée pour le chiffrement des sessions.
<2> On fait confiance à django_environ pour traduire la chaîne de connexion à la base de données.
=== Création des répertoires de logs
==== Création des répertoires de logs
[source,text]
----
mkdir -p /var/www/gwift/static
----
=== Création du répertoire pour le socket
==== Création du répertoire pour le socket
Dans le fichier `/etc/tmpfiles.d/gwift.conf`:
@ -115,12 +115,12 @@ Suivi de la création par systemd :
systemd-tmpfiles --create
----
=== Gunicorn
==== Gunicorn
[source,bash]
----
#!/bin/bash
# defines settings for gunicorn
NAME="gwift"
DJANGODIR=/home/gwift/webapps/gwift
@ -130,14 +130,14 @@ GROUP=gunicorn_sockets
NUM_WORKERS=5
DJANGO_SETTINGS_MODULE=config.settings_production
DJANGO_WSGI_MODULE=config.wsgi
echo "Starting $NAME as `whoami`"
source /home/gwift/.venvs/gwift/bin/activate
cd $DJANGODIR
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
export PYTHONPATH=$DJANGODIR:$PYTHONPATH
exec gunicorn ${DJANGO_WSGI_MODULE}:application \
--name $NAME \
--workers $NUM_WORKERS \
@ -147,9 +147,9 @@ exec gunicorn ${DJANGO_WSGI_MODULE}:application \
--log-file=-
----
=== Supervision, keep-alive et autoreload
==== Supervision, keep-alive et autoreload
Pour la supervision, on passe par Supervisor. Il existe d'autres superviseurs,
Pour la supervision, on passe par Supervisor. Il existe d'autres superviseurs,
[source,bash]
----
@ -218,7 +218,7 @@ gwift: started
----
=== Ouverture des ports
==== Ouverture des ports
et 443 (HTTPS).
@ -231,7 +231,7 @@ firewall-cmd --reload
<1> On ouvre le port 80, uniquement pour autoriser une connexion HTTP, mais qui sera immédiatement redirigée vers HTTPS
<2> Et le port 443 (forcément).
=== Installation d'Nginx
==== Installation d'Nginx
[source]
----
@ -252,16 +252,16 @@ server {
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/ { <2>
access_log off;
expires 30d;
@ -270,12 +270,12 @@ server {
add_header Vary "Accept-Encoding";
try_files $uri $uri/ =404;
}
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; <3>
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://gwift_app;
}
}
@ -283,7 +283,30 @@ server {
<2> Ce répertoire sera complété par la commande `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.
<3> Afin d'éviter que Django ne reçoive uniquement des requêtes provenant de 127.0.0.1
=== Configuration des sauvegardes
=== Déploiement sur Debian
==== Installation des dépendances systèmes
==== Préparation de l'environnement utilisateur
==== Configuration de l'application
==== Création des répertoires de logs
==== Création du répertoire pour le socket
==== Gunicorn
==== Supervision, keep-alive et autoreload
==== Ouverture des ports
==== Installation d'Nginx
== Configuration des sauvegardes
Les sauvegardes ont été configurées avec borg: `yum install borgbackup`.
@ -303,17 +326,17 @@ Et dans le fichier crontab :
----
=== Rotation des jounaux
== Rotation des jounaux
[source,bash]
----
/var/log/gwift/* {
weekly
rotate 3
size 10M
compress
delaycompress
}
weekly
rotate 3
size 10M
compress
delaycompress
}
----
Puis on démarre logrotate avec # logrotate -d /etc/logrotate.d/gwift pour vérifier que cela fonctionne correctement.
Puis on démarre logrotate avec # logrotate -d /etc/logrotate.d/gwift pour vérifier que cela fonctionne correctement.

View File

@ -1,12 +1,12 @@
== Bases de données
On l'a déjà vu, Django se base sur un pattern type https://www.martinfowler.com/eaaCatalog/activeRecord.html[ActiveRecords] pour la gestion de la persistance des données et supporte les principaux moteurs de bases de données connus:
On l'a déjà vu, Django se base sur un pattern type https://www.martinfowler.com/eaaCatalog/activeRecord.html[ActiveRecords] pour la gestion de la persistance des données et supporte les principaux moteurs de bases de données connus:
* SQLite (en natif, mais Django 3.0 exige une version du moteur supérieure ou égale à la 3.8)
* MariaDB (en natif depuis Django 3.0),
* PostgreSQL au travers de psycopg2 (en natif aussi),
* Microsoft SQLServer grâce aux drivers [...à compléter]
* Oracle via https://oracle.github.io/python-cx_Oracle/[cx_Oracle].
* MariaDB (en natif depuis Django 3.0),
* PostgreSQL au travers de psycopg2 (en natif aussi),
* Microsoft SQLServer grâce aux drivers [...à compléter]
* Oracle via https://oracle.github.io/python-cx_Oracle/[cx_Oracle].
CAUTION: Chaque pilote doit être utilisé précautionneusement ! Chaque version de Django n'est pas toujours compatible avec chacune des versions des pilotes, et chaque moteur de base de données nécessite parfois une version spécifique du pilote. Par ce fait, vous serez parfois bloqué sur une version de Django, simplement parce que votre serveur de base de données se trouvera dans une version spécifique (eg. Django 2.3 à cause d'un Oracle 12.1).
@ -22,26 +22,26 @@ Par exemple, dans le cas de debian, on exécute la commande suivante:
----
$$$ aptitude install postgresql postgresql-contrib
----
Ensuite, on crée un utilisateur pour la DB:
[source,bash]
----
$$$ su - postgres
postgres@gwift:~$ createuser --interactive -P
Enter name of role to add: gwift_user
Enter password for new role:
Enter it again:
Enter name of role to add: gwift_user
Enter password for new role:
Enter it again:
Shall the new role be a superuser? (y/n) n
Shall the new role be allowed to create databases? (y/n) n
Shall the new role be allowed to create more new roles? (y/n) n
postgres@gwift:~$
----
Finalement, on peut créer la DB:
[source,bash]
----
----
postgres@gwift:~$ createdb --owner gwift_user gwift
postgres@gwift:~$ exit
logout