On-premise (Debian/Ubuntu)
Le déploiement sur Debian est présenté dans le cadre d’un déploiment on-premises ou IaaS. Nous verrons trois types de déploiements:
- Manuel, en exécutant chaque étape une par une. Ce type de déploiement est intéressant à réaliser au moins une fois, afin de comprendre les différents composants utilisés.
- Semi-automatisé, via Ansible. Cette manière se base sur ce qui aura été présenté à la première section, tout en faciliant certains aspects. Plusieurs concepts devront cependant être abordés au préalable, ce qui pourra paraitre rebutant en première instance.
- Automatisé en container, qui reste un déploiement on-premise, mais qui prépare déjà le chemin pour les containeurs. Ce mode de déploiement-ci garde quelques contraintes, mais présente également quelques avantages.
Avantages : Un VPS ne coûte que quelques euros par mois et permet de mettre un projet à disposition de plusieurs utilisateurs
Désavantages : Nécessite du temps et de l’investissement, notamment pour suivre les évolutions, appliquer les mises à jour de sécurité, sécuriser son infrastructure.
Le choix de ces deux systèmes d’exploitation s’explique par une grande liberté, une bonne compatibilité avec différentes architectures, une bonne stabilité générale et une documentation suffisante:
- Debian fonctionne sur un mécanisme de canaux: stable, testing et unstable. Globalement, en choisissant le premier canal, vous aurez des paquets très stables et des procédures de sauts de versions correctement documentées.
- Ubuntu fonctionne grâce à des versions Long-Term Support , supportées durant cinq ans et sortant tous les deux ans. En pratique, en installant une version 22.04, celle-ci sera supportée jusqu’en avril 2027, ce qui vous laissera le temps de planifier le downtime. Pour un serveur en production, évitez les versions non-LTS, qui exigent une mise à jour tous les 9 mois maximum.
Le processus de configuration à :
- Initialiser le système : configuration SSHd, firewall, installation d’un moteur de base de données, …
- Configurer l’environnement de l’application
- Démarrer un service implémentant une interface WSGI (Web Server Gateway Interface) , qui sera chargé de créer autant de petits lutins travailleurs que nous le désirerons (Gunicorn),
- 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 (Supervisord),
- Configurer un reverse proxy, 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, pour exécution (_Nginx, Apache, …).
Initialisation du serveur
Nous allons commencer par initialiser le système, configurer les droits utilisateurs, installer une version de l’interpréteur Python et configurer les dépendances principales.
Dépendances systèmes
Debian et Ubuntu comprennent nativement une version récente de Python 3. Mettez vos dépôts à jour et installez la avec les commandes suivantes :
apt updateapt install python3
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, soit en utilisant pyenv (comme nous l’avons déjà vu), soit de la manière suivante :
apt install openssl-devel bzip2-devel libffi-develwget https://www.python.org/ftp/python/3.8.2/Python-3.8.2.tgzcd Python-3.8*/./configure --enable-optimizationsmake altinstall
Base de données
On l’a déjà vu, Django se base sur un pattern type ActiveRecords pour la gestion de la persistance des données et supporte les principaux moteurs de bases de données relationnelles connus :
- SQLite (en natif),
- MariaDB / MySQL (en natif depuis Django 3.0),
- PostgreSQL au travers de
psycopg2
(en natif aussi), - Microsoft SQLServer grâce aux drivers Microsoft,
- Oracle via cx_Oracle.
Chaque pilote doit être utilisé précautionneusement ! Chaque version de Django n’est pas toujours compatible avec chacune des versions des pilotes, et chaque moteur de base de données nécessite parfois une version spécifique du pilote. Par ce fait, vous serez parfois bloqué sur une version de Django, simplement parce que votre serveur de base de données se trouvera dans une version spécifique (eg. Django 2.3 à cause d’un Oracle 12.1).
Ci-dessous, nous détaillerons l’installation d’une base PostgreSQL sur notre serveur, afin de disposer de notre propre instance. A quelques commandes près, cette procédure peut être réutilisée telle quelle pour un autre moteur. Les autres moteurs présentent soit des problèmes liées au coût des licences (Oracle, MSSQL), soit de limitations dues au nombre d’utilisateurs concurrents (SQLite - bien que … cite:[consider_sqlite]).
apt install postgresql postgresql-contrib
Ensuite, nous créerons un utilisateur pour la base de données de notre application. De la même manière que pour l’utilisateur système, il n’est pas acceptable que la chaine de connexion au moteur de base de données soient associées à un compte administrateur :
# su - postgrespostgres@gwift:~$ createuser --interactive -PEnter name of role to add: gwift_userEnter password for new role:Enter it again:Shall the new role be a superuser? (y/n) nShall the new role be allowed to create databases? (y/n) nShall the new role be allowed to create more new roles? (y/n) n
Finalement, nous pouvons effectivemment créer la base de données qui hébergera les données:
postgres@gwift:~$ createdb --owner gwift_user gwiftpostgres@gwift:~$ exitlogout
Préparation de l’environnement utilisateur
Configuration des droits utilisateurs
La toute première étape pour la configuration de notre hôte consiste à définir les utilisateurs et groupes de droits.
Dans l’ordre, nous devons réaliser les étapes suivantes :
- Ajouter un nouveau groupe système, intitulé
webapps
, qui servira à partager des fichiers entre les différents composants. L’utilisateur qui fait tourner le proxy inverse sera également ajouté à ce groupe, un peu plus tard. - Ajouter un groupe qui servira à gérer la communications via sockets, qui consiste en un ensemble normalisé de fonctions de communication entre processus,
- Créer un utilisateur application, afin de le conserver isolé du reste du système,
- Les applications seront placées dans le répertoire
/home/gwift
, - Octroi des droits de notre utilisateur
+gwift+
sur son propre répertoire/home/gwift
.
Pour résumer, l’ensemble de ces commandes nous donne ceci:
#!/bin/bash
APP_NAME='<app_name>'groupadd --system webappsgroupadd --system gunicorn_socketsuseradd --system\ --gid webapps\ --shell /bin/bash\ --home /home/$APP_NAME\ $APP_NAMEmkdir -p /home/$APP_NAMEchown $APP_NAME:webapps /home/$APP_NAMEmkdir -p /var/www/$APP_NAME/static
Préparation du socket de communication
Dans le fichier /etc/tmpfiles.d/gwift.conf
:
D /run/webapps 0775 <app_name> gunicorn_sockets -
Suivi de la création par systemd :
systemd-tmpfiles --create
su - $APP_NAMEmkdir {.venvs,bin,webapps}python3 -m venv .venvs/$APP_NAMEsource .venvs/$APP_NAME/bin/activatecd /home/$APP_NAME/webapps
git clone <git_repository>
pip install -r requirements.txt
pip install gunicorn
cd webapps/$APP_NAME
gunicorn config.wsgi --bind localhost:3000
A ce stade, vous devriez déjà avoir une application qui tourne sur le port :3000
de votre serveur :
[INFO] Starting gunicorn 23.0.0[INFO] Listening at: http://127.0.0.1:8000 (4141198)[INFO] Using worker: sync[INFO] Booting worker with pid: 4141199
Préparation des fichiers media
TBC
Configuration de l’application
Comme nous l’avons vu dans les 12 facteurs, la configuration de l’environnement est réalisée à partir d’un fichier .env
que nous placerons dans le répertoire utilisateur.
Ce fichier .env
contiendra a minima les clés de configuration suivante :
SECRET_KEY=<set your secret key here> <1>ALLOWED_HOSTS=*STATIC_ROOT=/var/www/gwift/staticDATABASE=... <2>
Utilisée pour le chiffrement des sessions,
Nous ferons confiance à django_environ
pour traduire la chaîne de connexion à la base de données.
Création des répertoires de logs
TBC
Gunicorn
#!/bin/bash
APPNAME="jarvis"
DJANGODIR=/home/$APPNAME/webapps/$APPNAMESOCKFILE=/run/webapps/gunicorn_$APPNAME.sockUSER=$APPNAMEGROUP=gunicorn_socketsNUM_WORKERS=3DJANGO_SETTINGS_MODULE=config.settingsDJANGO_WSGI_MODULE=config.wsgi
echo "Starting $APPNAME as `whoami`"
source /home/$APPNAME/.venvs/$APPNAME/bin/activate
cd $DJANGODIRexport DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULEexport PYTHONPATH=$DJANGODIR:$PYTHONPATHexec gunicorn ${DJANGO_WSGI_MODULE}:application \ --name $APPNAME \ --workers $NUM_WORKERS \ --user $USER \ --bind=unix:$SOCKFILE \ --log-level=debug \ --log-file=-
Ce paramètre correspond généralement au nombre de CPUs présents sur la machine, auquel nous ajoutons 1.
Composants périphériques
Supervsion, keepalive et autoreload
Pour la supervision, nous avons deux choix principaux - bien qu’il existe d’autres solutions, que nous ne détaillerons pas ici - :
- Soit utiliser SystemD, qui est présent sur la majorité des distributions du marché - sauf exceptions (Gentoo, Devuan, les *BSD, …),
- Soit passer par Supervisord.
Systemd
[Unit]Description=<app_name> gunicorn socket
[Socket]ListenStream=/run/webapps/gunicorn_jarvis.socket
[Install]WantedBy=sockets.target
Ce paramètre pourra être réutilisé dans le démarrage de notre worker
# /etc/systemd/system/<app_name>.service
[Unit]Description=<app_name> gunicorn daemonRequires=gunicorn.socketAfter=network.target
[Service]PIDFile=/run/gunicorn/pidUser=Group=webappsRuntimeDirectory=gunicornWorkingDirectory=/home/jarvis/webapps/jarvisExecStart=/home/jarvis/bin/jarvis.shExecReload=/bin/kill -s HUP $MAINPIDExecStop=/bin/kill -s TERM $MAINPIDPrivateTmp=true
[Install]WantedBy=multi-user.target
Supervisord
apt install supervisor -y
On crée ensuite le fichier /etc/supervisord.d/gwift.ini
:
[program:gwift]command=/home/gwift/bin/start_gunicorn.shuser=gwiftstdout_logfile=/var/log/gwift/gwift.logautostart=trueautorestart=unexpectedredirect_stdout=trueredirect_stderr=true
Et on crée les répertoires de logs, on démarre supervisord et on vérifie qu’il tourne correctement :
$ mkdir /var/log/gwift$ chown gwift:nagios /var/log/gwift$ systemctl enable supervisord$ systemctl start supervisord.service$ systemctl status supervisord.service supervisord.service - Process Monitoring and Control Daemon Loaded: loaded (/usr/lib/systemd/system/supervisord.service; enabled; vendor preset: disabled) Active: active (running) since Tue 2019-12-24 10:08:09 CET; 10s ago Process: 2304 ExecStart=/usr/bin/supervisord -c /etc/supervisord.conf (code =exited, status=0/SUCCESS) Main PID: 2310 (supervisord) CGroup: /system.slice/supervisord.service - 2310 /usr/bin/python /usr/bin/supervisord -c /etc/supervisord.conf - 2313 /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:... - 2317 /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:... - 2318 /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:... - 2321 /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:... - 2322 /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:... - 2323 /home/gwift/.venvs/gwift/bin/python3 /home/gwift/.venvs/gwift/bin/gunicorn config.wsgi:...ls /var/run/webapps
On peut aussi vérifier que l’application est en train de tourner, à l’aide de la commande supervisorctl
:
$ supervisorctl status gwift gwift RUNNING pid 31983, uptime 0:01:00 supervisorctl stop gwift gwift: stopped root@ks3353535:/etc/supervisor/conf.d# supervisorctl start gwift gwift: started root@ks3353535:/etc/supervisor/conf.d# supervisorctl restart gwift gwift: stopped gwift: started
Firewall
Debian et Ubuntu utilise ufw
comme firewall.
Pour autoriser les connexions en http/https, démarrez les commandes suivantes :
systemctl status ufwufw allow "Nginx HTTP"ufw allow "Nginx HTTPS"
Assurez-vous ensuite que tout est en ordre, avec ufw app list
.
Reverse proxy
apt install nginx -yusermod -a -G gunicorn_sockets nginx
On configure ensuite le fichier +/etc/nginx/conf.d/gwift.conf+
:
apt install nginx -yusermod -a -G gunicorn_sockets nginx
On configure ensuite le fichier +/etc/nginx/conf.d/gwift.conf+
:
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;
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; } }}
- 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. - Afin d’éviter que Django ne reçoive uniquement des requêtes provenant de 127.0.0.1
Let’s Encrypt
Certificats externes + communication par socket interne.
Mise à jour
u - <user>source ~/.venvs/<app>/bin/activatecd ~/webapps/<app>git fetchgit checkout vX.Y.Zpip install -U requirements/prod.txtpython manage.py migratepython manage.py collectstatickill -HUP `ps -C gunicorn fch -o pid | head -n 1`
Logrotate
/var/log/gwift/* { 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.
Sauvegardes
Les sauvegardes ont été configurées avec borg :
+yum install borgbackup+
.
C’est l’utilisateur gwift qui s’en occupe.
mkdir -p /home/gwift/borg-backups/cd /home/gwift/borg-backups/borg init gwift.borg -e=noneborg create gwift.borg::{now} ~/bin ~/webapps
Et dans le fichier crontab :
0 23 * * * /home/gwift/bin/backup.sh
Check list
.
Ansible
On peut aussi passer par fabric, ansible, chef ou puppet.
Conclusions
Ce type de déploiement est complexe lors d’une première fois, mais est relativement rapide par la suite. Comptez une heure ou deux lorsque vous aurez pris l’habitude. Il reste cependant assez rébarbatif, même après en avoir pris l’habitude : il faut penser à beaucoup de points spécifiques (configuration des droits, paramétrage des répertoires, accès croisés entre le reverse proxy et le côté applicatif, s’assurer d’avoir une procédure de recouvrement des données en cas de plantage, …).
Nous pouvons aussi constater le manque de résilience : si le serveur venait à planter pour n’importe quelle raison, l’application serait rendue totalement inaccessible. De même, si vous veniez à devoir migrer votre serveur vers une machine plus puissante ou offrant plus d’espace disque, vous seriez contraint à devoir migrer l’ensemble des fichiers, y compris les média uploadés par vos utilisateurs.
En conclusion, cette manière de faire est correcte. Elle présente quelques avantages (tout est sur la machine, accessible et sans dépendances), mais aussi plusieurs désavantages principalement liés à la résilience.