As duas premissas de sustentação desta série são testáveis hoje, com um serviço gerenciado, em uma tarde. O AWS Fault Injection Service traz um cenário chamado "AZ Availability: Power Interruption" que vai parar toda instância marcada com tag em uma zona de disponibilidade, alimentar erros InsufficientInstanceCapacity aos Auto Scaling groups tentando repô-las, pausar o IO de volumes EBS e cortar o tráfego de subnet, e então suspender as falhas e reiniciar o que parou. A premissa de control plane da Parte 3 não tem falha de prateleira, mas uma aproximação honesta dela cabe em um arquivo JSON e um deny de IAM. Quase ninguém roda nenhuma das duas, e a razão não é ferramental; a razão é que um teste pode falhar e uma premissa não pode.

Esta é a Parte 4 de Nothing Fails Alone, a última. A Parte 1 pegou o incidente de maio em us-east-1 e mostrou a fronteira de zona aguentando enquanto um quórum morria dentro dela. A Parte 2 pegou o evento de março em me-central-1 e mostrou uma causa alcançando duas zonas com dezoito horas de diferença, para além de toda garantia de isolamento. A Parte 3 pegou três incidentes de control plane em dois provedores e mostrou planos de recuperação morrendo com a camada em que rodam. A espinha da série, em duas frases: a disponibilidade que você compra termina na fronteira do que o provedor prometeu, e tudo acima dessa linha é arquitetura que é sua. O que é seu e você nunca exercita não é arquitetura, é uma premissa com o seu nome nela. Esta parte a exercita, e depois pontua a série.

O drill de AZ vem pronto da prateleira

O AZ Availability: Power Interruption da biblioteca de cenários do FIS é um template de experimento publicado que induz os sintomas documentados de perder uma zona, e seu JSON publicado carrega oito ações através de sete tipos de falha (parar instâncias aparece duas vezes, uma para instâncias avulsas e uma para os próprios Auto Scaling groups). Instâncias em execução marcadas com tag na zona-alvo são paradas e reiniciadas após a duração configurada. Requisições de lançamento de Auto Scaling groups marcados com tag recebem InsufficientInstanceCapacity durante a duração, e o mesmo acontece com as chamadas de capacidade de quaisquer roles IAM que você nomeie, porque, segundo a página do cenário, "EC2 API calls made by the Auto Scaling control plane to recover lost capacity in the AZ will fail" durante uma interrupção de energia real. O IO de volumes EBS é pausado. O tráfego de subnet é negado trocando por um network ACL clonado cheio de regras de deny. Clusters RDS com um writer na zona sofrem failover, e replication groups do ElastiCache têm a energia da sua zona interrompida. Isso é a maior parte do que o incidente de maio fez com use1-az4: a falha que derrubou a Coinbase por uma noite agora é um item de biblioteca.

Seja preciso sobre o que ele não é. As instâncias são paradas pela porta da frente, a API StopInstances: um desligamento ordenado, não um corte de energia, então nada sobre consistência em caso de crash é testado. Tarefas do Fargate não são cobertas. Clusters RDS Multi-AZ com dois standbys legíveis não são suportados. Uma nota de drift: a página do cenário descreve uma ação de recuperação, aws:arc:start-zonal-autoshift, que o seu próprio snapshot JSON não contém; a biblioteca do console carrega a revisão mais nova, e o lab segue o JSON publicado.

O lab é 04-game-day no repositório da série, e ele quebra a regra de somente leitura da série pela segunda e última vez: --inject inicia experimentos reais contra qualquer conta que suas credenciais alcancem, então ele pertence a um sandbox, apontado para a stack de lab da Parte 3, e em lugar nenhum perto de produção.

Nomes, IDs e uma condição de parada entregue vazia

A Parte 2 fez alarde sobre nomes de zona versus IDs de zona, e o game day é onde o alarde compensa. Relatórios de incidente falam em IDs: o relatório de maio disse use1-az4, as atualizações de março disseram mec1-az2 e depois mec1-az3. Sua conta fala em nomes, aliases que a AWS randomiza por conta. E a ferramenta, na maior parte, exige o nome. Os alvos de instância e subnet filtram por Placement.AvailabilityZone, que casa com o nome local da sua conta, e o cenário publicado preenche os parâmetros de alvo de EBS, RDS e ElastiCache com o nome também. Exatamente duas ações aceitam o ID físico: o par de capacidade insuficiente, aws:ec2:api-insufficient-instance-capacity-error e aws:ec2:asg-insufficient-instance-capacity-error, cujo parâmetro availabilityZoneIdentifiers é documentado como aceitando IDs de zona além de nomes. Mesmo ali, o cenário publicado preenche o campo com um nome, us-east-1a.

Um game day, portanto, começa com o problema de tradução para o qual a Parte 2 entregou um lab. O runner o resolve uma vez, no topo: o único flag de zona que o run.py aceita é --az-id, a zona física, a coisa que um relatório de incidente nomeia. Ele resolve o nome local da conta via DescribeAvailabilityZones, imprime ambos e preenche cada placeholder com a forma que seu consumidor exige. Você ensaia a falha sob o nome que um relatório de incidente daria a ela, e o ensaio se configura no dialeto que a sua conta por acaso fala.

