Je me suis lancé dans un side project pour gérer les rétrospectives de mon équipe, une application que je construis depuis plusieurs mois et dont j’ai décidé de raconter les coulisses de la création.
- 1/X - L’architecture technique
- 2/X - Le harness
- 3/X - Les tests automatisés
- 4/X - L’infrastructure (cet article)
Avant l’IA, travailler sur un projet Web ne présentait pas de difficulté particulière: il suffisait de lancer les serveurs de développement qui étaient généralement directement intégrés aux frameworks. Mais lorsque l’on souhaite travailler avec des agents en parallèle, sans que ces derniers ne se marchent dessus, cela devient plus complexe. Je vais dans ce billet expliquer les décisions qui ont été prises pour permettre de gérer cette infrastructure en réutilisant un maximum de briques entre le développement local et le déploiement du projet en production.
Pour permettre à plusieurs agents de travailler simultanément sur des fonctionnalités différentes, il est nécessaire d’avoir plusieurs copies du projet dans des dossiers distincts. Pour cela, j’utilise sans surprise des worktree Git. Mais ce n’est qu’une partie du problème, car il faut ensuite que chaque worktree dispose de sa propre instance Web.
Afin d’y parvenir, j’ai conteneurisé le projet avec Docker. Chaque worktree dispose ainsi de ses propres conteneurs, identifiés par un slug et accessibles via une URL qui leur est dédiée: https://<slug>.retro.localhost. Les conteneurs sont orchestrés via Docker Compose.
Un reverse proxy (Traefik) est utilisé pour router les URLs vers les conteneurs FrankenPHP qui font tourner l’application. Chaque instance ne démarre pas une stack complète: certains services tels que la base de données (PostgreSQL), Redis ou encore Mailpit sont mutualisés. Un script dédié bin/worktree m’aide à gérer les worktree et les services qui leur sont associés. Une stack d’observabilité basée sur OpenTelemetry existe en local, même si cette dernière n’est pas réellement utilisée en production.
Redis est optionnel, mais il est utilisé sur le serveur de production pour permettre de faire des déploiements sans interruption de service. Son rôle est de stocker les sessions PHP ainsi que des données de cache.
Le code source du projet est principalement hébergé sur GitHub (et répliqué sur des services français et européens). L’intégration continue repose sur GitHub Actions. Néanmoins, elle nécessite beaucoup de ressources et le quota autorisé est rapidement atteint. Je fais donc tourner des runners GitHub Actions sur un serveur dédié, entièrement géré et provisionné au travers d’Ansible. La configuration GitHub Actions du projet me permet de passer des runners auto-hébergés aux runners managés en modifiant une seule ligne.
Une fois que l’on a tout ça, on peut concevoir et développer le projet avec un environnement de CI qui assure la sécurité de ce que l’on souhaite livrer. À ce stade, il ne reste plus qu’à déployer le projet en production avec deux contraintes: réutiliser un maximum de briques mises en place pour le développement local et avoir une gestion simple et automatisée des certificats SSL en utilisant Let’s Encrypt.
Quand on a un projet dockerisé, on pense tout de suite à Kubernetes. Mais c’est un écosystème que je connais très mal et au moment où je souhaite mettre l’outil à disposition, il faut également se rendre compte qu’il y a un besoin réel pour l’équipe dans laquelle je travaille. J’ai donc recherché des solutions autour de Docker Compose. Il en existe assez peu et la plupart des recherches ramènent à Docker Swarm. Pour moi, il s’agissait d’un vieil outil qui n’était plus réellement maintenu et qui n’était même plus mis en avant par les équipes de Docker.
C’est néanmoins la solution qui coche toutes les cases. Une configuration qui ressemble énormément à Docker Compose. Une utilisation simple, qui nécessite peu de ressources et ne pose aucun problème sur une instance mono-serveur. Et puis aucun arrêt du développement n’a été annoncé et le projet continue de recevoir des commits. Alors commençons simplement, et on verra avec le temps.
C’est donc un nœud Swarm qui fait actuellement tourner le projet, hébergé sur un VPS Scaleway. Ce dernier est composé des services ci-dessous:
| Service | Rôle |
|---|---|
caddy |
Le point d’entrée du trafic HTTP. |
php |
Instance applicative tournant avec FrankenPHP (deux réplicas). |
worker |
Un worker de messages pour le traitement des événements asynchrones. |
database |
La base de données relationnelle (PostgreSQL). |
redis |
Cache et sessions. |
mercure |
Hub pour la gestion des communications temps réel. |
Vous noterez que seul le service PHP est répliqué sur la même machine. Cela permet d’assurer la continuité de service lors des déploiements, qui nécessitent le redémarrage du conteneur PHP. Il est également important que les migrations de base de données soient rétrocompatibles avec la version précédente du code, sans quoi on pourrait avoir des erreurs applicatives le temps de la mise à jour du code PHP.
Des endpoints de type healthcheck sont disponibles dans l’application et utilisés par Swarm lors des déploiements pour s’assurer que tout est démarré et fonctionnel.
Parmi les outils que je n’ai pas encore cités: