Technologie

Comment j’ai conçu un déploiement sélectif par service avec Jenkins et Kubernetes

AAnonymous
10 min de lecture

Introduction

Cet article fait suite à Comment j’ai transformé le backend monolithique d’Omnilude en MSA avec l’aide de l’IA. Si l’article précédent expliquait pourquoi j’ai séparé les frontières entre services, celui-ci s’intéresse à la manière dont ces frontières sont devenues de véritables unités de déploiement.

Quand on parle de MSA, on s’arrête facilement au nombre de services créés. Pourtant, le moment où l’exploitation change vraiment de texture n’arrive pas quand l’arborescence du code change, mais quand l’unité de déploiement change.

Dans ce billet, je veux résumer quels problèmes la structure actuelle a réduits, comment elle fonctionne et ce qu’il reste encore à compléter.

Pourquoi la séparation du déploiement a été plus tangible que la séparation du code

Quand tout vivait encore dans un grand backend, le point le plus pénible n’était pas seulement le mélange des fonctionnalités. Le vrai problème était qu’une petite modification finissait toujours par se transformer en pression pour redéployer l’ensemble.

Même si je ne touchais qu’à une fonctionnalité du blog, je devais reconstruire toute l’application, recréer l’image et remettre en ligne le même gros bloc. Dans une structure de ce type, les zones qui changent souvent perdent d’abord leur vitesse d’expérimentation. Plus grave encore, les services qui doivent maintenir des connexions longues se retrouvaient eux aussi dans le même rayon d’impact.

Par exemple, un service comme backbone-service, proche de WebSocket, SSE et des flux d’événements, se dégrade immédiatement en exploitation s’il subit la même pression qu’un changement fonctionnel ordinaire. Pour moi, la séparation des services a donc fini par relever moins de la question de savoir qui possède quelle API que de celle de savoir ce qui peut être déployé indépendamment.

Comment Jenkins choisit les cibles de déploiement

L’exploitation des services repose sur Kubernetes, et Jenkins prend en charge le CI/CD.

Dans ce texte, je m’intéresse moins aux détails d’implémentation qu’à la façon dont j’ai découpé les frontières de déploiement du point de vue CI/CD.

Dans le pipeline actuel, le point de départ du déploiement est BUILD_MODE accompagné de paramètres booléens par service. Avec all, je peux tout déployer. En mode auto, je peux déployer uniquement les services cochés manuellement, ou laisser le pipeline détecter les cibles à partir des changements Git si rien n’est sélectionné. Autrement dit, je peux faire des déploiements complets ou partiels manuellement, et les déclenchements automatiques peuvent réduire la cible à partir des fichiers modifiés.

Les services sélectionnables sont auth-service, backbone-service, storage-service, ai-service, game-service, blog-service et legacy-service. Le dépôt reste unique, mais les cibles de déploiement sont choisies dès le départ au niveau du service.

La logique de détection automatique est elle aussi assez simple. Elle regarde git diff et rattache les changements sous modules/{service} au service correspondant. Il y a toutefois une exception importante. Si le module partagé common(jspring) change, le pipeline considère que le socle commun a bougé et reconstruit tous les services. Cette règle évite que les changements dans common soient masqués par un déploiement partiel.

Je considère ce point comme essentiel. Le déploiement sélectif est séduisant parce qu’il permet de déplacer uniquement ce qui est nécessaire, rapidement. Mais si l’on sous-estime les changements de contrats partagés, cela peut devenir plus dangereux.

Les artefacts sont eux aussi séparés par service

Le flux réel de build suit la même logique. Je commence par publier common dans le Maven local, puis je construis uniquement les services sélectionnés, de manière séquentielle, jusqu’à bootJar. Le choix d’un build séquentiel plutôt que parallèle était pragmatique. À ce stade, il était plus stable de réduire les problèmes de génération des classes Q et les conflits du Kotlin daemon.

La construction des images se fait dans un conteneur kaniko. Cette étape est elle aussi séquentielle. Si l’on ne regarde que la vitesse, le parallèle peut paraître plus impressionnant, mais au stade actuel d’Omnilude, il était plus pertinent de réduire les conflits de ressources et de rendre les points d’échec plus lisibles.

