Esta página que estás leyendo se publicó sola. Nadie subió ficheros por FTP: alguien hizo un commit, un pipeline validó el código, construyó una imagen y actualizó el servicio en el clúster. La primera vez que lo montamos así, el pipeline salió verde entero, el webhook de despliegue devolvió un HTTP 200 impecable… y la web siguió mostrando exactamente lo de antes. El despliegue «funcionó» sin desplegar nada. De esa tarde y de un par más salieron las tres reglas que hoy aplicamos a cualquier aplicación que publicamos, nuestra o de un cliente.
Un pipeline no está para desplegar rápido. Está para que lo roto no llegue a producción y para que puedas volver atrás sin pensar. La velocidad es un efecto secundario agradable; lo que compras de verdad es control.
Cómo se publica esto (tres pasos y ninguna magia)
El sitio corre sobre nuestra plataforma de contenedores: Docker Swarm con Traefik delante, un registry propio y GitLab CI/CD haciendo de pegamento. Cuando se hace push a la rama principal pasan tres cosas, en este orden. Primero lint: php -l sobre todos los .php del repositorio, de cuatro en cuatro. Segundo build: se construye la imagen con Kaniko — sin demonio Docker, porque el runner que la construye no levanta uno — con el sitio horneado dentro: el código se copia a la imagen, no se monta desde el disco del servidor. Tercero deploy: se actualiza el servicio del clúster, gestionado con Portainer, para que use esa imagen.
Ese es todo el secreto de lo que la gente llama GitOps: el repositorio es la única fuente de verdad y el despliegue es una consecuencia de un commit, no una tarea manual de alguien. Si un cambio no está en git, no existe. Nadie entra a tocar un fichero en el servidor, entre otras cosas porque no serviría de nada. Y no hace falta una plataforma interna con nombre propio ni un clúster de Kubernetes para tenerlo.
Lección 1: «latest» es un nombre, no una versión
Volvamos a la tarde del despliegue fantasma. La imagen nueva estaba construida y subida al registry con la etiqueta :latest. El servicio del clúster también apuntaba a :latest. Se relanzó el servicio y arrancó… la imagen de siempre. La documentación de Docker lo explica sin rodeos: cuando creas un servicio, queda fijado a un digest concreto de la imagen hasta que lo actualizas con service update --image; solo entonces el manager pregunta al registry a qué digest apunta ahora esa etiqueta.
Dicho de otra forma: el orquestador no persigue tu etiqueta. La resolvió una vez, se quedó con la huella y a partir de ahí es fiel a esa huella. Reiniciar el servicio, forzarlo o pedirle que se refresque no cambia nada, porque desde su punto de vista no le has pedido una imagen distinta. Y es un comportamiento razonable —nadie quiere que las réplicas de un servicio corran versiones distintas porque una arrancó cinco minutos después—, pero rompe la intuición de todo el mundo.
Lo que hacemos desde entonces: cada build etiqueta la imagen dos veces, con :latest y con el SHA corto del commit, para que cada versión exista con nombre propio en el registry. Y el despliegue que damos por bueno es el que apunta el servicio a la etiqueta del SHA con service update --image — un comando explícito que sí obliga al manager a resolver de nuevo el digest —, no el simple reinicio contra :latest. Con la verdad por delante: el fichero del stack de este sitio todavía declara :latest, y clavar ahí el SHA de cada commit es lo siguiente de la lista. La deuda existe y sabemos dónde está.
Lección 2: el linter dijo que sí y la web dijo que no
Un linter comprueba que el código está bien escrito. No lo ejecuta. Nos lo recordó un fatal de runtime en la paginación del listado del blog — una operación entre un entero y una cadena — que pasó el php -l sin despeinarse y tumbó la portada. Detalle cruel: las páginas de los artículos estaban perfectas; lo que se rompió fue el índice, que es justo por donde entra la gente.
La respuesta no fue una suite de tests que nadie iba a mantener. Fue un smoke test de veinte líneas que corre dentro del propio pipeline: levanta el sitio con el servidor embebido de PHP, pide cuatro rutas reales —el listado en los tres idiomas y la segunda página de la paginación— y falla si en la respuesta aparece Fatal error, Uncaught, Parse error o Unsupported operand —este último es justo el que caza el entero contra la cadena—, o si el HTML no llega a cerrar </html>. Nada más. Esa pregunta mínima —«¿la página que más tráfico recibe se dibuja entera?»— es exactamente lo que nos habría avisado del fatal de la portada antes de que llegara a producción.
La regla que sacamos de ahí vale para cualquier aplicación: el control va antes del despliegue, no después. Un aviso que llega cuando la página ya está rota no es un control, es una notificación. Y si tienes que elegir un solo test automático para una aplicación web, que sea el que pide sus dos o tres URLs más importantes y comprueba que responden enteras.
Esto conecta con algo que contamos hace unos días a propósito de quién mantiene realmente tu web: tener un proceso escrito no es tenerlo funcionando. La diferencia entre las dos cosas es si algo automático se atreve a decir «esto no sale» aunque haya prisa.
Lección 3: el «zero downtime» que ponía en nuestro propio fichero
Esta la contamos porque nos deja en mal lugar y por eso mismo es útil. Ahora mismo, mientras lees esto, el fichero de despliegue de este sitio sigue diciendo en un comentario, tan tranquilo, «rolling update (zero downtime)». Con una sola réplica —que es lo que te da Swarm si no declaras ninguna—, eso no es verdad. La especificación de Compose lo dice: el orden de actualización por defecto es stop-first, la tarea vieja se para antes de arrancar la nueva. Eso son unos segundos de error mientras el contenedor nuevo levanta.
Para que sea cierto hacen falta cuatro cosas juntas: al menos dos réplicas, order: start-first (la nueva arranca antes de que caiga la vieja), un healthcheck que diga cuándo la nueva está realmente sirviendo —si no, el orquestador cree que está lista en cuanto el proceso existe— y failure_action: rollback, porque el valor por defecto es pause: la actualización se queda a medias esperando a que alguien la mire.
Para una web corporativa, unos segundos de parpadeo a media mañana no arruinan nada, y montar dos réplicas por deporte tampoco es gratis. Para el ERP, el portal de pedidos o la aplicación con la que trabaja tu gente ocho horas al día, sí importa. Lo relevante no es exhibir un «zero downtime» en una diapositiva: es saber cuánto te cuesta cada despliegue y haberlo decidido. Un comentario en un YAML no es un SLA.
Lo mejor del sistema no es desplegar: es deshacer
Con imágenes etiquetadas por commit, volver atrás es desplegar la etiqueta anterior. No hay que revertir código en caliente, ni reconstruir nada, ni acordarse de qué ficheros se tocaron el viernes. La versión de ayer existe, entera, con su número, y sigue en el registry. Eso es lo que convierte un despliegue en una decisión reversible en lugar de un salto.
De ahí sale la pregunta que hacemos en las reuniones de IT y que casi nadie tiene contestada. No es «¿cuánto tardáis en publicar un cambio?». Es: ¿cuánto tardáis en volver a la versión de ayer, y quién sabe hacerlo un viernes a las ocho de la tarde? Si la respuesta incluye la palabra «depende» o el nombre de una sola persona, el problema no es el despliegue.
Cuatro atajos que no tomamos
- ✗Montar Kubernetes porque toca. Para una aplicación con un puñado de contenedores, Swarm con Traefik y un registry se entiende entero en una tarde y lo puede operar el equipo que ya tienes. Kubernetes resuelve problemas reales de escala y de organización; si no tienes esos problemas, lo que has comprado es una segunda infraestructura que mantener. La pregunta no es cuál es mejor, es cuál sabes arreglar tú a las tres de la madrugada.
- ✗Editar en producción. El contenedor es inmutable: el arreglo heroico que alguien hace por SSH sobrevive exactamente hasta el siguiente despliegue, que se lo lleva por delante sin avisar. Peor aún: durante ese rato, lo que corre en producción no está en ningún sitio. Si hay que arreglar algo con urgencia, se arregla en el repositorio y se despliega — el camino rápido es el mismo que el camino correcto, y esa es justo la gracia.
- ✗Dar por bueno un webhook que devuelve 200. Un HTTP 200 significa «he recibido tu petición», no «he desplegado tu código». Lo dimos por bueno una vez y de ahí viene esta historia. Ahora la comprobación es la de siempre: que el servicio haya convergido y que la URL pública devuelva lo que tiene que devolver. Si no lo has mirado, no está desplegado.
- ✗Meter secretos en la imagen. Las credenciales de correo, las claves de las APIs y los tokens se inyectan como variables del entorno del stack, nunca se copian dentro de la imagen. Una imagen se copia sin pensar y se queda en un registry mucho más tiempo del que crees; una contraseña dentro de una capa no se borra editando el fichero.
Esto no va de nuestra web
Contamos nuestro caso porque lo tenemos delante todos los días, pero el mismo esquema es el que montamos para las aplicaciones de negocio de nuestros clientes: la web corporativa, el portal interno, la aplicación a medida que lleva quince años funcionando y que nadie se atreve a tocar. La tecnología concreta importa menos de lo que parece — lo que cambia el día a día es que exista una fuente de verdad única, un control automático que sepa decir que no, y un camino de vuelta que no dependa de la memoria de nadie.
Cuatro preguntas para saber en un minuto cómo estás: ¿podrías reconstruir hoy tu aplicación desde el repositorio, sin ficheros sueltos en un servidor? ¿Sabes, sin mirar, qué versión exacta está sirviendo ahora mismo? ¿Hay algo automático que impida publicar una página rota? ¿Cuánto tardas en volver atrás? Con estas cuatro contestadas, la conversación sobre orquestadores, contenedores y siglas se vuelve secundaria. Sin contestarlas, no hay herramienta que te salve — igual que la monitorización que montamos en contenedores solo sirve si alguien mira lo que dice.
Nosotros operamos esta plataforma de contenedores en producción desde hace tiempo, con la misma filosofía que aplicamos al resto de la infraestructura que gestionamos: preferimos aburrido y reproducible antes que brillante y frágil. Este post, por cierto, se ha publicado por el mismo camino que describe. Si el smoke test hubiera fallado, no lo estarías leyendo.
Fuentes (verificadas): comportamiento de las etiquetas y los digests en servicios de Swarm, en la documentación oficial de Docker — «Deploy services to a swarm» («when you create a service, it is constrained to create tasks using a specific digest of an image until you update the service using service update with the --image flag»). Valores por defecto de update_config (order: stop-first, failure_action: pause) en la especificación de Compose (deploy). El resto — el pipeline, el fatal de paginación, el smoke test y el comentario optimista del YAML — es nuestro, con sus cicatrices.
¿Cuánto tardas en volver a la versión de ayer?
En everyWAN diseñamos y operamos el ciclo de vida de datos y aplicaciones de empresa: repositorio como fuente de verdad, despliegues reproducibles y un camino de vuelta que funciona un viernes por la tarde. Si tu aplicación se publica a mano, o nadie sabe qué versión corre ahora mismo, hablemos.
Hablar con everyWAN