
\chapter{Travailler en isolation}

Nous allons aborder la gestion et l'isolation des dépendances. Cette section est aussi utile pour une personne travaillant seule, que pour transmettre les connaissances à un nouveau membre de l'équipe ou pour déployer l'application elle-même.

Il en était déjà question au deuxième point des 12 facteurs: même dans le cas de petits projets, il est déconseillé de s'en passer.
Cela évite les déploiements effectués à l'arrache à grand renfort de \texttt{sudo} et d'installation globale de dépendances, pouvant potentiellement occasioner des conflits entre les applications déployées:

\begin{enumerate}
    \item
        Il est tout à fait envisagable que deux applications différentes soient déployées sur un même hôte, et nécessitent chacune deux versions différentes d'une même dépendance.
    \item
        Pour la reproductibilité d'un environnement spécifique, cela évite notamment les réponses type "Ca marche chez moi", puisque la construction du nouvel environnement fait partie intégrante du processus de développement et de la documentation du projet; grâce à elle, nous avons la possibilité de construire un environnement sain et d'appliquer des dépendances identiques, quelle que soit l'hôte destiné à accueillir le déploiment.
\end{enumerate}

\begin{figure}
    \centering
    \includegraphics{images/it-works-on-my-machine.jpg}
    \caption{It works on my machine}
\end{figure}

\section{Environnements virtuels}

