Conteneuriser vos applications Python – Part 1

Conteneuriser vos applications

Développer des projets Python dans des environnements locaux peut s’avérer difficile si plusieurs projets sont développés en même temps. Déjà, l’amorçage d’un projet peut être chronophage car il faut gérer les versions, configurer les dépendances et les configurations. Auparavant, nous avions l’habitude d’installer toutes les exigences du projet directement dans notre environnement local. Ensuite nous démarrions l’écriture du code. Mais la simultanéité et la multiplicité de projets dans le même environnement crée des conflits de configuration ou de dépendance. De plus, lors du partage d’un projet avec des coéquipiers, nous devons également coordonner nos environnements. Pour cela, nous devons définir l’environnement de notre projet de manière à ce qu’il soit facilement partageable. 

Pour résoudre ces difficultés, il convient de créer des environnements de développement isolés pour chaque projet. Cela peut être facilement réalisé à l’aide de conteneurs et Docker Compose pour les gérer. Nous allons vous faire découvrir cela au travers d’une série d’articles avec chacun avec un objectif spécifique.

Cette première partie explique comment conteneuriser un service / outil Python et les meilleures pratiques pour celui-ci.

Conditions

Pour l’ensemble des pratiques proposées dans notre série d’articles de blog, un ensemble minimal d’outils est requis pour gérer les environnements conteneurisés localement :

Conteneuriser vos applications Python

Pour cela, nous utilisons avec un service Flask simple afin de l’exécuter de manière autonome. Ainsi il n’y a pas besoin de configurer d’autres composants.

server.py
from flask import Flask
server = Flask(__name__)

@server.route("/")
 def hello():
    return "Hello Digicactus!"

if __name__ == "__main__":
   server.run(host='0.0.0.0')

Pour exécuter ce programme, nous devons nous assurer que toutes les dépendances requises sont installées en premier. Une façon de gérer les dépendances consiste à utiliser un programme d’installation de package tel que pip. Pour cela, nous devons créer un fichier requirements.txt et y écrire les dépendances. Un exemple d’un tel fichier pour notre simple server.py est le suivant: requirements.txt

Flask==1.1.1

Nous avons maintenant la structure suivante :

app
├─── requirements.txt
└─── src
     └─── server.py

Ensuite nous créons un répertoire dédié pour le code source afin de l’isoler des autres fichiers de configuration. Nous verrons plus tard pourquoi nous faisons cela.

Pour exécuter notre programme Python, il ne reste plus qu’à installer un interpréteur Python et à l’exécuter. 

Nous pourrions exécuter ce programme localement. Mais cela va à l’encontre du but de la conteneurisation de notre développement. En effet, conserver un environnement standard propre permet de basculer facilement entre des projets avec des exigences contradictoires différentes.

Voyons ensuite comment nous pouvons facilement conteneuriser ce service Python.

Dockerfile 

La façon d’exécuter notre code Python dans un conteneur consiste à conteneuriser vos applications et la conditionner en tant qu’image Docker, puis à l’exécuter. Les étapes sont esquissées ci-dessous.

Conteneuriser vos applications

Pour générer une image Docker, nous devons créer un Dockerfile qui contient les instructions nécessaires pour créer l’image. Le Dockerfile est ensuite traité par le constructeur Docker qui génère l’image Docker. Puis, avec une simple commande docker run, nous créons et exécutons un conteneur avec le service Python.

Analyse d’un Dockerfile

Voici un exemple de Dockerfile contenant des instructions pour assembler une image Docker pour notre service Python hello world :

Dockerfile# set base image (host OS)
FROM python:3.8

# set the working directory in the container
WORKDIR /code

# copy the dependencies file to the working directory
COPY requirements.txt .

# install dependencies
RUN pip install -r requirements.txt

# copy the content of the local src directory to the working directory
COPY src/ .

# command to run on container start
CMD [ "python", "./server.py" ]

Pour chaque instruction ou commande du Dockerfile, le constructeur Docker génère un calque d’image et l’empile sur les précédents. Par conséquent, l’image Docker résultant du processus est simplement une pile en lecture seule de différentes couches.

