Una republicación fallida nunca borra un backend que funciona
El 25 de septiembre, una republicación fallida borró el backend de una app que había estado en vivo y funcionando durante unas dos horas y media. El código que deshace una publicación fallida eliminó todas las apps de servidor que la publicación había tocado, incluidas las que ya estaban sirviendo antes de empezar. Ahora solo se borran las apps que una publicación ha creado; una app que ya existía vuelve exactamente a la versión que ejecutaba antes. Dos correcciones posteriores ese mismo día garantizaron que la vuelta atrás también se ejecute cuando falla la herramienta de despliegue de la propia plataforma de hosting, y que una app sana no se dé por fallida solo porque estaba dormida.
Cómo se publica un backend
Cuando tu app de Mythex tiene un backend, es decir, una API o un servidor en lugar de solo archivos estáticos, la publicación lo compila a partir de un Dockerfile (una receta para empaquetar la app en una imagen) y despliega esa imagen en máquinas de nuestra plataforma de hosting. Después, un health check espera a que la nueva versión arranque de verdad y se mantenga en pie antes de marcar la publicación como activa.
Si algo falla por el camino, se ejecuta una vuelta atrás (rollback) para deshacerlo. Hacer bien esa vuelta atrás importa más que casi cualquier otra cosa en la publicación, porque se ejecuta justo cuando algo ya está yendo mal.
Problema 1: la vuelta atrás borraba lo que debía restaurar
La vuelta atrás llevaba una lista de las apps de servidor en las que había desplegado una publicación y, si fallaba, las destruía todas. Eso es correcto en una primera publicación: la app se acaba de crear, nada depende de ella y borrarla deja las cosas como estaban.
Es exactamente incorrecto en una republicación. La app ya existía y estaba atendiendo a visitantes reales. El 25 de septiembre, la API de una app que llevaba en vivo unas dos horas y media fue borrada por una republicación fallida.
En el mismo código había un segundo fallo oculto. Cuando el borrado no se completaba, la imagen nueva y rota se quedaba en las máquinas, mientras que al propietario le decíamos que su sitio actual seguía activo. Cuando el health check puede fallar, la nueva imagen ya se ha desplegado en todas las máquinas. No hacer nada no es lo mismo que deshacer.
La corrección: borrar solo lo que creaste, restaurar el resto
Ahora cada publicación registra, para cada app de servidor que toca:
- si esta publicación la creó, y
- si no, qué imagen estaba ejecutando justo antes.
Al fallar, cada app recibe uno de tres tratamientos:
- Creada por esta publicación: se destruye, como antes.
- Ya existía y se conoce la imagen anterior: se vuelve a desplegar la imagen anterior con la misma configuración. Sin compilar; solo se mueve la imagen hacia atrás.
- Ya existía y no se conoce la imagen anterior: no se toca. Una app existente nunca se borra, aunque no podamos restaurarla.
Estas reglas son una pequeña función pura con sus propias pruebas, de modo que la decisión no puede volver a “destruir todo” sin que falle un test.
Problema 2: la herramienta de despliegue podía fallar antes de que escribiéramos cómo deshacer
Poco después de esa corrección, encontramos un hueco. La herramienta de despliegue de la plataforma ejecuta sus propias comprobaciones de humo (smoke checks): si las nuevas máquinas se caen, termina con un error. Pero lo hace después de que la nueva imagen ya esté en las máquinas.
Nuestro código solo registraba una app para la vuelta atrás cuando la herramienta terminaba con éxito. Cuando la propia herramienta hacía fallar el despliegue, la app todavía no estaba en la lista de vuelta atrás y no se restauraba nada. En nuestro entorno de staging, una republicación rota dejó sirviendo la imagen rota.
La corrección: registrar cómo deshacer antes de empezar
Ahora la app se registra para la vuelta atrás, con indicación de si fue creada y qué imagen ejecutaba, en cuanto está preparada y antes de desplegar nada sobre ella. Falle lo que falle después, la vuelta atrás sabe qué devolver. Lo verificamos en local: una republicación rota falló en las comprobaciones de humo de la propia herramienta, y la versión anterior siguió respondiendo.
Problema 3: una app dormida se daba por fallida como “nunca arrancó”
Las apps publicadas en Mythex se duermen cuando están inactivas para ahorrar tus créditos. Una republicación deja una máquina dormida parada, así que el health check tiene que despertarla. Pedía a la plataforma que arrancara la máquina una vez e ignoraba la respuesta.
El 25 de septiembre, esa única petición de arranque no llegó a ninguna parte. Una API sana permaneció parada durante todo el health check de cinco minutos y se dio por fallida con el mensaje “1 máquina nunca arrancó (último estado: stopped)”.
La plataforma a veces sí rechaza un arranque. Mientras reproducíamos el problema, devolvió un conflicto “lease currently held” sin que lo provocáramos, y rechaza los arranques mientras una máquina se está actualizando.
La corrección: seguir pidiéndolo y llamar a la puerta
Ahora el health check:
- vuelve a pedir arrancar una máquina parada cada 20 segundos mientras espera,
- registra lo que respondió la plataforma cuando rechaza el arranque, y
- envía a la app una petición normal, como lo haría un visitante: el proxy de la plataforma despierta una máquina dormida cuando llega una petición.
Lo reproducimos en la plataforma real con una app de prueba cuyo primer arranque era rechazado. Antes de la corrección, la publicación fallaba a los 91 segundos. Después, se publicó en 20 segundos. Una app que de verdad se cae al arrancar sigue fallando, como debe ser.
Lo que aprendimos
- La vuelta atrás tiene que distinguir entre lo nuevo y lo existente. “Deshacer” algo que creaste es borrarlo. “Deshacer” algo que ya estaba es restaurarlo. Tratarlos igual es la forma en que un mecanismo de seguridad acaba borrando una app que funcionaba.
- Escribe cómo deshacer antes de actuar. Si el deshacer solo se registra después de que un paso tenga éxito, un paso que falla a medio camino no deja nada con qué deshacer.
- Nunca ignores la respuesta a una petición de la que dependes. Un “no” ignorado convirtió una app sana en una publicación fallida.
Una corrección relacionada del mismo día: atrapar apps que se caen justo después de publicar. Para saber qué pasa cuando una publicación falla por tu lado, consulta solución de problemas de publicación en la documentación.