Les deux hypothèses porteuses de cette série sont testables dès aujourd'hui, avec un service managé, en un après-midi. AWS Fault Injection Service livre un scénario nommé "AZ Availability: Power Interruption" qui arrête chaque instance étiquetée d'une zone de disponibilité, envoie des erreurs InsufficientInstanceCapacity aux groupes Auto Scaling qui tentent de les remplacer, met en pause l'IO des volumes EBS et coupe le trafic du sous-réseau, puis lève les pannes et redémarre ce qu'il a arrêté. L'hypothèse du plan de contrôle de la Partie 3 n'a pas de panne prête à l'emploi, mais une approximation honnête de celle-ci tient dans un fichier JSON et un refus IAM. Presque personne n'exécute ni l'un ni l'autre, et la raison n'est pas l'outillage ; la raison est qu'un test peut échouer et pas une hypothèse.

Ceci est la Partie 4 de Nothing Fails Alone, la dernière. La Partie 1 a pris l'incident us-east-1 de mai et montré la frontière de zone en train de tenir tandis qu'un quorum mourait à l'intérieur. La Partie 2 a pris l'événement me-central-1 de mars et montré une seule cause atteignant deux zones à dix-huit heures d'écart, au-delà de toute garantie d'isolation. La Partie 3 a pris trois incidents de plan de contrôle chez deux fournisseurs et montré des plans de reprise mourant avec la couche sur laquelle ils s'exécutent. La colonne vertébrale de la série, en deux phrases : la disponibilité que vous achetez s'arrête à la frontière de ce que le fournisseur a promis, et tout ce qui est au-dessus de cette ligne est de l'architecture qui vous appartient. Ce que vous possédez et n'exercez jamais n'est pas de l'architecture, c'est une hypothèse à votre nom. Cette partie l'exerce, puis note la série.

L'exercice d'AZ est disponible sur étagère

L'AZ Availability: Power Interruption de la bibliothèque de scénarios FIS est un template d'expérience publié qui induit les symptômes documentés de la perte d'une zone, et son JSON publié porte huit actions sur sept types de pannes (l'arrêt d'instances apparaît deux fois, une fois pour les instances autonomes et une fois pour celles des groupes Auto Scaling). Les instances étiquetées en cours d'exécution dans la zone cible sont arrêtées et redémarrées après la durée configurée. Les demandes de lancement des groupes Auto Scaling étiquetés reçoivent InsufficientInstanceCapacity pendant la durée, et il en va de même pour les appels de capacité de tout rôle IAM que vous nommez, parce que, selon la page du scénario, "EC2 API calls made by the Auto Scaling control plane to recover lost capacity in the AZ will fail" pendant une vraie interruption d'alimentation. L'IO des volumes EBS est mise en pause. Le trafic du sous-réseau est refusé en échangeant une network ACL clonée pleine de règles de refus. Les clusters RDS avec un writer dans la zone sont basculés, et les groupes de réplication ElastiCache voient l'alimentation de leur zone interrompue. C'est l'essentiel de ce que l'incident de mai a fait à use1-az4 : la défaillance qui a mis Coinbase à terre pour une nuit est désormais un élément de bibliothèque.

Soyez précis sur ce qu'il n'est pas. Les instances sont arrêtées par la grande porte, l'API StopInstances : un arrêt ordonné, pas une coupure de courant, donc rien de la cohérence en cas de crash n'est testé. Les tâches Fargate ne sont pas couvertes. Les clusters RDS Multi-AZ avec deux standbys lisibles ne sont pas pris en charge. Une note de dérive : la page du scénario décrit une action de récupération, aws:arc:start-zonal-autoshift, que son propre instantané JSON ne contient pas ; la bibliothèque de la console porte la révision la plus récente, et le lab suit le JSON publié.

Le lab est 04-game-day dans le dépôt de la série, et il enfreint la règle read-only de la série pour la deuxième et dernière fois : --inject démarre de vraies expériences contre le compte que vos identifiants atteignent, donc il a sa place dans un sandbox, pointé vers la stack du lab de la Partie 3, et nulle part près de la production.

Noms, ID, et une condition d'arrêt livrée vide

La Partie 2 a fait grand cas des noms de zone contre les ID de zone, et le game day est là où ce cas paie. Les rapports d'incident parlent en ID : le rapport de mai disait use1-az4, les mises à jour de mars disaient mec1-az2 puis mec1-az3. Votre compte parle en noms, des alias qu'AWS randomise par compte. Et l'outil, pour l'essentiel, exige le nom. Les cibles d'instances et de sous-réseaux filtrent sur Placement.AvailabilityZone, qui correspond à votre nom local au compte, et le scénario publié remplit lui aussi les paramètres de cible EBS, RDS et ElastiCache avec le nom. Exactement deux actions acceptent l'ID physique : la paire de capacité insuffisante, aws:ec2:api-insufficient-instance-capacity-error et aws:ec2:asg-insufficient-instance-capacity-error, dont le paramètre availabilityZoneIdentifiers est documenté comme acceptant les ID de zone aussi bien que les noms. Même là, le scénario publié remplit le champ avec un nom, us-east-1a.

Un game day commence donc par le problème de traduction pour lequel la Partie 2 a livré un lab. Le runner le résout une fois, en tête : le seul flag de zone que run.py accepte est --az-id, la zone physique, la chose qu'un rapport d'incident nomme. Il résout le nom local au compte via DescribeAvailabilityZones, imprime les deux, et remplit chaque emplacement avec la forme que son consommateur exige. Vous répétez la défaillance sous le nom qu'un rapport d'incident lui donnerait, et la répétition se configure elle-même dans le dialecte que votre compte se trouve parler.

La deuxième chose que le runner refuse d'hériter est plus discrète. Le bloc de condition d'arrêt du scénario publié ressemble à ceci, mot pour mot :

"stopConditions": [
    {
        "source": "aws:cloudwatch:alarm",
        "value": ""
    }
]

La forme d'un garde-fou sans rien dedans, et la section limitations de la page dit que les conditions d'arrêt sont à vous d'ajouter. Un défaut défendable, puisque AWS ne peut pas connaître votre état stable ; mais aussi un défaut piégé, parce que le JSON se colle proprement et qu'une chaîne vide ne ressemble pas à un système de sécurité manquant. La position du lab : run.py sort avant de créer quoi que ce soit si --alarm-arn n'est pas fourni, parce qu'une expérience sans condition d'arrêt significative n'est pas un test, c'est un pari. L'état stable est un nombre auquel vous vous engagez avant la panne, sinon le game day ne peut pas échouer, et un game day qui ne peut pas échouer ne peut rien vous apprendre.

À quoi ressemble une réussite, mesurée

Exécutez l'expérience contre la stack à deux chemins de la Partie 3, en faisant tomber l'une de ses deux zones :

python3 run.py experiments/az-power-interruption.json \
  --az-id euw1-az1 \
  --fis-role-arn arn:aws:iam::111111111111:role/fis-game-day \
  --alarm-arn arn:aws:cloudwatch:eu-west-1:111111111111:alarm:game-day-abort \
  --target-group-arn ... \
  --inject

Sans --inject, la même commande est un dry run : elle imprime le template résolu et ne démarre rien. Avec lui, le runner crée et démarre l'expérience, puis interroge jusqu'à un état terminal, journalisant le statut de chaque action et échantillonnant le compte d'instances saines du target group comme mesure. Cette chronologie est le livrable ; ce qui suit est la forme d'une réussite, pas un résultat que j'ai obtenu, parce que je n'ai pas de compte à l'échelle d'une production pour le mériter. Le chemin B, le statiquement stable, ne descend jamais sous son exigence post-défaillance de deux cibles saines, puisque la zone survivante porte déjà la charge. Le chemin A tombe à la moitié et y reste toute la durée de la panne, parce que l'expérience même qui a arrêté ses instances alimente ses lancements de remplacement en InsufficientInstanceCapacity. Le script de mesure de la Partie 3 a chronométré la reprise du chemin A contre un plan de contrôle coopératif ; le game day montre le même chemin contre un plan non coopératif, et la différence entre ces deux nombres est la taille de l'hypothèse que votre plan de DR faisait.

Quatre nombres méritent d'être enregistrés. Le temps jusqu'à la capacité rétablie, à partir des échantillons de cibles saines. Le nombre d'étapes du runbook qui nécessitaient un appel de plan de contrôle qui aurait échoué, compté en parcourant votre runbook pendant la fenêtre de panne. L'écart entre le nombre coopératif de la Partie 3 et le nombre non coopératif du game day. Et le sort de chaque constat du détecteur de la Partie 1, puisque chaque ligne qu'il imprime est une prédiction que le game day peut trancher : le groupe Auto Scaling single-AZ laisse bien sa capacité en rade derrière une erreur de capacité ; le sous-réseau de zone saine dont la route par défaut traverse vers la zone défaillante perd bien son trafic sortant. Un constat de détecteur est une hypothèse. Un game day est l'expérience.

