Projet Dremmvro : le déploiement
Quelques jours semaines après le premier article introductif du projet Dremmvro, voici le deuxième de la série !
Cela fait quelque temps que je n’ai pas eu le temps d’avancer sur le projet, mais la série d’articles a du retard, alors j’ai des choses à raconter.
Aujourd’hui nous allons parler du déploiement !
C’est quoi le plan ?
J’avais en tête plusieurs objectifs et contraintes quand j’ai commencé le projet.
Déjà, je voulais faire du déploiement continu, pour pouvoir montrer à Marine les évolutions au fur et à mesure.
Et donc, l’idée était de mettre à jour automatiquement la production quand la branche main est mise à jour.
Deuxième objectif : faire tourner une suite de tests automatiques.
On a par contre une petit contrainte : j’autohéberge ladite production, c’est-à-dire que le site tourne tourne sur une machine à mon domicile. Souvent, les grosses entreprises d’hébergement proposent des outils clé en main pour faire ce déploiement continu ; il a fallu que je me débrouille.
La CI
(à prononcer à l’anglaise, "scie aïe")
Dans un premier temps j’ai mis en place l’outil d’intégration continue, qu’on appelle CI dans le jargon. Le code est hébergé chez Codeberg, alors j’aurais pu profiter de leur Forgejo actions ou de leur instance Woodpecker. Mais pour ne pas trop peser sur leurs serveurs, j’ai décidé d’héberger mon propre Woodpecker et de le connecter à Codeberg.
Woodpecker, est un outil d’intégration continue, analogue à Jenkins par exemple.
Ça s’installe et se connecte facilement.
Et après on bidouille un peu pour configurer : ça se passe dans un fichier .woodpecker.yml ou plusieurs fichiers dans le répertoire .woodpecker/, qu’on place à la racine du dépôt.
Par exemple, voici ce que contient le fichier de configuration qui lance la suite de tests :
when:
- event: pull_request
- event: push
branch: main
steps:
- name: test
image: ghcr.io/astral-sh/uv:python3.14-alpine
environment:
DJANGO_PG_DBNAME: dremmvro
DJANGO_PG_HOST: postgres
DJANGO_PG_PORT: 5432
DJANGO_PG_USER: postgres
DJANGO_PG_PASSWORD: password
commands:
- apk add --no-cache make
- make test
services:
- name: postgres
image: postgres:18
environment:
POSTGRES_PASSWORD: password
ports:
- 5432
Le premier bloc, when, permet d’indiquer à Woodpecker qu’il faut exécuter cette action à chaque modification de pull request, ainsi qu’à chaque fois qu’on pousse sur la branche main.
Le deuxième bloc décrit les étapes de l’action : ici on va lancer la commande make test après avoir installé make dans le conteneur lancé à partir d’une image qui contient l’utilitaire uv.
Et on donne quelques variables d’environnement pour configurer la base de données, ce qui permet de tester "en conditions réelles", ou en tout cas assez proches du réel.
Et justement, le dernier bloc configure un service, celui de la base de données.
Le déploiement
Pour déployer, j’ai un deuxième fichier Woodpecker :
when:
- event: push
branch: main
steps:
- name: build and publish
image: woodpeckerci/plugin-docker-buildx
settings:
repo: codeberg.org/${CI_REPO_OWNER}/${CI_REPO_NAME}
tag: latest
registry: codeberg.org
username: ${CI_REPO_OWNER}
password:
from_secret: REGISTRY_TOKEN
build_args:
DJANGO_GIT_COMMIT: ${CI_COMMIT_SHA}
- name: redeploy
image: alpine:latest
environment:
HOOK_SERVER:
from_secret: HOOK_SERVER
TOKEN:
from_secret: TOKEN
SERVICE:
from_secret: SERVICE
commands:
- apk add curl
- 'curl --request POST $${HOOK_SERVER}/pull -H "Authorization: Bearer $${TOKEN}" --data "repo=codeberg.org/$${CI_REPO_OWNER}/$${CI_REPO_NAME}&tag=latest"'
- 'curl --request POST $${HOOK_SERVER}/service_update -H "Authorization: Bearer $${TOKEN}" --data "service=$${SERVICE}&image=codeberg.org/$${CI_REPO_OWNER}/$${CI_REPO_NAME}:latest"'
Celui-ci fait deux choses :
- il génère une image Docker à partir du dépôt, j’utilise un plugin qui simplifie l’action. Une fois l’image construite, elle est envoyée sur Codeberg, qui sert également de registre de paquets. On voit d’ailleurs que le jeton pour s’y connecté est passé par le secret
REGISTRY_TOKEN, qu’on renseigne dans les paramètres du dépôt dans Woodpecker. - il déploie la mise à jour en prod !
Pour cette dernière étape, j’ai imaginé plusieurs solutions :
- un cron côté serveur qui regarde à intervalle régulier si le dépôt a changé : bof, soit on fait plein de requêtes inutiles, soit on attend trop longtemps entre le commit poussé sur
mainet le déploiement, soit les deux. - une action de la CI qui se connecte au serveur de production et qui fait différentes actions : récupérer l’image Docker puis relancer le service
Mais exécuter des commandes directement en prod depuis la CI, ce n’est pas très propre.
J’ai opté pour une 3e solution, qui a requis un peu plus de développement.
On voit dans l’action Woodpecker que j’utilise curl pour effectuer deux requêtes.
Ce sont en fait des webhooks (liens de rappel HTTP en bon français), qui indiquent au serveur de prod qu’il y a une mise à jour à déployer.
N’ayant pas trouvé d’outil existant permettant de télécharger une image Docker et la mettre en prod, j’ai développé une petite application en Flask pour le faire. Le nom est nul, mais le code est fonctionnel et couvre mes besoins : simple-hook-docker-deploy.
L’application attend de recevoir une requête POST avec quelques informations : principalement un en-tête pour s’authentifier, et une action.
Elle utilise docker-py, une bibliothèque Python qui permet de piloter Docker, et peut donc réaliser l’action : télécharger une image ou bien mettre à jour le service.
Et hop ! Si tout se passe bien, la production est à jour. 🎉
Docker Swarm
Mes services autohébergés tournent grâce à Docker et Docker Compose. Quand j’ai commencé, c’était pas mal à la mode et quand même facile à mettre en place.
Malheureusement, docker-py ne gère pas Docker Compose !
Après quelques recherches j’ai vu que la bibliothèque gérait les services, et j’ai creusé dans cette direction.
Et je me suis demandé pourquoi est-ce que ne n’avais pas utilisé Docker Swarm plus tôt !
Avec un fichier presque identique au docker-compose.yml, on a un ensemble de services, qui peuvent permettre de faire du déploiement sans mettre hors-ligne le service (zero downtime deployment), même avec une seule machine.
Il suffit d’avoir deux conteneurs instanciés pour le même service.
Ainsi, docker-py permet de mettre à jour un service avec une nouvelle image : on appelle Docker Swarm qui s’occupe d’éteindre un conteneur, d’en créer un nouveau avec l’image mise à jour, puis de faire la même chose avec l’autre conteneur initial.
Et ça juste marche ! (à peu près, j’ai l’impression qu’il peut y avoir quelques secondes hors-ligne, mais c’est tout à fait convenable pour moi)
Conclusion
J’ai donc un petit environnement sympa pour développer et déployer Dremmvro.
Je travaille sur une branche, je pousse mes modifications. Ces modifications sont testées avec la suite de tests (que je lance aussi en local).
Une fois que la branche est fusionnée dans main, une image est créée, envoyée dans le registre de Codeberg, puis télécharée et déployée en prod.
Y’a plus qu’à développer les améliorations petit à petit !