Dans cet article, nous allons revenir sur un talk présenté lors de l’AFUP, par Smaïne MILIANI qui nous a présenté une méthode très intéressante pour déployer sans interruption de service : le ZDD, soit Zero Downtime Deployment.
Nous nous concentrerons sur son procédé le plus répandu soit le blue/green deployment. A l’instar du speech, nous n’aborderons que le point de vue du développeur, pas de l’ops même si son rôle est important avec la mise en place de cette méthode. Sans rentrer dans le détail, il implique côté serveur d’avoir deux environnements identiques : un « bleu » correspondant à la version courante de l’application et un autre, « vert », dans lequel la mise à jour sera déployée. Le load balancer n’a plus qu’à cibler l’environnement « vert » pour exposer la nouvelle version aux utilisateurs et peux facilement rebasculer sur l’environnement « bleu » si un rollback est nécessaire.
Quelle est cette méthode de déploiement ?
Cette méthode de déploiement permet de livrer ses applications sans avoir à interrompre ou dégrader le service.
L’idée est de prévoir le développement des applications pour leur permettre de fonctionner avec leurs sources de données et dépendances à la fois dans leurs nouvelles versions (n+1) mais également avec leurs versions actuelles ou précédentes (n).
En cas de détection de régression, chaque dépendance doit pouvoir être restaurée sans impacter le bon fonctionnement des autres éléments du système et sans avoir besoin de les restaurer en cascade. Par exemple, l’application déployée doit pouvoir fonctionner avec la base de donnée mise à jour mais aussi avec sa version avant modification (dump pré-déploiement). Elle doit également pouvoir consommer les versions précédentes des APIs qu’elle exploite.
Pour réaliser cette façon de faire, il faut prévoir un déploiement en trois étapes :
- une étape de préparation
- Par exemple, consommation/enregistrement des données sans contrainte forte selon l’ancienne _et_ la nouvelle structure (ajout de table sans clé étrangère, réponse API avec nouveaux et anciens attributs, colonnes nullables…)
- le déploiement de la fonctionnalité
- Activation des contraintes fortes, consommation/publication des nouveaux attributs uniquement
- le nettoyage du code devenu inutile
- étape importante pour éviter le code mort et le stockage de données obsolètes
Quelques exemples concrets :
- Les changements de structure en base de données implique un procédé intermédiaire :
- Ajouter une colonne obligatoire nécessite de créer dans un premier temps, la colonne nullable en préparation, la consommer via un DTO en attribut nullable dans une seconde étape puis de contraindre la colonne en base et le DTO dans un troisième déploiement.
- Rendre une colonne obligatoire nécessite de rendre la colonne non nulle avant de contraindre le DTO pour ne pas générer une erreur en cas de rollback sur les données nulles.
- Ajouter une nouvelle clé étrangère implique de créer la table sans relation dans un premier temps puis ajouter la clé étrangère dans un deuxième déploiement.
- Pour une suppression de colonne en base de donnée, il faut au préalable rendre la colonne et le DTO associé nullable en base et dans le code avant de pouvoir supprimer définitivement la colonne.
- Les changements dans les APIs nécessite de les versionner et de pourvoir consommer les différentes versions des APIs en même temps. Par exemple, pour renommer une url d’API, il faudra être capable de publier/consommer les deux endpoints de façon interchangeable.
- L’utilisation de workers asynchrones comme RabbitMQ oblige a créer des jobs qui peuvent être exécutés dans la version actuelle de l’application mais aussi dans la version précédente.
Pourquoi cette méthode ?
Cette méthode a été mise en place pour répondre à une problématique : déployer les applications sans mise en maintenance ou détérioration de service et donc sans frustration utilisateur !
Elle prend tout son sens avec des applications micro-services et à forte affluence. Il faut pouvoir revenir en arrière suite à une régression très rapidement sans que l’utilisateur ne s’en rende compte.
Elle permet également de livrer des fonctionnalités progressivement afin de limiter les risques. La première fonctionnalité est livrée, activée et vérifiée avant de déployer les suivantes.
Quels sont les inconvénients de cette solution ?
Ce principe vient aussi avec son lot de contraintes :
- formation des équipes
- chaque fonctionnalité nécessite 2 à 3 déploiements
- engendre le risque de code mort dans le cas où la troisième étape de déploiement n’est pas respectée
- complexification du code et des migrations
- complexification de l’hébergement et de l’exploitation
Pour aller plus loin…
En complément du talk de Smaïne, nous avons approfondi les recherches autour de ce concept avec les articles dont vous pouvez retrouver les liens en base de cet article (dont un dans lequel Smaïne rentre également plus dans le détail).
Nous avons donc étudié les impacts pour nos clients et comment le mettre en place sur nos projets. Sachant que nous avons récemment rencontré une problématique similaire pour un de nos clients : nous avons développé pour lui une application hybride qui est déployée dans des boutiques partout dans le monde dont le déploiement se fait unitairement et à des délais très variables. Il faut donc que les APIs soient fonctionnelles pour la nouvelle version déployée sur certaines boutiques mais également pour l’ancienne version toujours en place chez les autres. Nous avons donc mis les principes de la ZDD en place.
Cependant, ce principe de déploiement est est très coûteux et pas forcément adapté si cela n’est pas nécessaire. Il implique un coût de formation des ressources : il faut les former à ces principes de migrations en plusieurs étapes et amène son lot d’erreurs si l’utilisateur n’est pas consciencieux. Pour ce dernier point nous avons évidement des analyseurs syntaxiques pour les repérer… Surtout quand les projets ne sont pas toujours gérés par les mêmes équipes, la ZDD complexifie la passation. Il a également un impact sur l’hébergement et l’exploitation. Il faut dupliquer les serveurs de production et même préproduction pour que la compatibilité entre versions soit testée en amont. Sans parler de l’orchestration et de la gestion des logs qu’il faut bien cibler pour identifier les problèmes dans toutes les situations.
D’autres alternatives… plus ou moins complexes
Dans la famille des ZDD, d’autres pattern sont disponibles, comme par exemple le Canary release qui permet de migrer un pourcentage restreint de la population sur la nouvelle version de l’application puis le reste après vérification des logs. Cela permet de limiter le test des nouvelles fonctionnalités à un certain type d’utilisateur ou à un petit pourcentage. D’autres solutions sont disponibles comme le rolling Deployment ou le Dark Launch dont vous trouverez les détails dans les liens référencés.
Sans aller jusqu’au ZDD, nous avions déjà l’habitude de versionner nos APIs. Notamment pour les applications mobiles grand public pour lesquelles les utilisateurs ne mettent pas forcément à jour leurs applications, il faut donc conserver les anciennes versions d’APIs.
Le procédé feature flag est également une bonne alternative pour développer des fonctionnalités sans les rendre visibles par les utilisateurs. Il consiste à déployer une fonctionnalité cachée mais accessible depuis un paramètre activable par l’administrateur dans le BO.
Mais chez nous, tous nos projets sont en déploiement automatique et récurrent, ils se font déjà sans interruption de service et passent toujours bien, pas besoin de prévoir de rollback 😉
Quelques liens utiles sur le sujet :
https://smaine-milianni.medium.com/le-z%C3%A9ro-downtime-deployment-74eb9112be3d
https://blog.octo.com/zero-downtime-deployment
https://inapp.com/blog/how-to-achieve-zero-downtime-deployment-a-journey-towards-uninterrupted-software-updates Article rédigé par Guillaume Meot