\begin{figure}
    \centering
    \includegraphics{images/xkcd-1987.png}
    \caption{\url{https://xkcd.com/1987}}
\end{figure}

Un des reproches que l'on peut faire au langage concerne sa versatilité: il est possible de réaliser beaucoup de choses, mais celles-ci ne sont pas toujours simples ou directes.
Pour quelqu'un qui débarquererait, la quantité d'options différentes peut paraître rebutante.
Nous pensons notamment aux environnements virtuels : ils sont géniaux à utiliser, mais on est passé par virtualenv (l'ancêtre), virtualenvwrapper (sa version améliorée et plus ergonomique), \texttt{venv} (la version intégrée depuis la version 3.3 de l'interpréteur, et \href{https://docs.python.org/3/library/venv.html}{la manière recommandée} de créer un environnement depuis la 3.5).

Pour créer un nouvel environnement, vous aurez donc besoin:

\begin{enumerate}
    \item
        D'une installation de Python - \url{https://www.python.org/}
    \item
        D'un terminal - voir le point \href{../environment/_index.xml\#un-terminal}{Un terminal}
\end{enumerate}


Il existe plusieurs autres modules permettant d'arriver au même résultat, avec quelques avantages et inconvénients pour chacun d'entre eux.
Le plus prometteur d'entre eux est \href{https://python-poetry.org/}{Poetry}, qui dispose d'une interface en ligne de commande plus propre et plus moderne que ce que PIP propose.


\subsection{Poetry}

Poetry se propose de gérer le projet au travers d'un fichier pyproject.toml.
TOML (du nom de son géniteur, Tom Preston-Werner, légèrement CEO de GitHub à ses heures), se place comme alternative aux formats comme JSON, YAML ou INI.


\begin{verbatim}
    $ poetry new django-gecko
    $ tree django-gecko/
    django-gecko/
    django_gecko
        __init__.py
        pyproject.toml
        README.rst
        tests
        __init__.py
        test_django_gecko.py
    2 directories, 5 files
\end{verbatim}


Ceci signifie que nous avons directement (et de manière standard):

\begin{itemize}
    \item
        Un répertoire django-gecko, qui porte le nom de l'application que vous venez de créer
    \item
        Un répertoires tests, libellé selon les standards de pytest
    \item
        Un fichier README.rst (qui ne contient encore rien)
    \item
        Un fichier pyproject.toml, qui contient ceci:
\end{itemize}

\begin{verbatim}
[tool.poetry]
name = "django-gecko"
version = "0.1.0"
description = ""
authors = ["... <...@grimbox.be>"]

[tool.poetry.dependencies]
python = "^3.9"

[tool.poetry.dev-dependencies]
pytest = "^5.2"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
\end{verbatim}

La commande \texttt{poetry\ init} permet de générer interactivement les fichiers nécessaires à son intégration dans un projet existant.

J'ai pour habitude de conserver mes projets dans un répertoire \texttt{\textasciitilde{}/Sources/} et mes environnements virtuels dans un répertoire \texttt{\textasciitilde{}/.venvs/}.

Cette séparation évite que l'environnement virtuel ne se trouve dans le même répertoire que les sources, ou ne soit accidentellement envoyé vers le système de gestion de versions.
Elle évite également de rendre ce répertoire "visible" - il ne s'agit au fond que d'un paramètre de configuration lié uniquement à votre environnement de développement; les environnements virtuels étant disposables, il n'est pas conseillé de trop les lier au projet qui l'utilise comme base.
Dans la suite de ce chapitre, je considérerai ces mêmes répertoires, mais n'hésitez pas à les modifier.

DANGER: Indépendamment de l'endroit où vous stockerez le répertoire contenant cet environnement, il est primordial de \textbf{ne pas le conserver dans votre dépôt de stockager}.
Cela irait à l'encontre des douze facteurs, cela polluera inutilement vos sources et créera des conflits avec l'environnement des personnes qui souhaiteraient intervenir sur le projet.

Pur créer notre répertoire de travail et notre environnement virtuel, exécutez les commandes suivantes:

\begin{verbatim}
    mkdir ~/.venvs/
    python -m venv ~/.venvs/gwift-venv
\end{verbatim}

Ceci aura pour effet de créer un nouveau répertoire (\texttt{\textasciitilde{}/.venvs/gwift-env/}), dans lequel vous trouverez une installation complète de l'interpréteur Python.
Votre environnement virtuel est prêt, il n'y a plus qu'à indiquer que nous souhaitons l'utiliser, grâce à l'une des commandes suivantes:

\begin{verbatim}
    # GNU/Linux, macOS
    source ~/.venvs/gwift-venv/bin/activate
    # MS Windows, avec Cmder
    ~/.venvs/gwift-venv/Scripts/activate.bat
    # Pour les deux
    (gwift-env) fred@aerys:~/Sources/.venvs/gwift-env$
\end{verbatim}


A présent que l'environnement est activé, tous les binaires de cet environnement prendront le pas sur les binaires du système.
De la même manière, une variable \texttt{PATH} propre est définie et utilisée, afin que les librairies Python y soient stockées.
C'est donc dans cet environnement virtuel que nous retrouverons le code source de Django, ainsi que des librairies externes pour Python une fois que nous les
aurons installées.

Pour les curieux, un environnement virtuel n'est jamais qu'un répertoire dans lequel se trouve une installation fraîche de l'interpréteur, vers laquelle pointe les liens symboliques des binaires.
Si vous recherchez l'emplacement de l'interpréteur avec la commande \texttt{which\ python}, vous recevrez comme réponse \texttt{/home/fred/.venvs/gwift-env/bin/python}.

Pour sortir de l'environnement virtuel, exécutez la commande \texttt{deactivate}.
Si vous pensez ne plus en avoir besoin, supprimer le dossier.
Si nécessaire, il suffira d'en créer un nouveau.

Pour gérer des versions différentes d'une même librairie, il nous suffit de jongler avec autant d'environnements que nécessaires.
Une application nécessite une version de Django inférieure à la 2.0 ? On crée un environnement, on l'active et on installe ce qu'il faut.

Cette technique fonctionnera autant pour un poste de développement que sur les serveurs destinés à recevoir notre application.

Par la suite, nous considérerons que l'environnement virtuel est toujours activé, même si \texttt{gwift-env} n'est pas indiqué.

a manière recommandée pour la gestion des dépendances consiste à les épingler dans un fichier requirements.txt, placé à la racine du projet.
Ce fichier reprend, ligne par ligne, chaque dépendance et la version nécessaire.
Cet épinglage est cependant relativement basique, dans la mesure où les opérateurs disponibles sont ==,  et \textgreater=.

Poetry propose un épinglage basé sur SemVer.
Les contraintes qui peuvent être appliquées aux dépendances sont plus touffues que ce que proposent pip -r, avec la présence du curseur \^{}, qui ne modifiera pas le nombre différent de zéro le plus à gauche:

\begin{verbatim}
^1.2.3 (où le nombre en question est 1) pourra proposer une mise à jour jusqu'à la version juste avant la version 2.0.0
^0.2.3 pourra être mise à jour jusqu'à la version juste avant 0.3.0.
...
\end{verbatim}


L'avantage est donc que l'on spécifie une version majeure - mineure - patchée, et que l'on pourra spécifier accepter toute mise à jour jusqu'à la prochaine version majeure - mineure patchée (non incluse).

Une bonne pratique consiste également, tout comme pour npm, à intégrer le fichier de lock (poetry.lock) dans le dépôt de sources : de cette manière, seules les dépendances testées (et intégrées) seront considérées sur tous les environnements de déploiement.

Il est alors nécessaire de passer par une action manuelle (poetry update) pour mettre à jour le fichier de verrou, et assurer une mise à jour en sécurité (seules les dépendances testées sont prises en compte)
et de qualité (tous les environnements utilisent la même version d'une dépendance).

L'ajout d'une nouvelle dépendance à un projet se réalise grâce à la commande \texttt{poetry\ add\ \textless{}dep\textgreater{}}:

\begin{verbatim}
$ poetry add django
Using version ^3.2.3 for Django

Updating dependencies
Resolving dependencies... (5.1s)

Writing lock file

Package operations: 8 installs, 1 update, 0 removals

  • Installing pyparsing (2.4.7)
  • Installing attrs (21.2.0)
  • Installing more-itertools (8.8.0)
  • Installing packaging (20.9)
  • Installing pluggy (0.13.1)
  • Installing py (1.10.0)
  • Installing wcwidth (0.2.5)
  • Updating django (3.2 -> 3.2.3)
  • Installing pytest (5.4.3)
\end{verbatim}

Elle est ensuite ajoutée à notre fichier \texttt{pyproject.toml}:

\begin{verbatim}
[...]

[tool.poetry.dependencies]
python = "^3.9"
Django = "^3.2.3"

[...]
\end{verbatim}

Et contrairement à \texttt{pip}, pas besoin de savoir s'il faut pointer vers un fichier (\texttt{-r}) ou un dépôt VCS (\texttt{-e}), puisque Poetry va tout essayer, {[}dans un certain ordre{]}(\url{https://python-poetry.org/docs/cli/\#add}).
L'avantage également (et cela m'arrive encore souvent, ce qui fait hurler le runner de Gitlab), c'est qu'il n'est plus nécessaire de penser à épingler la dépendance que l'on vient d'installer parmi les fichiers de
requirements, puisqu'elles s'y ajoutent automatiquement grâce à la commande \texttt{add}.

\subsubsection{Python Packaging Made Easy}

Cette partie dépasse mes compétences et connaissances, dans la mesure où je n'ai jamais rien packagé ni publié sur {[}pypi.org{]}(pypi.org).
Ce n'est pas l'envie qui manque, mais les idées et la nécessité.
Ceci dit, Poetry propose un ensemble de règles et une préconfiguration qui (doivent) énormément facilite(r) la mise à disposition de librairies sur Pypi - et rien que ça, devrait ouvrir une partie de l'écosystème.

Les chapitres 7 et 8 de {[}Expert Python Programming - Third Edtion{]}(\#), écrit par Michal Jaworski et Tarek Ziadé en parlent très bien:

\begin{quote}
    Python packaging can be a bit overwhelming at first. The main reason for
    that is the confusion about proper tools for creating Python packages.
    Anyway, once you create your first package, you will se that this is as
    hard as it looks. Also, knowing propre, state-of-the-art packaging helps
    a lot.
\end{quote}

En gros, c'est ardu-au-début-mais-plus-trop-après.
Et c'est heureusement suivi et documenté par la PyPA (\textbf{\href{https://github.com/pypa}{Python Packaging Authority}}).

Les étapes sont les suivantes:

\begin{enumerate}
\item
  Utiliser setuptools pour définir les projets et créer les distributions sources,
\item
  Utiliser \textbf{wheels} pour créer les paquets,
\item
  Passer par \textbf{twine} pour envoyer ces paquets vers PyPI
\item
  Définir un ensemble d'actions (voire, de plugins nécessaires - lien avec le VCS, etc.) dans le fichier \texttt{setup.py}, et définir les propriétés du projet ou de la librairie dans le fichier \texttt{setup.cfg}.
\end{enumerate}

Avec Poetry, deux commandes suffisent (théoriquement - puisque je n'ai pas essayé) : \texttt{poetry\ build} et \texttt{poetry\ publish}:

\begin{verbatim}
$ poetry build
Building geco (0.1.0)
  - Building sdist
  - Built geco-0.1.0.tar.gz
  - Building wheel
  - Built geco-0.1.0-py3-none-any.whl

$ tree dist/
dist/
-- geco-0.1.0-py3-none-any.whl
-- geco-0.1.0.tar.gz

0 directories, 2 files
\end{verbatim}

Ce qui est quand même 'achement plus simple que d'appréhender tout un
écosystème.

\section{Un système de virtualisation}

Par "\emph{système de virtualisation}", nous entendons n'importe quel application, système d'exploitation, système de containeurisation, \ldots qui permette de créer ou recréer un environnement de développement aussi proche que celui en production.
Les solutions sont nombreuses:

\begin{itemize}
\item
  \href{https://www.virtualbox.org/}{VirtualBox}
\item
  \href{https://www.vagrantup.com/}{Vagrant}
\item
  \href{https://www.docker.com/}{Docker}
\item
  \href{https://linuxcontainers.org/lxc/}{Linux Containers (LXC)}
\item
  \href{https://docs.microsoft.com/fr-fr/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v}{Hyper-V}
\end{itemize}

Ces quelques propositions se situent un cran plus loin que la "simple" isolation d'un environnement, puisqu'elles vous permettront de construire un environnement complet.
Elles constituent donc une étape supplémentaires dans la configuration de votre espace de travail, mais en amélioreront la qualité.

Dans la suite, nous détaillerons Vagrant et Docker, qui constituent deux solutions automatisables et multiplateformes, dont la configuration peut faire partie intégrante de vos sources.

\subsection{Vagrant}

\includegraphics{images/logo-vagrant.png}

Vagrant consiste en un outil de création et de gestion d'environnements virtualisés, en respectant toujours une même manière de travailler, indépendamment des choix techniques et de l'infrastructure que vous pourriez sélectionner.

\begin{quote}
    Vagrant is a tool for building and managing virtual machine environments
    in a single workflow. With an easy-to-use workflow and focus on
    automation, Vagrant lowers development environment setup time, increases
    production parity, and makes the "works on my machine" excuse a relic of
    the past. \footnote{\url{https://www.vagrantup.com/intro}}
\end{quote}

La partie la plus importante de la configuration de Vagrant pour votre projet consiste à placer un fichier \texttt{Vagrantfile} - \emph{a priori} à la racine de votre projet - et qui contiendra les information suivantes:

\begin{itemize}
    \item
        Le choix du \emph{fournisseur} (\textbf{provider}) de virtualisation (Virtualbox, Hyper-V et Docker sont natifs; il est également possible de passer par VMWare, AWS, etc.)
    \item
        Une \emph{box}, qui indique le type et la version attendue du système virtualisé (Debian 10, Ubuntu 20.04, etc. - et \href{https://app.vagrantup.com/boxes/search}{il y a du choix}).
    \item
        La manière dont la fourniture (\textbf{provisioning}) de l'environnement doit être réalisée : scripts Shell, fichiers, Ansible, Puppet, Chef, \ldots
        Il est toujours possible de passer par une installation et une maintenance manuelle, après s'être connecté sur la machine.
    \item
        Si un espace de stockage doit être partagé entre la machine virtuelle et l'hôte
    \item
        Les ports qui doivent être transmis de la machine virtuelle vers l'hôte.
\end{itemize}

La syntaxe de ce fichier \texttt{Vagrantfile} est en \href{https://www.ruby-lang.org/en/}{Ruby}.
Vous trouverez ci-dessous un exemple, généré (et nettoyé) après avoir exécuté la commande \texttt{vagrant\ init}:

\begin{listing}[H]
    \begin{minted}{ruby}
    # -*- mode: ruby -*-
    # vi: set ft=ruby :

    Vagrant.configure("2") do |config|

    config.vm.box = "ubuntu/bionic64"
    config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip:
    "127.0.0.1"

    config.vm.provider "virtualbox" do |vb|
        vb.gui = true
        vb.memory = "1024"
    end

    config.vm.provision "shell", inline: <<-SHELL
        apt-get update
        apt-get install -y nginx
        SHELL
    end
    \end{minted}
\end{listing}

Dans le fichier ci-dessus, nous créons une nouvelle machine virtuelle (ie. \emph{invitée}) sous Ubuntu Bionic Beaver, en x64

\begin{itemize}
    \item
        Avec une correspondance du port \texttt{80} de la machine vers le port \texttt{8080} de l'hôte, en limitant l'accès à celui-ci - accédez à \texttt{localhost:8080} et vous accéderez au port \texttt{80} de la machine virtuelle.
    \item
        En utilisant Virtualbox comme backend - la mémoire vive allouée sera limitée à 1Go de RAM et nous ne voulons pas voir l'interface graphique au démarrage
    \item
        Et pour finir, nous voulons appliquer un script de mise à jour \texttt{apt-get\ update} et installer le paquet \texttt{nginx}
\end{itemize}

Par défaut, le répertoire courant (ie. le répertoire dans lequel notre fichier \texttt{Vagrantfile} se trouve) sera synchronisé dans le répertoire \texttt{/vagrant} sur la machine invitée.

\subsection{Docker}

\includegraphics{images/docker.jpg}

(copié/collé de cookie-cutter)

\begin{listing}[H]
    \begin{verbatim}
    version: '3'

    volumes:
        local_postgres_data: {}
        local_postgres_data_backups: {}

    services:
        << description des services >>
    \end{verbatim}
\end{listing}

\begin{listing}[H]
    \begin{verbatim}
    django: &django
        build:
            context: .
            dockerfile: ./compose/local/django/Dockerfile
        image: khana_local_django
        container_name: django
        depends_on:
            - postgres
        volumes:
            - .:/app:z
        env_file:
            - ./.envs/.local/.django
            - ./.envs/.local/.postgres
        ports:
            - "8000:8000"
        command: /start
    \end{verbatim}
\end{listing}

\begin{listing}[H]
    \begin{verbatim}
    postgres:
        build:
        context: .
            dockerfile: ./compose/production/postgres/Dockerfile
        image: khana_production_postgres
        container_name: postgres
        volumes:
            - local_postgres_data:/var/lib/postgresql/data:Z
            - local_postgres_data_backups:/backups:z
        env_file:
            - ./.envs/.local/.postgres
    \end{verbatim}
\end{listing}

\begin{listing}[H]
    \begin{verbatim}
    docs:
        image: khana_local_docs
        container_name: docs
        build:
            context: .
            dockerfile: ./compose/local/docs/Dockerfile
        env_file:
            - ./.envs/.local/.django
        volumes:
            - ./docs:/docs:z
            - ./config:/app/config:z
            - ./khana:/app/khana:z
        ports:
            - "7000:7000"
        command: /start-docs
    \end{verbatim}
\end{listing}

\begin{listing}[H]
    \begin{verbatim}
    redis:
        image: redis:5.0
        container_name: redis
    \end{verbatim}
\end{listing}

\begin{listing}[H]
    \begin{verbatim}
    celeryworker:
        <<: *django
        image: khana_local_celeryworker
        container_name: celeryworker
        depends_on:
            - redis
            - postgres
        ports: []
        command: /start-celeryworker
    \end{verbatim}
\end{listing}

\begin{listing}[H]
    \begin{verbatim}
    celerybeat:
        <<: *django
        image: khana_local_celerybeat
        container_name: celerybeat
        depends_on:
        - redis
        - postgres
        ports: []
        command: /start-celerybeat
    \end{verbatim}
\end{listing}

\begin{listing}[H]
    \begin{verbatim}
    flower:
        <<: *django
        image: khana_local_flower
        container_name: flower
        ports:
        - "5555:5555"
        command: /start-flower
    \end{verbatim}
\end{listing}

\subsection{Docker-compose}

\begin{listing}[H]
  \begin{verbatim}
  # docker-compose.yml

  version: '3.8'
  services:
    web:
      build: .
        command: python /code/manage.py runserver 0.0.0.0:8000
      volumes:
      - .:/code
      ports:
      - 8000:8000
      depends_on:
      - slqserver

    slqserver:
      image: mcr.microsoft.com/mssql/server:2019-latest
      environment:
      - "ACCEPT_EULA=Y"
      - "SA_PASSWORD=sqklgjqihagrtdgqk12§!"
      ports:
      - 1433:1433
      volumes:
      - ../sqlserver/data:/var/opt/mssql/data
      - ../sqlserver/log:/var/opt/mssql/log
      - ../sqlserver/secrets:/var/opt/mssql/secrets
  \end{verbatim}
\end{listing}

\subsection{Dockerfile}

\begin{listing}[H]
\begin{verbatim}
  FROM python:3.8-slim-buster

  ENV PYTHONUNBUFFERED 1
  ENV PYTHONDONTWRITEBYTECODE 1

  RUN apt-get update \

    # dependencies for building Python packages
    && apt-get install -y build-essential \
    # psycopg2 dependencies
    && apt-get install -y libpq-dev \
    # Translations dependencies
    && apt-get install -y gettext \
    # cleaning up unused files
    && apt-get purge -y --auto-remove -o
    APT::AutoRemove::RecommendsImportant=false \
    && rm -rf /var/lib/apt/lists/*

  # Requirements are installed here to ensure they will be cached.
  COPY ./requirements /requirements
  RUN pip install -r /requirements/local.txt

  COPY ./compose/production/django/entrypoint /entrypoint
  RUN sed -i 's/\r$//g' /entrypoint
  RUN chmod +x /entrypoint

  COPY ./compose/local/django/start /start
  RUN sed -i 's/\r$//g' /start
  RUN chmod +x /start

  COPY ./compose/local/django/celery/worker/start /start-celeryworker
  RUN sed -i 's/\r$//g' /start-celeryworker
  RUN chmod +x /start-celeryworker

  COPY ./compose/local/django/celery/beat/start /start-celerybeat
  RUN sed -i 's/\r$//g' /start-celerybeat
  RUN chmod +x /start-celerybeat

  COPY ./compose/local/django/celery/flower/start /start-flower
  RUN sed -i 's/\r$//g' /start-flower
  RUN chmod +x /start-flower

  WORKDIR /app

  ENTRYPOINT ["/entrypoint"]
\end{verbatim}
\end{listing}

\section{Docker \& Dockerfile}

\begin{listing}
	\begin{verbatim}
		# Dockerfile

		# Pull base image
		FROM python:3.8-slim-buster

		# Set environment variables
		ENV PYTHONDONTWRITEBYTECODE 1
		ENV PYTHONUNBUFFERED 1
		ENV DEBIAN_FRONTEND noninteractive
		ENV ACCEPT_EULA=Y

		# install Microsoft SQL Server requirements.
		ENV ACCEPT_EULA=Y
		RUN apt-get update -y && apt-get update \
		  && apt-get install -y --no-install-recommends curl gcc g++ gnupg


		# Add SQL Server ODBC Driver 17
		RUN curl https://packages.microsoft.com/keys/microsoft.asc | apt-key add -
		RUN curl https://packages.microsoft.com/config/debian/10/prod.list > /etc/apt/sources.list.d/mssql-release.list
		RUN apt-get update \
		  && apt-get install -y msodbcsql17 unixodbc-dev

		# clean the install.
		RUN apt-get -y clean

		# Set work directory
		WORKDIR /code

		# Install dependencies
		COPY ./requirements/base.txt /code/requirements/
		RUN pip install --upgrade pip
		RUN pip install -r ./requirements/base.txt

		# Copy project
		COPY . /code/

	\end{verbatim}
	\caption{Un exemple de Dockerfile}
\end{listing}
