\chapter{Debian/Ubuntu}

\includegraphics{images/logos/debian-ubuntu.png}

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:

\begin{enumerate}
    \item
        \textbf{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.
    \item
        \textbf{Semi-automatisé}, \textit{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.
    \item
        \textbf{Automatisé}, qui reste un déploiement \textit{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.
\end{enumerate}

Par soucis de simplification, quelques raccourcis seront pris:

\begin{itemize}
    \item
        La base de données sera hébergée sur la même machine que l'application
    \item
        Il n'y aura pas de \textit{load balancers}
    \item
        \ldots
\end{itemize}

\begin{tabular}{ll}
    Avantages &
        \textbf{Une grande accessibilité}: un VPS ne coûte que quelques euros par mois et permet de mettre un projet à disposition de plusieurs utilisateurs \\
    \hline
    Désavantages & Nécessite du temps et de l'investissement personnel \\
    & Nécessite de suivre les évolutions et de prendre une petite journée par an pour appliquer les dernières mises à jour \\
    & Nécessite de sécuriser soi-même son infrastructure (clés SSH, Fail2ban, \ldots) \\
    & Le montage final ressemble un peu à de l'artisanat (bien qu'Ansible "professionalise" sérieusement le tout) \\
    \hline
\end{tabular}

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:

\begin{itemize}
    \item
        Debian fonctionne sur un mécanisme de \href{https://www.debian.org/releases/}{canaux}: stable, testing et unstable.
        Globalement, en choisissant le premier canal, vous aurez des paquets \textit{très} stables et des procédures de sauts de versions correctement documentées.
    \item
        Ubuntu fonctionne grâce à des versions \textit{Long-Term Support} \index{LTS}, 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 \textit{downtime}.
        Entre chacun de ces versions LTS, des versions intermédiaires sont mises à dispositions tous les six mois, mais ne sont supportées que durant 9 mois.
        Pour cette raison, leur utilisation sur un serveur est fortement déconseillée.
\end{itemize}

Le processus de configuration à :

\begin{enumerate}
    \item
        Initialiser le système
    \item
        Déployer ou mettre les sources à disposition
    \item
        Démarrer un service implémentant une interface WSGI (\textbf{Web Server Gateway Interface}) \index{WSGI}, qui sera chargé de créer autant de petits lutins travailleurs que nous le désirerons (\textit{Gunicorn})
    \item
        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 (\textit{Supervisord})
    \item
        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, pour exécution (\textit{Nginx, Apache, \ldots}).
\end{enumerate}

La machine hôte peut être louée chez Digital Ocean, Scaleway, OVH, Vultr, \ldots
Il existe des dizaines d'hébergements typés VPS (\textbf{Virtual Private Server}).
A vous de choisir celui qui vous convient \footnote{Personnellement, j'ai un petit faible pour Hetzner Cloud}.

\section{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.

\subsection{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.
Il est faut absolument éviter de faire tourner une application en tant qu'utilisateur \textbf{root}, car la moindre faille pourrait avoir des conséquences catastrophiques.

Dans l'ordre, nous devons réaliser les étapes suivantes :

\begin{itemize}
    \item
        Ajouter un nouveau groupe système, intitulé \texttt{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.
    \item
        Ajouter un groupe qui servira à gérer la communications \textit{via sockets}, qui consiste en un ensemble normalisé de fonctions de communication entre processus \footnote{\url{https://fr.wikipedia.org/wiki/Berkeley_sockets}}
    \item
        Créer un utilisateur application, afin de le conserver isolé du reste du système.
        Ceci est un principe de sécurité fondamental: si votre application comprend une faille qui permettrait (par exemple) de supprimer un fichier $\lambda$ dont le chemin serait passé en paramètre passer un chemin de fichier 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.
    \item
        Les applications seront placées dans le répertoire \texttt{/home/gwift}
    \item
        Octroi des droits de notre utilisateur \texttt{gwift} sur son propre répertoire \texttt{/home/gwift}.
\end{itemize}

Pour résumer, l'ensemble de ces commandes nous donne ceci:

\begin{enumerate}
    \item \texttt{groupadd --system webapps}
    \item \texttt{groupadd --system gunicorn\_sockets}
    \item \texttt{useradd --system --gid webapps --shell /bin/bash --home /home/gwift gwift}
    \item \texttt{mkdir -p /home/gwift}
    \item \texttt{chown gwift:webapps /home/gwift}
\end{enumerate}

\subsection{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:

\begin{verbatim}
    apt update
    apt install python3
\end{verbatim}

Si vous souhaitez utiliser une version ultérieure, il suffit de l'installer \textbf{en parallèle} de la version officiellement supportée par votre distribution.

Ou passer par une installation alternative:

\begin{verbatim}
    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
\end{verbatim}

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

\subsection{Base de données}

On l'a déjà vu, Django se base sur un pattern type \href{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 :

\begin{itemize}
    \item
        SQLite (en natif),
    \item
        MariaDB (en natif depuis Django 3.0),
    \item
        PostgreSQL au travers de psycopg2 (en natif aussi),
    \item
        Microsoft SQLServer grâce aux drivers \href{https://github.com/microsoft/mssql-django}{Microsoft}
    \item
        Oracle via \href{https://oracle.github.io/python-cx_Oracle/}{cx\_Oracle}.
\end{itemize}

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, quelques procédures d'installation pour mettre un serveur à disposition.
Les deux plus simples seront MariaDB et PostgreSQL, que nous couvrirons ci-dessous.

\subsection{PostgreSQL}

Dans le cas de Debian ou Ubuntu, nous exécuterons la commande suivante:

\begin{verbatim}
    apt install postgresql postgresql-contrib
\end{verbatim}

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:

\begin{verbatim}
    # 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
\end{verbatim}

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

\begin{verbatim}
    postgres@gwift:~$ createdb --owner gwift_user gwift
    postgres@gwift:~$ exit
    logout
\end{verbatim}

\subsection{MariaDB}

Idem, installation, configuration, backup, tout ça.
A copier de grimboite, je suis sûr d’avoir des notes là-dessus.

\subsection{Comparatif PostgreSQL/MariaDB}

\begin{tabular}{lll}
    Etape & PostgreSQL & MariaDB \\
    \hline
    Installation & \texttt{apt install postgresql postgresql-contrib} & \texttt{apt install maria-db} \\
    \hline
    Création de l'utilisateur & & \\
    \hline
    Création de la base de données &  & \\
    \hline
    Dump & & \\
 \end{tabular}

\section{Préparation de l'environment utilisateur}

\begin{verbatim}
    su - gwift
    cp /etc/skel/.bashrc .
    cp /etc/skel/.bash_profile .
    ssh-keygen
    mkdir bin
    mkdir .venvs
    mkdir webapps
    python3.6 -m venv .venvs/gwift
    source .venvs/gwift/bin/activate
    cd /home/gwift/webapps
    git clone ...
\end{verbatim}

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 :

\begin{verbatim}
    # en tant qu'utilisateur 'gwift'
    source .venvs/gwift/bin/activate
    pip install -U pip
    pip install -r requirements/base.txt
    pip install gunicorn
    cd webapps/gwift
    gunicorn config.wsgi:application --bind localhost:3000 --settings
    =config.settings_production
\end{verbatim}


\subsection{Configuration de l'application}

\begin{verbatim}
    SECRET_KEY=<set your secret key here>
    ALLOWED_HOSTS=*
    STATIC_ROOT=/var/www/gwift/static
    DATABASE=
\end{verbatim}

\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}


\subsection{Création des répertoires de logs}

\begin{verbatim}
    mkdir -p /var/www/gwift/static
\end{verbatim}

\subsection{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}


\subsection{Gunicorn}

\begin{verbatim}
    #!/bin/bash
    # defines settings for gunicorn
    NAME="gwift"
    DJANGODIR=/home/gwift/webapps/gwift
    SOCKFILE=/var/run/webapps/gunicorn_gwift.sock
    USER=gwift
    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 \
        --user $USER \
        --bind=unix:$SOCKFILE \
        --log-level=debug \
        --log-file=-
\end{verbatim}

\section{Composants périphériques}

\subsection{Supervsion, keepalive et autoreload}

Pour la supervision, on passe par Supervisor.
Il existe d'autres superviseurs,

\begin{verbatim}
    yum install supervisor -y
\end{verbatim}

On crée ensuite le fichier \texttt{/etc/supervisord.d/gwift.ini}:

\begin{verbatim}
    [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
\end{verbatim}

Et on crée les répertoires de logs, on démarre supervisord et on vérifie qu'il tourne correctement :

\begin{verbatim}
    $ 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
\end{verbatim}

On peut aussi vérifier que l'application est en train de tourner, à l'aide de la commande \texttt{supervisorctl} :

\begin{verbatim}
    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
\end{verbatim}

\subsection{Firewall}

\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}


\subsection{Unattented-upgrades}


\subsection{Healthchecks.io}


\subsection{Reverse proxy}

\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}
  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;

        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}

\subsubsection{Let's Encrypt}

Certificats externes + communication par socket interne.

\section{Mise à jour}

\begin{verbatim}
    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`
\end{verbatim}

\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}

\subsection{Logrotate}

\begin{verbatim}
    /var/log/gwift/* {
        weekly
        rotate 3
        size 10M
        compress
        delaycompress
    }
\end{verbatim}

Puis on démarre logrotate avec \texttt{logrotate -d /etc/logrotate.d/gwift} pour vérifier que cela fonctionne correctement.

\subsection{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}

\subsection{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.

\section{Ansible}

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

\section{Docker-Compose}

(c/c Ced' - 2020-01-24)

Ça y est, j'ai fait un test sur mon portable avec docker et cookiecutter pour django.

D'abords, après avoir installer docker-compose et les dépendances sous debian, tu dois t'ajouter dans le groupe docker, sinon il faut être root pour utiliser docker. Ensuite, j'ai relancé mon pc car juste relancé un shell n'a pas suffit pour que je puisse utiliser docker avec mon compte.

Bon après c'est facile, un petit virtualenv pour cookiecutter, suivi d'une installation du template django.
Et puis j'ai suivi \url{https://cookiecutter-django.readthedocs.io/en/latest/developing-locally-docker.html}

Alors, il télécharge les images, fait un petit update, installe les dépendances de dev, install les requirement pip \ldots

Du coup, ça prend vite de la place: image.png

L'image de base python passe de 179 à 740 MB. Et là j'en ai pour presque 1,5 GB d'un coup.

Mais par contre, j'ai un python 3.7 direct et postgres 10 sans rien faire ou presque.

La partie ci-dessous a été reprise telle quelle de \href{https://cookiecutter-django.readthedocs.io/en/latest/deployment-with-docker.html}{la
documentation de cookie-cutter-django}.

le serveur de déploiement ne doit avoir qu'un accès en lecture au dépôt source.
