Retour de terrain Publié le · 8 min de lecture

Notre bascule n'avait jamais fonctionné, et tout était au vert

Une paire VRRP qui n'a pas déplacé son adresse flottante pendant deux mois, et les deux autres contrôles menteurs trouvés le même jour.

Nous appliquions des correctifs de noyau. Travail ennuyeux et nécessaire : nos mises à jour automatiques installent les correctifs de sécurité du noyau mais ne redémarrent jamais, si bien que le parc se déclarait entièrement à jour tout en exécutant une image vieille de plusieurs mois. Tout redémarrer, par vagues, prudemment.

Au milieu de cette opération, nous avons découvert que notre paire de haute disponibilité n’avait jamais fonctionné. Pas une seule fois.

Deux couches, dont une seule existait vraiment

Une adresse IP flottante chez un fournisseur cloud n’a rien à voir avec une IP virtuelle sur votre propre commutateur. Il y a deux couches, et les deux doivent se produire.

La première est locale : keepalived ajoute l’adresse sur l’interface du nœud qui détient le rôle MASTER. La seconde est chez le fournisseur : un appel d’API demande à son réseau de router cette adresse vers cette machine. Sans la seconde, le nouveau MASTER a bien l’adresse configurée et ne reçoit rien, parce que le fournisseur continue d’envoyer les paquets vers l’ancien.

Notre script faisait la première. Il n’avait jamais fait la seconde.

Trouvé par accident, et c’est tout le problème

Le redémarrage du nœud de secours a produit une fenêtre de trois secondes pendant laquelle il a brièvement revendiqué MASTER avant d’entendre l’annonce du nœud principal. Ce battement a déclenché le script, et pour la première fois quelqu’un regardait au moment où il s’exécutait.

Le journal disait :

Becoming MASTER, assigning floating IP to <nœud>
Resolved FIP ID: <id>

Puis plus rien. Aucune ligne d’affectation. Aucune erreur. Le script s’arrêtait, simplement.

Il résolvait son propre serveur par son nom :

GET /v1/servers?name=<nom-court>

Notre fournisseur préfixe les noms de serveurs par l’environnement. Le nom court ne correspondait à rien, l’API répondait très poliment HTTP 200 avec une liste vide, l’analyseur en ligne levait une exception, et set -euo pipefail tuait le script une ligne avant l’appel qui comptait.

Le fichier de journal montrait le même couple de lignes tronquées pour chaque transition MASTER, sur les deux nœuds, depuis deux mois. Six transitions. Silencieux à chaque fois.

Pourquoi rien ne l’a détecté

C’est la partie qui mérite qu’on s’y arrête. Nous avions de la supervision. La paire paraissait saine sous tous les angles dont nous disposions :

  • keepalived tournait sur les deux nœuds.
  • Exactement un nœud détenait l’adresse.
  • ip addr sur le MASTER montrait bien l’adresse flottante.
  • Aucune alerte ne s’était jamais déclenchée.

Chacune de ces observations était vraie. Aucune ne testait ce qui était cassé. La liaison locale est ce que l’on vérifie naturellement, et la liaison locale a toujours fonctionné. La panne vivait entièrement dans une couche que rien ne regardait.

Et une bascule l’aurait révélée en quelques secondes. Nous n’en avions jamais fait. Non par décision, mais parce que rien n’obligeait jamais à poser la question.

Le correctif a failli aggraver les choses

Le correctif évident consiste à cesser de résoudre par nom. Le service de métadonnées du fournisseur donne à une machine son propre identifiant, sans jeton et sans convention de nommage susceptible de dériver :

curl -s http://169.254.169.254/<fournisseur>/v1/metadata/instance-id

Cette partie était facile. Le problème, c’était ce que le correctif aurait fait tout seul.

Rappelez-vous le battement de trois secondes. Un nœud de secours qui démarre à côté d’un nœud principal vivant attend environ trois intervalles d’annonce, n’entend rien parce que la pile réseau n’est pas encore prête, et se promeut lui-même. Il redescend un instant plus tard. Historiquement sans conséquence, puisque le script était cassé.

Avec un script fonctionnel, ce battement confie l’adresse flottante à un nœud sur le point de rétrograder. Et rien ne la restitue, parce que le vrai MASTER ne change pas d’état et ne rejoue donc jamais le script. Notre adresse de service se serait retrouvée garée sur le nœud de secours, définitivement, et le « correctif » aurait provoqué une panne pire que le défaut.

Le script attend donc maintenant quelques secondes et revérifie qu’il détient toujours l’adresse avant de toucher à l’API. Séparément, nous retardons le démarrage de VRRP pour que le battement cesse largement de se produire. La garde reste quand même : elle couvre les promotions réelles mais brèves, que le délai de démarrage ne peut pas traiter.

Puis nous en avons trouvé deux autres de la même forme

Le même jour, la même classe de problème, des mécanismes différents.

