Uma republicação com falha nunca apaga um backend que funciona
No dia 25 de setembro, uma republicação com falha apagou o backend de um app que estava no ar e funcionando havia cerca de duas horas e meia. O código que desfaz uma publicação com falha removeu todos os apps de servidor que a publicação tinha tocado, inclusive os que já estavam servindo antes de ela começar. Agora só são apagados os apps que a própria publicação criou; um app que já existia é colocado de volta exatamente na versão que rodava antes. Duas correções de acompanhamento, no mesmo dia, garantiram que essa desfazer também rode quando a ferramenta de deploy da plataforma de hospedagem falha, e que um app saudável não seja marcado como falho só porque estava em modo de espera.
Como um backend é publicado
Quando seu app na Mythex tem um backend (uma API ou servidor, e não apenas arquivos estáticos), a publicação o compila a partir de um Dockerfile (uma receita para empacotar o app em uma imagem) e coloca essa imagem nas máquinas da nossa plataforma de hospedagem. Depois, um health check aguarda a nova versão realmente subir e se manter de pé antes de marcar a publicação como ativa.
Se qualquer coisa falhar pelo caminho, um rollback é executado para desfazer o que foi feito. Acertar esse rollback importa mais do que quase qualquer outra coisa na publicação, porque ele roda justamente quando algo já está dando errado.
Problema 1: o rollback apagava o que deveria restaurar
O rollback mantinha uma lista dos apps de servidor para os quais a publicação tinha feito deploy e, em caso de falha, destruía todos eles. Isso está certo para uma primeira publicação: o app foi criado há pouco, nada depende dele, e apagá-lo deixa tudo como estava.
É exatamente errado para uma republicação. O app já existia e atendia visitantes de verdade. Em 25 de setembro, a API de um app que estava no ar havia cerca de duas horas e meia foi apagada por uma republicação com falha.
Havia um segundo modo de falha escondido no mesmo código. Quando a exclusão não se concluía, a imagem nova e quebrada continuava nas máquinas, enquanto avisávamos o dono de que o site atual ainda estava no ar. Quando o health check pode falhar, a nova imagem já foi colocada em todas as máquinas. Não fazer nada não é o mesmo que desfazer.
A correção: destruir só o que você criou, restaurar o resto
Cada publicação agora registra, para cada app de servidor que toca:
- se esta publicação o criou, e
- se não, qual imagem ele estava rodando logo antes.
Em caso de falha, cada app recebe um de três tratamentos:
- Criado por esta publicação: destruir, como antes.
- Já existia, e a imagem anterior é conhecida: fazer redeploy da imagem anterior com a mesma configuração. Sem build; apenas a imagem volta.
- Já existia, e a imagem anterior é desconhecida: não mexer. Um app existente nunca é apagado, mesmo quando não conseguimos restaurá-lo.
Essas regras são uma pequena função pura com testes próprios, então a decisão não pode voltar a ser “destruir tudo” sem que um teste falhe.
Problema 2: a ferramenta de deploy podia falhar antes de registrarmos como desfazer
Logo depois dessa correção, encontramos uma lacuna nela. A ferramenta de deploy da plataforma roda os próprios checks de fumaça: se as novas máquinas travam, ela sai com erro. Mas faz isso depois que a nova imagem já está nas máquinas.
Nosso código só registrava um app para rollback quando a ferramenta retornava com sucesso. Quando a própria ferramenta reprovava o deploy, o app ainda não estava na lista de rollback, então nada era restaurado. No nosso ambiente de staging, uma republicação quebrada deixou a imagem quebrada no ar.
A correção: registrar o desfazer antes de começar
O app agora é registrado para rollback, com a informação de se foi criado e de qual imagem rodava, assim que é preparado e antes de qualquer coisa ser colocada nele. O que falhar depois, o rollback sabe o que recolocar. Verificamos localmente: uma republicação quebrada falhou nos próprios checks de fumaça da ferramenta de deploy, e a versão anterior continuou respondendo.
Problema 3: um app em espera foi reprovado como “nunca iniciou”
Apps publicados na Mythex entram em espera quando ficam ociosos, para economizar seus créditos. Uma republicação deixa a máquina parada em espera, então o health check precisa acordá-la. Ele pedia à plataforma que iniciasse a máquina uma única vez e ignorava a resposta.
Em 25 de setembro, esse único pedido de início não surtiu efeito. Uma API saudável ficou parada durante todo o health check de cinco minutos e foi reprovada com “1 máquina nunca iniciou (último estado: stopped)”.
A plataforma às vezes recusa um início. Enquanto reproduzíamos o problema, ela retornou um conflito “lease currently held” sem que tivéssemos provocado isso, e recusa inícios enquanto uma máquina está sendo atualizada.
A correção: continuar pedindo e bater na porta
O health check agora:
- pede de novo para iniciar uma máquina parada a cada 20 segundos enquanto espera,
- registra o que a plataforma respondeu quando recusou, e
- envia uma requisição comum ao app, do jeito que um visitante faria: o proxy da plataforma acorda a máquina em espera quando chega uma requisição.
Reproduzimos o caso na plataforma real com um app descartável cujo primeiro início foi recusado. Antes da correção, a publicação falhava após 91 segundos. Depois, publicou em 20 segundos. Um app que trava de fato ao iniciar continua falhando, como deve.
O que tiramos disso
- O rollback precisa saber a diferença entre novo e existente. “Desfazer” algo que você criou é apagar. “Desfazer” algo que já estava ali é restaurar. Tratar as duas coisas igual é como um mecanismo de segurança acaba apagando um app que funcionava.
- Registre como desfazer antes de agir. Se o desfazer só é registrado depois que um passo tem sucesso, um passo que falha no meio deixa nada para desfazer.
- Nunca ignore a resposta de um pedido do qual você depende. Um “não” ignorado transformou um app saudável em uma publicação falha.
Uma correção relacionada, do mesmo dia: apps que travam logo depois de publicar. Para o que acontece quando uma publicação falha do seu lado, veja solução de problemas de publicação na documentação.