L'exercice de plan de contrôle que FIS ne peut pas vous donner

L'hypothèse de la Partie 3 est plus difficile, et ici la phrase honnête vient en premier : FIS n'a pas d'action de perte de plan de contrôle. La référence complète des actions ne contient jamais cette expression, et rien en elle ne retire un plan de contrôle. Les plus proches sont des injections d'erreur limitées aux appelants que vous nommez : la paire de capacité insuffisante ci-dessus, et un trio d'actions d'erreur d'API génériques (internal, throttle, unavailable) qui injectent des défaillances dans les requêtes faites par des rôles IAM nommés, pour les seuls namespaces EC2 et Kinesis. Le plan reste debout ; les appelants choisis se voient dire non. Rien de ce que vous pouvez louer ne reproduit la console qui sert des 504 pendant que STS échoue à trois reprises distinctes.

Donc le deuxième template du lab est une approximation et est étiqueté comme telle, dans une clé approximation de premier niveau à l'intérieur du fichier lui-même, pour que l'étiquette ne puisse pas dériver de la chose qu'elle étiquette. La moitié FIS injecte InsufficientInstanceCapacity sur les lancements des ASG de la charge et sur les appels de capacité du rôle de déploiement, RunInstances, CreateFleet, StartInstances, CreateCapacityReservation, dans la zone défaillante pendant vingt minutes. L'autre moitié n'est pas du tout du FIS : un refus IAM manuel, limité et borné dans le temps sur ec2:RunInstances, autoscaling:* et cloudformation:*, ce qui transforme l'étape « il suffit de pousser la stack d'urgence » du runbook en la défaillance qu'elle aurait été en juin 2023. run.py imprime les commandes d'application et de retrait et ne les exécute jamais, la borne de temps est décidée avant que la politique soit attachée, et le refus ne va jamais sur le rôle dont vous aurez besoin pour le retirer. Cette dernière règle n'est pas de la pédanterie ; c'est la différence entre un exercice et un incident.

Ce que l'approximation ne reproduit pas, d'après la propre liste du fichier : une vraie dégradation de plan de contrôle est de la latence et du brownout, rarement une erreur nette ; des services que le fichier ne nomme jamais, ELB, Route 53, IAM lui-même, la console, continuent de fonctionner ici et pourraient ne pas le faire lors d'un vrai événement ; les lectures continuent de réussir, donc vos dashboards paraissent en meilleure santé qu'ils ne le seraient ; les mutations hors capacité comme TerminateInstances et les changements de configuration de load balancer continuent de réussir, donc ceci refuse la création de capacité, pas le plan de contrôle ; et c'est la capacité d'une seule zone, là où un vrai événement peut être régional. Mesurez une chose de plus sur cet exercice : le temps de détection. Combien de temps a-t-il fallu à un humain pour remarquer que le correctif ne marchait pas ? Si quelqu'un vous dit qu'il a simulé une perte de plan de contrôle, demandez lesquels de ces cinq écarts il a couverts. Le mien n'en couvre aucun non plus ; il couvre l'affirmation plus étroite selon laquelle votre architecture n'a pas besoin des appels refusés pendant la première heure.

Noter la série

Quatre incidents, quatre labs, une colonne vertébrale. Ce que la série a établi, tenu à son propre standard : une affirmation ne compte que si elle repose sur les mots publiés de l'opérateur. Premièrement, l'isolation de zone est réelle contre les pannes pour lesquelles elle est conçue : l'événement thermique de mai est resté à l'intérieur de use1-az4, et l'architecture qui a échoué cette nuit-là a échoué de l'aveu de son propre postmortem, pas de celui d'AWS. Deuxièmement, les défaillances de zone ne sont pas des événements indépendants : une seule cause a atteint mec1-az2 et mec1-az3 à dix-huit heures d'écart, et deux zones perdues ont poussé les services régionaux au-delà de leur tolérance déclarée d'une zone. Troisièmement, le plan de contrôle échoue tandis que le plan de données continue de tourner, chez plus d'un fournisseur, et les plans de reprise qui sont des opérations de plan de contrôle échouent avec lui : 2023 et octobre 2025 selon les propres post-event summaries d'AWS, février 2026 selon celui d'Azure.