Nous pouvons également observer dans la sortie de la commande build, les instructions Dockerfile étant exécutées comme des étapes.

$ docker build -t digicactus .
Sending build context to Docker daemon 6.144kB
Step 1/6 : FROM python:3.8
3.8.3-alpine: Pulling from library/python
…
Status: Downloaded newer image for python:3.8.3-alpine
---> 8ecf5a48c789
Step 2/6 : WORKDIR /code
---> Running in 9313cd5d834d
Removing intermediate container 9313cd5d834d
---> c852f099c2f9
Step 3/6 : COPY requirements.txt .
---> 2c375052ccd6
Step 4/6 : RUN pip install -r requirements.txt
---> Running in 3ee13f767d05
…
Removing intermediate container 3ee13f767d05
---> 8dd7f46dddf0
Step 5/6 : COPY ./src .
---> 6ab2d97e4aa1
Step 6/6 : CMD python server.py
---> Running in fbbbb21349be
Removing intermediate container fbbbb21349be
---> 27084556702b
Successfully built 70a92e92f3b5
Successfully tagged digicactus:latest

Ensuite, nous pouvons vérifier que l’image est dans le magasin d’images local :

$ docker images
REPOSITORY    TAG       IMAGE ID        CREATED          SIZE
digicactus       latest    70a92e92f3b5    8 seconds ago    991MB

Pendant le développement, l’image pour notre service Python sera nécessairement plusieurs fois reconstruite. Afin de minimiser ce cycle de développement, nous pouvons appliquer quelques bonnes pratiques.

Meilleures pratiques de développement pour Dockerfiles

Concentrons nous maintenant sur les meilleures pratiques pour accélérer le cycle de développement. Pour ceux axés sur la production, cet article de blog et la documentation les couvrent plus en détail.

Image de base

La première instruction du Dockerfile spécifie l’image de base sur laquelle nous ajoutons de nouvelles couches pour notre application. Son choix est essentiel car les fonctionnalités qu’elle embarque peuvent avoir un impact sur la qualité des couches construites dessus. 

Dans l’idéal, nous devons privilégier l’utilisation des images officielles. Elles sont généralement fréquemment mises à jour présentant ainsi moins de problèmes de sécurité.

Une autre caractéristique importante à prendre en compte dans le choix de l’image de base est la taille. En effet, cela peut avoir un impact sur la taille de l’image finale. Si nous privilégions la taille, il faut des images de bases de très petites tailles n’entrainant que de faible surcharge. Ces images sont généralement basées sur la distribution alpine et sont étiquetées en conséquence. A noter que pour les applications Python, l’image officielle Docker Python, telle que python : 3.8-slim, est fonctionnelle.

L’importance de l’ordre des instructions pour tirer parti du cache de compilation

Lorsqu’une image est recrée fréquemment, il est nécessaire d’utiliser le mécanisme de cache du générateur afin d’accélérer les versions ultérieures. Comme énoncé précédemment, les instructions Dockerfile sont exécutées dans l’ordre spécifié. Pour chaque instruction, le générateur vérifie d’abord son cache pour une image à réutiliser. Lorsqu’un changement dans un calque est détecté, ce calque et tous ceux qui suivent sont en cours de reconstruction.

L’utilisation du mécanisme de mise en cache doit être optimisée. Pour cela nous devons hiérarchiser les instructions des couches. Ainsi, les instructions pour les couches les plus changeantes seront après celles subissant le moins de changements.

Vérifions notre exemple Dockerfile pour comprendre comment l’ordre des instructions affecte la mise en cache. Les lignes intéressantes sont celles ci-dessous :

...
# copy the dependencies file to the working directory
COPY requirements.txt .

# install dependencies
RUN pip install -r requirements.txt

# copy the content of the local src directory to the working directory
COPY src/ .
...

