Tester du Python dans un docker sans prise de front
Quand on développe, il est très probable qu’il y ait des différences entre votre environnement de travail et l’environnement du conteneur construit à partir du Dockerfile. Et souvent, ces différences font qu’un code qui s’exécute correctement localement sur votre machine, crash dans le conteneur Docker. Alors, comment faire pour corriger rapidement et sans vous prendre le front votre conteneur ?
Toute l’astuce consiste à combiner une librairie nommée watchdog avec la possibilité de monter un volume dans ledit docker.
Contexte
La phase de développement se fait généralement hors conteneur et dans un environnement (pyenv / conda) dédié.
Une fois le développement fini, on est ensuite amené à tester notre code dans un conteneur Docker qui sera, par la suite, déployé en production.
Avant d’aller plus avant dans les détails, il faut bien garder en mémoire que le Dockerfile décrit comment builder (construire) une image fonctionnelle de votre code qui fonctionnera partout de la même manière, peu importe la machine ou l’OS de base qui l’exécutera. En complément, on parle de conteneur (container) quand une image a été instanciée (démarrée). Un conteneur, c’est l’image “en vie”.
Tester votre code dans un docker
Généralement, pour tester votre code dans un docker, vous allez être amené à builder une image et à l’exécuter :
docker build -t nom-de-mon-app .
docker run nom-de-mon-app
Ensuite, vous devrez tester si votre code a le même comportement dans le conteneur que ce que vous aviez localement sur votre machine.
Si ce n’est pas le cas, vous allez devoir corriger le code localement chez vous, rebuilder puis tester de nouveau votre application.
Un premier pas pour se simplifier la vie, serait de supprimer l’étape de build qui n’est utile que dans le cas où on ajoute / supprime des dépendances.
Pour cela rien de plus simple ! Mettons que dans mon docker, le dossier de mon application s’appelle app. Normalement, après le build, ce dossier est une copie intégrale du dossier éponyme se trouvant localement sur ma machine. Toutefois, si je le souhaite, au moment d’instancier un conteneur, je peux substituer le dossier app du docker, par le dossier de mon projet local. Pour cela, il suffit juste de monter un volume dans le conteneur, au moment de son démarrage.
En exemple avec le répertoire courant :
docker run -p 8000:8000 -w /app -v "C:/Chemin_vers_mon_repertoire_local:/app" -t nom-de-mon-app .
Ici, dans le docker, le dossier app est substitué par le dossier local de ma machine. Cette commande a créé un lien entre le répertoire /app du docker et mon répertoire courant de travail localement sur machine. On appelle cela le bind mount.
Ainsi, si je modifie du code localement sur ma machine, je n’ai plus besoin de rebuilder l’image à chaque fois, il me suffit juste d’exécuter à nouveau la commande précédemment écrite !
Attention, cela ne dispense pas du test final qui consiste à rebuilder entièrement l’image et à l’exécuter sans le volume monté.
Aller plus loin avec la librairie watchdog
Maintenant que vous avez compris l’astuce que je vous ai présenté dans la section précédente, ne serait-ce pas génial si nous n’avions qu’à exécuter qu’une fois la commande docker run ?
Watchdog est une librairie qui permet de recharger du code quand une modification de fichiers est détecté dans un répertoire.
Certains frameworks, comme uvicorn l’intègre nativement à l’aide du flag --reload.
Donc, si votre application utilise uvicorn, combiner l’astuce de monter votre projet dans le conteneur avec l’utilisation du flag –reload, fera que le code qui s’exécute dans votre docker se rechargera automatiquement à chaque modification de votre répertoire local ! Et voilà.
À présent, libre à toi d’utiliser directement watchdog pour arriver au même résultat si tu n’utilises pas déjà quelque chose qui s’en sert.
Voici l’exemple par défaut :
import time
from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler
def on_created(event):
print(f"hey, {event.src_path} a été créé!")
def on_deleted(event):
print(f"hey, {event.src_path} a été supprimé!")
def on_modified(event):
print(f"hey, {event.src_path} a été modifié!")
def on_moved(event):
print(f"hey, {event.src_path} a été déplacé vers {event.dest_path}")
if __name__ == "__main__":
patterns = ["*"]
ignore_patterns = None
ignore_directories = False
case_sensitive = True
my_event_handler = PatternMatchingEventHandler(patterns, ignore_patterns, ignore_directories, case_sensitive)
# Lier nos fonctions aux évènements
my_event_handler.on_created = on_created
my_event_handler.on_deleted = on_deleted
my_event_handler.on_modified = on_modified
my_event_handler.on_moved = on_moved
# Créer un observeur
path = "."
recursive = True
mon_observateur = Observer()
mon_observateur.schedule(my_event_handler, path, recursive=recursive)
# Exécuter l'observateur
mon_observateur.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
mon_observateur.stop()
mon_observateur.join()
Le mot de la fin
Voilà, maintenant, vous devriez pouvoir déboguer vos conteneurs docker comme des boss !
Si tu as des questions, les commentaires sont là pour ça ! Et si tu veux que je traite un sujet, n’hésites pas à m’écrire.