A segunda coisa que o runner se recusa a herdar é mais silenciosa. O bloco de condição de parada do cenário publicado se parece com isto, literalmente:

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

O formato de um guardrail sem nada dentro, e a seção de limitações da página diz que as condições de parada são suas para adicionar. Um padrão defensável, já que a AWS não pode saber o seu estado estável; também um padrão traiçoeiro, porque o JSON cola limpinho e uma string vazia não parece um sistema de segurança faltando. A posição do lab: o run.py sai antes de criar qualquer coisa se --alarm-arn não for fornecido, porque um experimento sem uma condição de parada significativa não é um teste, é uma aposta. Estado estável é um número com o qual você se compromete antes da falha, ou o game day não pode falhar, e um game day que não pode falhar não pode te dizer nada.

A cara de uma aprovação, medida

Rode o experimento contra a stack de dois caminhos da Parte 3, derrubando uma de suas duas zonas:

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

Sem --inject o mesmo comando é um dry run: ele imprime o template resolvido e não inicia nada. Com ele, o runner cria e inicia o experimento, então faz polling até um estado terminal, registrando o status de cada ação e amostrando a contagem saudável do target group como a medição. Essa linha do tempo é o entregável; o que segue é a cara de uma aprovação, não um resultado que eu tenha obtido, porque não tenho uma conta em formato de produção para conquistá-lo. O Caminho B, o estaticamente estável, nunca cai abaixo do seu requisito pós-falha de dois alvos saudáveis, já que a zona sobrevivente já carrega a carga. O Caminho A cai para a metade e fica lá durante toda a duração da falha, porque o mesmo experimento que parou suas instâncias está alimentando seus lançamentos de reposição com InsufficientInstanceCapacity. O script de medição da Parte 3 cronometrou a recuperação do Caminho A contra um control plane cooperativo; o game day mostra o mesmo caminho contra um não cooperativo, e a diferença entre esses dois números é o tamanho da premissa que o seu plano de DR estava fazendo.

Quatro números valem o registro. Tempo até a capacidade recuperada, a partir das amostras de alvos saudáveis. A contagem de passos de runbook que precisavam de uma chamada de control plane que teria falhado, contada percorrendo seu runbook durante a janela da falha. A lacuna entre o número cooperativo da Parte 3 e o número não cooperativo do game day. E o destino de cada achado do detector da Parte 1, já que cada linha que ele imprime é uma previsão que o game day pode resolver: o Auto Scaling group single-AZ de fato encalha sua capacidade atrás de um erro de capacidade; a subnet da zona saudável cuja rota padrão cruza para a zona que falhou de fato perde a saída. Um achado do detector é uma hipótese. Um game day é o experimento.

O drill de control plane que o FIS não pode te dar

A premissa da Parte 3 é mais difícil, e aqui a frase honesta vem primeiro: o FIS não tem ação de perda de control plane. A referência completa de ações nunca contém a expressão, e nada nela tira um control plane. O mais próximo são injeções de erro com escopo nos chamadores que você nomeia: o par de capacidade insuficiente acima, e um trio de ações genéricas de erro de API (internal, throttle, unavailable) injetando falhas em requisições feitas por roles IAM nomeados, apenas para os namespaces EC2 e Kinesis. O plano continua de pé; chamadores escolhidos recebem um não. Nada que você possa alugar reproduz o console servindo 504s enquanto o STS falha três vezes distintas.

Então o segundo template do lab é uma aproximação e é rotulado como uma, em uma chave approximation de nível superior dentro do próprio arquivo, para que o rótulo não possa se separar da coisa que ele rotula. A metade FIS injeta InsufficientInstanceCapacity nos lançamentos dos ASGs de carga e nas chamadas de capacidade do deploy role, RunInstances, CreateFleet, StartInstances, CreateCapacityReservation, na zona que falhou por vinte minutos. A outra metade não é FIS de jeito nenhum: um deny de IAM manual, com escopo e prazo definido em ec2:RunInstances, autoscaling:* e cloudformation:*, que é o que transforma o passo "é só empurrar a stack de emergência" do runbook na falha que ele teria sido em junho de 2023. O run.py imprime os comandos de aplicar e remover e nunca os executa, o prazo é decidido antes de a política ser anexada, e o deny nunca vai no role que você vai precisar para removê-lo. Essa última regra não é preciosismo; é a diferença entre um drill e um incidente.

O que a aproximação não reproduz, da própria lista do arquivo: degradação real de control plane é latência e brownout, raramente um erro limpo; serviços que o arquivo nunca nomeia, ELB, Route 53, o próprio IAM, o console, continuam funcionando aqui e podem não funcionar em um evento real; leituras continuam tendo sucesso, então seus dashboards parecem mais saudáveis do que estariam; mutações que não são de capacidade, como TerminateInstances e mudanças de configuração de load balancer, continuam tendo sucesso, então isto nega criação de capacidade, não o control plane; e é a capacidade de uma zona, onde um evento real pode ser regional. Meça uma coisa a mais neste drill: tempo até detectar. Quanto tempo um humano levou para notar que a correção não estava funcionando? Se alguém te disser que simulou perda de control plane, pergunte qual dessas cinco lacunas ele cobriu. A minha não cobre nenhuma delas também; ela cobre a afirmação mais estreita de que a sua arquitetura não precisa das chamadas negadas na primeira hora.