Pendant le développement, les dépendances de notre application changent moins fréquemment que le code Python. Pour cette raison, nous choisissons d’installer les dépendances dans une couche précédente à celle du code. Par conséquent, nous copions le fichier de dépendances et les installons, puis nous copions le code source. De ce fait, nous avons isolé le code source dans un répertoire dédié dans la structure de notre projet.

Constructions en plusieurs étapes 

Négligeable lors du développement, mais intéressant une fois le développement terminé, nous vous montrons comment livrer l’application Python conteneurisée.

La construction en plusieurs étapes permet de ne fournir que les fichiers nécessaires à l’exécution de notre code Python. Ainsi nous dépouillons l’image finale de l’application de tous les fichiers et packages logiciels inutiles. Voici un exemple rapide de fichier Dockerfile à plusieurs étapes pour notre exemple précédent:

# first stage
FROM python:3.8 AS builder
COPY requirements.txt .

# install dependencies to the local user directory (eg. /root/.local)
RUN pip install --user -r requirements.txt

# second unnamed stage
FROM python:3.8-slim
WORKDIR /code

# copy only the dependencies installation from the 1st stage image
COPY --from=builder /root/.local/bin /root/.local
COPY ./src .

# update PATH environment variable
ENV PATH=/root/.local:$PATH

CMD [ "python", "./server.py" ]

Notons que nous avons une construction en deux étapes où nous ne nommons que la première en tant que constructeur . Nous nommons une étape en ajoutant un AS <nom> à partir de l’ instruction et nous utilisons ce nom dans le COPY instruction où nous voulons copier uniquement les fichiers nécessaires à l’image finale.

Le résultat est une image finale plus mince pour notre application :

$ docker images
REPOSITORY    TAG      IMAGE ID       CREATED         SIZE
digicactus       latest   70a92e92f3b5   2 hours ago     991MB
multistage    latest   e598271edefa   6 minutes ago   197MB
…

Dans cet exemple, nous nous appuyons sur l’ option –user de pip. En effet, cela permet d’installer les dépendances dans le répertoire utilisateur local et copier ce répertoire dans l’image finale. Cependant, il existe d’autres solutions telles que virtualenv. Cela permet la construction de packages sous forme de roues, et ainsi les copier et les installer dans l’image finale.

Exécutez le conteneur

Une fois le Dockerfile écrit puis l’image construite, nous pouvons exécuter le conteneur avec notre service Python.

$ docker images
REPOSITORY   TAG      IMAGE ID       CREATED       SIZE
digicactus      latest   70a92e92f3b5   2 hours ago   991MB
...

$ docker ps
CONTAINER ID   IMAGE   COMMAND   CREATED   STATUS   PORTS   NAMES

$ docker run -d -p 5000:5000 digicactus
befb1477c1c7fc31e8e8bb8459fe05bcbdee2df417ae1d7c1d37f371b6fbf77f

Nous avons maintenant conteneurisé notre serveur hello world et nous pouvons interroger le port mappé sur localhost.

$ docker ps
CONTAINER     ID        IMAGE        COMMAND        PORTS                   ...
befb1477c1c7  digicactus   "/bin/sh -c  'python ..."   0.0.0.0:5000->5000/tcp  ...

$ curl http://localhost:5000
"Hello Digicactus!"

Et après ?

Cet article vous a montré comment conteneuriser un service Python pour une meilleure expérience de développement. 

Grâce à la conteneurisation, les résultats sont facilement reproductibles. On évite également des conflits de dépendances. De plus, cela permet également de conserver un environnement de développement standard propre. L’ environnement de développement conteneurisé est d’une part facile à gérer et d’autre part à partager avec d’autres développeurs : Il se déploie facilement sans aucune modification de leur environnement standard.  

Le prochain article de cette série sera consacré à la mise en place d’un projet multi-services basé sur des conteneurs. Nous vous ferons découvrir la connexion entre le composant Python et les autres composants externes. Nous vous apprendrons aussi comment gérer le cycle de vie de tous ces composants de projet avec Docker Compose.

Ressources

About Oussama ABAI

One thought on “Conteneuriser vos applications Python – Part 1

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.

Résoudre : *
21 ⁄ 1 =