Maintenant l'autre colonne, parce qu'une rétrospective qui ne compte que les victoires est du marketing. Ce qui reste une hypothèse : tout ce qui est quantitatif à votre sujet. Je n'ai jamais exécuté ces labs contre une flotte de production, donc chaque nombre de la série est la forme d'une sortie, honnêtement étiquetée mais toujours une forme. Le coût en ruée (thundering herd) d'un basculement de masse, les "longer than usual provisioning times" d'AWS, apparaît dans deux incidents et n'a jamais reçu de nombre, ni d'AWS ni de moi. La prime de stabilité statique de la Partie 3 était de l'arithmétique, pas une facture d'une vraie flotte. Et savoir si votre chemin break-glass fonctionne est inconnaissable d'ici.

Ce que je ferais différemment : exécuter le game day en premier, pas en dernier. La série a ordonné ses labs pédagogiquement, détecter, traduire, mesurer, injecter, et cet ordre est à l'envers d'un point de vue opérationnel : une seule exécution d'injection produit la liste de constats que le détecteur ne peut que prédire. Et j'aurais dû me méfier plus tôt des sources secondaires. La règle, ne citer que la propre page de l'opérateur, a continué de payer jusqu'au bout : elle a rattrapé de mauvaises durées, un horodatage décalé d'une demi-journée, et des affirmations dont les sources apparentes, une fois consultées, ne les contenaient pas. Une série sur les hypothèses non testées a failli en livrer plusieurs, ce qui est à peu près la démonstration la plus nette de sa propre thèse que j'aurais pu arranger.

Les game days échouent aussi

L'instrument mérite le même scepticisme que la chose qu'il mesure, alors terminons sur les trois façons dont un game day vous ment.

  • Le test qui prouve seulement que le test s'est exécuté. FIS résout les cibles par tag, et un décalage de tag ne fait pas échouer l'expérience : le scénario d'AZ ignore les actions dont les cibles ne se résolvent en rien, et le lab garde ce comportement (emptyTargetResolutionMode: skip) pour que des stacks partielles puissent s'exécuter. Le prix : une expérience peut atteindre completed sans rien avoir arrêté. Résultat vert, zéro panne injectée. Lisez le tableau par action, pas la ligne de statut ; le template de plan de contrôle met fail à la place, car là une cible vide rend l'exécution inutile. Les codes de sortie sont d'accord : completed sort avec 0, une expérience stopped sort avec 1 même si s'arrêter signifie que votre alarme a fonctionné. Le garde-fou qui se déclenche, c'est le garde-fou qui fonctionne, et ce n'est toujours pas une réussite.
  • La condition d'arrêt qui se déclenche trop tôt. Liez l'alarme à une métrique interne avec un seuil serré et l'expérience s'arrête au bout de deux minutes, avant la défaillance intéressante. Vous n'apprenez que le fait que l'alarme se déclenche, et la tentation ensuite est de la relâcher ou de la retirer, ce qui est la façon dont on revient à la chaîne vide du scénario par choix. Le correctif n'est pas moins de garde-fou ; c'est de lier l'alarme à la métrique d'état stable visible par l'utilisateur, au seuil où vous voudriez qu'un vrai incident soit arrêté.
  • Le compte qui n'est pas la production. Une réussite en sandbox prouve le mécanisme, pas le résultat. Pas de trafic de production, pas de gravité des données, pas de voisins bruyants, pas d'opérateur réveillé à 3 heures du matin. Ajoutez la propre douceur de l'injection, arrêts ordonnés, erreurs nettes au lieu de brownouts, et la chaîne de preuves honnête se lit ainsi : le détecteur prédit, le game day en sandbox teste le mécanisme de la prédiction, et la production hérite d'une extrapolation étiquetée. C'est bien mieux que rien et bien moins qu'une preuve, et prétendre le contraire est la façon dont « on a fait du chaos engineering » devient une hypothèse non testée de plus.

C'est là que la série se termine. Rien ne défaille seul : chaque partie a trouvé la défaillance enchevêtrée avec quelque chose à côté d'elle, un quorum avec un bâtiment, une zone avec une guerre, un correctif avec un plan de contrôle. Rien ne réussit seul non plus. Chaque réussite est enchevêtrée avec l'environnement qui l'a produite, et l'étiquette qui le dit est la ligne la plus porteuse du rapport.

À lire ensuite

Pour du conseil en AWS, architecture cloud, revues de résilience et travail de plateforme, commencez par ercanermis.com.

Références