6  On-premise

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:

Avantages : Un VPS ne coûte que quelques euros par mois et permet d’héberger un service accessible à plusieurs utilisateurs simultanément.

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:

Le processus de configuration à :

Note

La machine hôte peut être louée chez https://www.digitalocean.com/[Digital Ocean], https://www.scaleway.com/fr/dedibox-vps/[Scaleway], https://www.ovhcloud.com/fr/vps/[OVH], https://www.vultr.com/[Vultr], … Il existe des dizaines d’hébergements typés VPS (Virtual Private Server). A vous de choisir celui qui vous convient.

Personnellement, j’ai un petit faible pour Hetzner Cloud, notamment pour les fonctionnalités qu’ils proposent au niveau du roulement des sauvegardes, de l’ergonomie générale de leur solution et de la sécurité d’accès à la console grâce à du MFA.

6.1 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.

6.1.1 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 update
apt 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 :

Warning

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.

apt install openssl-devel bzip2-devel libffi-devel
wget https://www.python.org/ftp/python/3.8.2/Python-3.8.2.tgz
cd Python-3.8*/
./configure --enable-optimizations
make altinstall

6.1.2 Base 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 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 https://github.com/microsoft/mssql-django[Microsoft],
  • Oracle via https://oracle.github.io/python-cx_Oracle/[cx_Oracle].

Chaque pilote doit être utilisé précautionneusement ! Toutes les versions de Django ne sont pas compatibles avec tous les 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 … (Consider SQLite 2021)).

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 - postgres
postgres@gwift:~$ createuser --interactive -P
Enter name of role to add: gwift_user
Enter password for new role:
Enter it again:
Shall the new role be a superuser? (y/n) n
Shall the new role be allowed to create databases? (y/n) n
Shall the new role be allowed to create more new roles? (y/n) n

Finalement, nous pouvons effectivemment créer la base de données qui hébergera les données:

postgres@gwift:~$ createdb --owner gwift_user gwift
postgres@gwift:~$ exit
logout

6.2 Préparation de l’environnement utilisateur

6.2.1 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.

Warning

Faire en sorte que chaque application tourne dans son propre espace de privilèges est un principe de sécurité fondamental. Il évite qu’on ne se retrouve avec une application démarrée en tant que root - la moindre faille pourrait avoir des conséquences catastrophiques.

Si votre application comprenait une faille qui permettrait (par exemple) de supprimer un fichier lambda dont le chemin serait passé en paramètre, le fait d’exécuter notre application sous son propre utilisateur empêche au moins que des fichiers hors de son périmètre ne soit supprimés - ce qui ne serait pas le cas si l’application tournait en tant que root.

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 https://fr.wikipedia.org/wiki/Berkeley_sockets[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 webapps
groupadd --system gunicorn_sockets
useradd --system\
    --gid webapps\
    --shell /bin/bash\
    --home /home/$APP_NAME\
    $APP_NAME
mkdir -p /home/$APP_NAME
chown $APP_NAME:webapps /home/$APP_NAME
mkdir -p /var/www/$APP_NAME/static

6.2.2 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_NAME
mkdir {.venvs,bin,webapps}
python3 -m venv .venvs/$APP_NAME
source .venvs/$APP_NAME/bin/activate
cd /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

6.2.3 Préparation des fichiers media

Cette section concerne la gestion des fichiers uploadés par les utilisateurs, comme des images de profil, des documents, etc.

mkdir -p /var/www/gwift/media
chown -R gwift:webapps /var/www/gwift/media
chmod 755 /var/www/gwift/media

Ajoute ensuite cette variable dans .env :

MEDIA_ROOT=/var/www/gwift/media
MEDIA_URL=/media/

Dans urls.py de Django, pense à ajouter :

from django.conf import settings
from django.conf.urls.static import static

urlpatterns = [
    ...
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Et dans Nginx, ajoute dans le bloc server :

location /media/ {
    root /var/www/gwift/;
    try_files $uri $uri/ =404;
}

6.2.4 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/static
DATABASE=... (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.

6.2.5 Création des répertoires de logs

Créer un dossier pour les journaux applicatifs :

mkdir -p /var/log/gwift
chown gwift:webapps /var/log/gwift
chmod 750 /var/log/gwift

Tu peux configurer Gunicorn ou Django pour y écrire leurs logs :

Dans settings.py de Django :

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'file': {
            'level': 'DEBUG',
            'class': 'logging.FileHandler',
            'filename': '/var/log/gwift/django.log',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'DEBUG',
            'propagate': True,
        },
    },
}

6.2.6 Gunicorn

#!/bin/bash

APPNAME="jarvis"