Les noms d’image sont également mappés par service. Par exemple, auth-service devient jongmin-auth, ai-service devient jongmin-ai, et blog-service devient jongmin-blog. Ainsi, l’unité de déploiement reste lisible dans le registre, et il devient plus simple de suivre un service précis par la suite.

La structure des dossiers suit aussi l’unité d’exploitation

Si Jenkins choisit ce qu’il faut construire, Kubernetes fixe ce résultat en véritables unités d’exploitation. La structure actuelle est kubernetes/services/{service}/{dev|prd}. Chaque service possède son propre Deployment et son propre Service, et l’ingress est géré séparément par environnement sous ingress/{env}.

L’avantage de cette structure est sa facilité d’explication. Si l’on veut comprendre comment un service est déployé, il suffit de regarder le dossier de ce service. Comme la frontière du code et celle des manifests coïncident dans l’ensemble, la responsabilité devient aussi moins floue d’un point de vue opérationnel.

Le routage suit la même logique. Sous dev-api.omnilude.com et api.omnilude.com, chaque service est raccordé par path prefix. En d’autres termes, la séparation des services n’est pas seulement un concept interne au dépôt. Elle s’étend jusqu’à la propriété réelle des API et au routage ingress.

Les dispositifs de stabilité de base s’ajoutent également à ce niveau. Chaque Deployment de service inclut déjà readinessProbe et livenessProbe, et certains services de production disposent aussi de HPA, PDB et topology spread constraint. Tous les services ne disposent pas encore du même niveau de garde-fous opérationnels, mais je peux désormais au moins gérer séparément jusqu’où chaque service est allé et quels mécanismes de stabilité ont déjà été appliqués.

Garder un flux de déploiement simple

Le flux actuel de déploiement est plus linéaire qu’il n’en a l’air.

  • Jenkins décide quels services seront déployés dans cette exécution.
  • Seuls les services sélectionnés sont construits et transformés en images.
  • kubectl apply applique les manifests de chaque service.
  • kubectl rollout restart lance la nouvelle version.
  • kubectl rollout status attend que chaque service soit bien remonté.
  • Les états de démarrage, de succès et d’échec sont envoyés sur Slack.

Ce qui compte ici, ce n’est pas l’effet spectaculaire, mais la lisibilité. À ce stade, je préfère un flux où n’importe qui peut comprendre clairement ce qui a été déployé, où cela a échoué et ce que le système attend. Des stratégies plus complexes pourront être ajoutées plus tard, mais si la structure actuelle est déjà difficile à expliquer, l’exploitation risque de devenir moins stable au lieu de l’être davantage.

Ce qui a changé après l’avoir mis en place

Le plus grand changement apporté par cette structure de déploiement sélectif, c’est qu’une petite modification n’impose plus un redéploiement complet. ai-service, où la vitesse d’expérimentation compte beaucoup, blog-service, où je veux améliorer rapidement les fonctionnalités produit, et le travail côté jeu, relativement indépendant, peuvent désormais avancer à des rythmes différents.

L’autre gain, c’est la capacité à l’expliquer. Si quelqu’un me demande comment Omnilude est exploité aujourd’hui, je peux maintenant relier en une seule ligne la liste des services, les noms d’image, les dossiers Kubernetes, les chemins d’ingress et le flux de notifications Slack. Même pour un projet personnel, je pense que cette explicabilité compte beaucoup. Une structure d’exploitation ne doit pas seulement fonctionner. Elle doit aussi pouvoir expliquer pourquoi elle est séparée de cette manière, afin que l’étape suivante d’amélioration puisse continuer.

Cela dit, la structure actuelle n’est pas achevée. le rollback automatique, les smoke tests après déploiement et les gates de déploiement basés sur les métriques manquent encore. Pour l’instant, le centre de gravité reste la vérification du rollout et le résumé Slack.

Conclusion

La structure actuelle ne mérite pas encore le nom de grande plateforme CI/CD, mais au moins le principe d’un dépôt unique avec un déploiement découpé par service est relié de manière cohérente depuis les paramètres Jenkins jusqu’aux noms d’image, aux dossiers Kubernetes, aux chemins d’ingress et aux notifications Slack.

La prochaine étape est claire. Je dois ajouter de vrais garde-fous sur cette base, comme le rollback automatique, les smoke tests et de meilleures alertes d’exploitation. Cette partie est encore en cours, et j’y reviendrai dans un prochain article une fois l’implémentation terminée.