Pontuando a série

Quatro incidentes, quatro labs, uma espinha. O que a série estabeleceu, mantido no seu próprio padrão: uma afirmação só conta se repousar nas palavras publicadas do operador. Primeiro, o isolamento de zona é real contra as falhas para as quais é projetado: o evento térmico de maio ficou dentro de use1-az4, e a arquitetura que falhou naquela noite falhou por admissão do próprio postmortem dela, não da AWS. Segundo, falhas de zona não são eventos independentes: uma causa alcançou mec1-az2 e mec1-az3 com dezoito horas de diferença, e duas zonas perdidas levaram serviços regionais para além da sua tolerância declarada de uma zona. Terceiro, o control plane falha enquanto o data plane segue rodando, em mais de um provedor, e planos de recuperação que são operações de control plane falham com ele: 2023 e outubro de 2025 nos próprios post-event summaries da AWS, fevereiro de 2026 no da Azure.

Agora a outra coluna, porque uma retrospectiva que só conta vitórias é marketing. O que permanece premissa: tudo o que é quantitativo sobre você. Nunca rodei esses labs contra uma frota de produção, então cada número da série é a cara de uma saída, honestamente rotulada mas ainda uma cara. O custo de manada em disparada do failover em massa, os "longer than usual provisioning times" da AWS, aparece em dois incidentes e nunca recebeu um número, nem da AWS nem de mim. O prêmio de estabilidade estática da Parte 3 foi aritmética, não uma conta de uma frota real. E se o seu caminho break-glass funciona é indeterminável daqui.

O que eu faria diferente: rodar o game day primeiro, não por último. A série ordenou seus labs pedagogicamente, detectar, traduzir, medir, injetar, e essa ordem é o contrário operacionalmente: uma execução de injeção produz a lista de achados que o detector só consegue prever. E eu teria desconfiado de fontes secundárias mais cedo. A regra, citar apenas da própria página do operador, continuou justificando seu valor até o fim: pegou durações erradas, um timestamp errado por meio dia, e afirmações cujas aparentes fontes, quando buscadas, não as continham. Uma série sobre premissas não testadas quase enviou várias, o que é uma demonstração da própria tese tão limpa quanto eu poderia ter arranjado.

Game days também falham

O instrumento merece o mesmo ceticismo que a coisa que ele mede, então termine nas três formas em que um game day mente para você.

  • O teste que só prova que o teste rodou. O FIS resolve alvos por tag, e uma incompatibilidade de tag não faz o experimento falhar: o cenário de AZ pula ações cujos alvos resolvem para nada, e o lab mantém esse comportamento (emptyTargetResolutionMode: skip) para que stacks parciais possam rodar. O preço: um experimento pode chegar a completed sem ter parado nada. Resultado verde, zero falhas injetadas. Leia a tabela por ação, não a linha de status; o template de control plane define fail em vez disso, já que ali um alvo vazio torna a execução inútil. Os códigos de saída concordam: completed sai com 0, um experimento stopped sai com 1 mesmo que parar signifique que seu alarme funcionou. O guardrail disparando é o guardrail funcionando, e ainda assim não é uma aprovação.
  • A condição de parada que dispara cedo demais. Vincule o alarme a uma métrica interna com um threshold apertado e o experimento para dois minutos depois de começar, antes da falha interessante. Você aprende apenas que o alarme dispara, e a tentação depois disso é afrouxar ou remover ele, que é como você volta à string vazia do cenário por escolha. A correção não é menos guardrail; é vincular o alarme à métrica de estado estável voltada ao usuário, no threshold em que você gostaria que um incidente real fosse parado.
  • A conta que não é produção. Uma aprovação em sandbox prova o mecanismo, não o desfecho. Sem tráfego de produção, sem gravidade de dados, sem vizinhos barulhentos, sem operador acordado às 3 da manhã. Some a suavidade da própria injeção, desligamentos ordenados, erros limpos em vez de brownouts, e a cadeia de evidência honesta fica: o detector prevê, o game day em sandbox testa o mecanismo da previsão, e produção herda uma extrapolação rotulada. Isso é muito melhor que nada e muito menos que prova, e fingir o contrário é como "fizemos chaos engineering" vira mais uma premissa não testada.

É aí que a série termina. Nada falha sozinho: cada parte encontrou a falha entrelaçada com algo ao seu lado, um quórum com um prédio, uma zona com uma guerra, uma correção com um control plane. Nada passa sozinho também. Toda aprovação está entrelaçada com o ambiente que a produziu, e o rótulo que diz isso é a linha mais carregada de significado do relatório.

Leia isto a seguir

Para consultoria em AWS, arquitetura de nuvem, revisões de resiliência e trabalho de plataforma, comece em ercanermis.com.

Referências