DJANGODIR=/home/$APPNAME/webapps/$APPNAME
SOCKFILE=/run/webapps/gunicorn_$APPNAME.sock
USER=$APPNAME
GROUP=gunicorn_sockets
NUM_WORKERS=3
DJANGO_SETTINGS_MODULE=config.settings
DJANGO_WSGI_MODULE=config.wsgi

echo "Starting $APPNAME as `whoami`"

source /home/$APPNAME/.venvs/$APPNAME/bin/activate

cd $DJANGODIR
export DJANGO_SETTINGS_MODULE=$DJANGO_SETTINGS_MODULE
export PYTHONPATH=$DJANGODIR:$PYTHONPATH
exec 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.

6.3 Composants périphériques

6.3.1 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 (https://www.gentoo.org[Gentoo], https://www.devuan.org/[Devuan], les https://en.wikipedia.org/wiki/Comparison_of_BSD_operating_systems[*BSD], …),
  • Soit passer par http://supervisord.orgSupervisord.
Note

Comme indiqué, il existe d’autres superviseurs, comme https://circus.readthedocs.io/en/latest/[Circus] ou https://uwsgi-docs.readthedocs.io/en/latest/[uWSGI]. Ils fonctionnent tous un peu de la même manière. Choisissez celui qui vous convient le mieux.

6.3.1.1 Systemd

# /etc/systemd/system/gunicorn.socket

[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 daemon
Requires=gunicorn.socket
After=network.target

[Service]
PIDFile=/run/gunicorn/pid
User=
Group=webapps
RuntimeDirectory=gunicorn
WorkingDirectory=/home/jarvis/webapps/jarvis
ExecStart=/home/jarvis/bin/jarvis.sh
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID
PrivateTmp=true

[Install]
WantedBy=multi-user.target

6.3.1.2 Supervisord

apt install supervisor -y

On crée ensuite le fichier /etc/supervisord.d/gwift.ini:

[program:gwift]
command=/home/gwift/bin/start_gunicorn.sh
user=gwift
stdout_logfile=/var/log/gwift/gwift.log
autostart=true
autorestart=unexpected
redirect_stdout=true
redirect_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

6.3.2 Firewall

Debian et Ubuntu utilise ufw comme firewall. Pour autoriser les connexions en http/https, démarrez les commandes suivantes :

systemctl status ufw
ufw allow "Nginx HTTP"
ufw allow "Nginx HTTPS"

Assurez-vous ensuite que tout est en ordre, avec ufw app list.

6.3.3 Reverse proxy

apt install nginx -y
usermod -a -G gunicorn_sockets nginx

On configure ensuite le fichier +/etc/nginx/conf.d/gwift.conf+ :

apt install nginx -y
usermod -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

6.3.4 Let’s Encrypt

Certificats externes + communication par socket interne.

6.4 Mise à jour

u - <user>
source ~/.venvs/<app>/bin/activate
cd ~/webapps/<app>
git fetch
git checkout vX.Y.Z
pip install -U requirements/prod.txt
python manage.py migrate
python manage.py collectstatic
kill -HUP `ps -C gunicorn fch -o pid | head -n 1`
  • https://stackoverflow.com/questions/26902930/how-do-i-restart-gunicorn-hup-i-dont-know-masterpid-or-location-of-pid-file

6.4.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.

6.5 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=none
borg create gwift.borg::{now} ~/bin ~/webapps

Et dans le fichier crontab :

0 23 * * * /home/gwift/bin/backup.sh

6.6 Check list

.

6.7 Ansible

On peut aussi passer par fabric, ansible, chef ou puppet.

6.8 Conclusions

Le déploiement on-premise sur Debian ou Ubuntu offre une maîtrise totale de l’environnement et une grande souplesse dans la configuration. En contrepartie, il requiert une rigueur opérationnelle et une bonne compréhension des différents composants de l’infrastructure.

En procédant par étapes — de l’initialisation du système jusqu’à la mise en place du reverse proxy — vous bâtissez un socle solide pour l’hébergement de vos applications. Ce type de déploiement est particulièrement adapté aux environnements maîtrisés, aux infrastructures internes, ou lorsqu’une solution cloud n’est pas envisageable pour des raisons de sécurité, de budget ou de politique d’entreprise.

Gardez à l’esprit que ce type d’architecture nécessite un suivi régulier : application des correctifs de sécurité, supervision du système, gestion des sauvegardes. En contrepartie, vous gagnez en transparence, en indépendance, et en compréhension profonde de vos applications.

Comme souvent en informatique, il n’existe pas une seule bonne solution : le meilleur choix reste celui qui répond à vos contraintes tout en étant compréhensible et maintenable sur le long terme.