Un contrôle incapable d’échouer. Notre vérification après redémarrage demandait si WireGuard était revenu avec une clé :

wg show wg0 public-key   # code de sortie 0, affiche "(none)"

Le contrôle testait un code de sortie nul et une sortie non vide. (none) satisfait les deux. Il déclarait le VPN sain sur un hôte dont le tunnel n’avait aucune clé privée et dont tous les pairs étaient à zéro établissement de liaison. Ce contrôle n’avait jamais eu la capacité de rendre un verdict négatif sur quoi que ce soit.

Des règles jamais chargées. Deux règles de détection supprimant un faux positif connu se trouvaient dans un groupe analysé avant celui définissant leur règle parente. Le moteur résout les références dans l’ordre du fichier, ne trouvait pas la parente, et ignorait silencieusement les deux. Elles étaient présentes dans le fichier. Elles se relisaient correctement. Elles ne s’étaient jamais déclenchées, et les faux positifs qu’elles existent pour éviter se déclenchaient depuis des semaines.

Le moteur le disait. À chaque démarrage :

Signature ID '<parente>' was not found ... in the 'if_sid' option of rule '<fille>'
Empty 'if_sid' value. Rule '<fille>' will be ignored.

Rien dans notre processus de déploiement n’obligeait à lire cette sortie. Nous écrivions le fichier, redémarrions le service, et passions à autre chose.

Trois façons de produire une assurance fausse

Mises côte à côte, la logique est plus claire que chacune prise isolément :

Mode de défaillance
Le script de basculeMuet. Mourait sans rien journaliser.
Le contrôle du VPNMenteur. Structurellement incapable d’un verdict négatif.
Les règles de détectionIgnoré. Disait exactement ce qui n’allait pas, dans le vide.

Des mécanismes différents, un résultat identique : un système qui annonce une santé qu’il n’a pas. Et un contrôle qui ne peut pas échouer est pire que pas de contrôle du tout, parce qu’il consomme l’attention que vous auriez autrement dirigée vers le problème.

Ce que nous avons réellement changé

Pas seulement les trois défauts. Les conditions qui leur ont permis de vivre.

Exercer la panne, à intervalle régulier. Nous avons désormais un playbook qui effectue une vraie bascule et la vérifie auprès de l’API du fournisseur, et non de l’interface locale, puisque c’est précisément l’interface locale qui mentait. Il refuse de s’exécuter sur une paire déjà dégradée, se pilote depuis le nœud qui reste debout, et redémarre keepalived dans un bloc always afin qu’aucune défaillance ne puisse laisser le nœud principal sans lui.

Nous l’exécutons mensuellement, à la main, et nous avons délibérément refusé d’en faire une tâche planifiée. Une bascule non surveillée qui tourne mal à trois heures du matin sur une infrastructure que personne ne regarde est un problème pire que celui qu’elle prévient. L’exercice dure une minute. La détection continue est un mécanisme différent, et celui-là tourne bien tout seul.

Rendre le silence impossible. Les deux scripts journalisent maintenant une ligne fatale lorsqu’ils ne peuvent pas faire leur travail, et nous avons ajouté des règles d’alerte qui se déclenchent sur ces lignes. Un script qui échoue bruyamment dans un fichier que personne ne lit, c’est le même problème une couche plus loin.

Passer par le validateur. Les déploiements de règles exécutent désormais la validation du moteur avant tout redémarrage, et restaurent la version précédente en cas de rejet. Cette porte s’est justifiée immédiatement en interceptant une règle invalide de notre part dès la première tentative. C’est aussi ainsi que nous avons trouvé les règles jamais chargées : les avertissements étaient là depuis toujours, dans une sortie que rien ne nous obligeait à regarder.

Tester les contrôles contre la panne, pas contre la santé. Chacun de ces contrôles avait été validé sur un système fonctionnel, où il rendait la réponse attendue, ce qui ne prouve rien. La question à poser d’un contrôle n’est pas « passe-t-il quand tout va bien » mais « l’ai-je déjà vu échouer une fois ».

La partie inconfortable

Nous avons trouvé tout cela en faisant autre chose. Des correctifs de noyau, sur un parc qui fonctionnait très bien.

Nous ne l’avons pas trouvé parce que notre supervision était bonne. Sur ce point précis, notre supervision était pire qu’inutile : elle était activement rassurante. Nous l’avons trouvé parce qu’un redémarrage a créé par hasard une fenêtre de trois secondes où une chose cassée est devenue visible, et parce que quelqu’un lisait la sortie à cet instant.

Ce n’est pas une stratégie de détection reproductible. C’est pourquoi les correctifs qui comptent ne sont pas les trois rustines. Ce sont l’exercice, la porte de validation, et l’habitude de demander, devant chaque voyant vert, ce qu’il faudrait pour qu’il passe un jour au rouge.

haute-disponibilitekeepaliveddetection-engineeringsupervisionincident