Une republication ratée ne supprime jamais un backend qui fonctionne
Le 25 septembre, une republication ratée a supprimé le backend d'une application qui était en ligne et fonctionnait depuis environ deux heures et demie. Le code qui annule une publication en échec a retiré toutes les applications serveur que la publication avait touchées, y compris celles qui servaient déjà avant son lancement. Désormais, seules les applications créées par une publication peuvent être supprimées ; une application déjà existante est remise sur la version exacte qu'elle exécutait auparavant. Deux correctifs de suivi le même jour ont fait en sorte que cette annulation s'exécute aussi lorsque l'outil de déploiement de la plateforme d'hébergement échoue, et qu'une application saine ne soit pas déclarée en échec simplement parce qu'elle était en veille.
Comment un backend est publié
Lorsque votre application Mythex a un backend, c'est-à-dire une API ou un serveur et pas seulement des fichiers statiques, la publication le construit à partir d'un Dockerfile (une recette qui empaquette l'application dans une image) puis déploie cette image sur des machines de notre plateforme d'hébergement. Un contrôle de santé attend ensuite que la nouvelle version démarre réellement et tourne de façon stable avant que la publication soit marquée comme en ligne.
Si quoi que ce soit échoue en cours de route, une restauration est lancée pour tout défaire. Bien réussir cette restauration compte plus que presque tout le reste dans la publication, car elle s'exécute précisément quand quelque chose tourne déjà mal.
Problème 1 : la restauration supprimait ce qu'elle aurait dû rétablir
La restauration tenait une liste des applications serveur sur lesquelles une publication avait déployé, et en cas d'échec, les détruisait toutes. C'est correct pour une première publication : l'application vient d'être créée, rien ne dépend d'elle, et la supprimer remet les choses dans leur état initial.
C'est exactement faux pour une republication. L'application existait déjà et servait de vrais visiteurs. Le 25 septembre, l'API d'une application qui était en ligne depuis environ deux heures et demie a été supprimée par une republication ratée.
Une deuxième défaillance se cachait dans le même code. Lorsque la suppression n'aboutissait pas, la nouvelle image défectueuse restait sur les machines, alors que nous indiquions au propriétaire que son site actuel était toujours en ligne. Au moment où le contrôle de santé peut échouer, la nouvelle image a déjà été déployée sur toutes les machines. Ne rien faire n'est pas la même chose que défaire.
La correction : ne détruire que ce qu'on a créé, restaurer le reste
Chaque publication enregistre désormais, pour chaque application serveur qu'elle touche :
- si cette publication l'a créée, et
- si ce n'est pas le cas, quelle image elle exécutait juste avant.
En cas d'échec, chaque application reçoit l'un de trois traitements :
- Créée par cette publication : on la détruit, comme auparavant.
- Déjà existante, image précédente connue : on redéploie l'image précédente avec la même configuration. Pas de construction : seule l'image revient en arrière.
- Déjà existante, image précédente inconnue : on n'y touche pas. Une application existante n'est jamais supprimée, même lorsqu'on ne peut pas la restaurer.
Ces règles tiennent dans une petite fonction pure, avec ses propres tests : la décision ne peut donc pas retomber dans « tout détruire » sans qu'un test échoue.
Problème 2 : l'outil de déploiement pouvait échouer avant qu'on ait noté comment défaire
Peu après cette correction, nous avons trouvé une faille. L'outil de déploiement de la plateforme exécute ses propres vérifications de fumée : si les nouvelles machines plantent, il sort en erreur. Mais il le fait après que la nouvelle image est déjà sur les machines.
Notre code n'enregistrait une application pour la restauration qu'une fois l'outil revenu avec succès. Lorsque l'outil lui-même faisait échouer le déploiement, l'application n'était pas encore sur la liste de restauration, donc rien n'était rétabli. Sur notre environnement de préproduction, une republication cassée a laissé l'image défectueuse en service.
La correction : enregistrer l'annulation avant de commencer
L'application est désormais inscrite pour la restauration, avec l'information de savoir si elle a été créée et quelle image elle exécutait, dès qu'elle est préparée et avant que quoi que ce soit y soit déployé. Quoi qu'il échoue ensuite, la restauration sait ce qu'il faut remettre. Nous l'avons vérifié en local : une republication cassée a échoué dans les vérifications de fumée de l'outil de déploiement, et la version précédente a continué de répondre.
Problème 3 : une application en veille était déclarée « jamais démarrée »
Les applications publiées sur Mythex se mettent en veille lorsqu'elles sont inactives, pour économiser vos crédits. Une republication laisse une machine en veille arrêtée, donc le contrôle de santé doit la réveiller. Il demandait à la plateforme de démarrer la machine une seule fois, et ignorait la réponse.
Le 25 septembre, cette unique demande de démarrage n'a abouti à rien. Une API saine est restée arrêtée pendant toute la durée des cinq minutes du contrôle de santé, puis a été déclarée en échec avec le message « 1 machine jamais démarrée (dernier état : arrêtée) ».
La plateforme refuse parfois de démarrer une machine. Pendant nos tests, elle a renvoyé un conflit « lease currently held » sans que nous le provoquions, et elle refuse les démarrages pendant qu'une machine est en cours de mise à jour.
La correction : insister, et frapper à la porte
Le contrôle de santé fait maintenant ceci :
- il redemande de démarrer une machine arrêtée toutes les 20 secondes pendant l'attente,
- il journalise la réponse de la plateforme lorsqu'elle refuse, et
- il envoie une requête ordinaire à l'application, comme le ferait un visiteur : le proxy de la plateforme réveille une machine en veille dès qu'une requête arrive.
Nous avons reproduit le problème sur la vraie plateforme avec une application jetable dont le premier démarrage était refusé. Avant la correction, la publication échouait au bout de 91 secondes. Après, elle était publiée en 20 secondes. Une application qui plante réellement au démarrage échoue toujours, comme il se doit.
Ce que nous en retenons
- La restauration doit distinguer le nouveau de l'existant. « Annuler » quelque chose que l'on a créé veut dire supprimer. « Annuler » quelque chose qui était déjà là veut dire restaurer. Traiter les deux de la même façon, c'est ainsi qu'un mécanisme de sécurité supprime une application qui fonctionne.
- Notez comment défaire avant d'agir. Si l'annulation n'est enregistrée qu'après la réussite d'une étape, une étape qui échoue à mi-chemin ne laisse rien avec quoi annuler.
- Ne jamais ignorer la réponse à une requête dont vous dépendez. Un « non » ignoré a transformé une application saine en publication échouée.
Une correction liée, du même jour : détecter les applications qui plantent juste après la publication. Pour ce qui se passe lorsqu'une publication échoue de votre côté, consultez le dépannage de la publication dans la documentation.