Extracto #

En este caso anonimizado, comparto cómo abordé la recuperación de varias aplicaciones legacy después de un incidente: restauración controlada en una infraestructura limpia, encapsulamiento con contenedores, publicación detrás de un borde administrado, separación de uploads, corrección de permisos, diagnóstico de errores 500 y análisis de rendimiento causado por generación de documentos pesados al vuelo.

La principal lección: estabilizar no es modernizar, pero una buena estabilización puede ser el puente correcto hacia una modernización segura.


Introducción #

Hace poco me tocó participar en una intervención técnica posterior a un incidente, en la que el objetivo principal era claro: recuperar operación lo antes posible sin volver a exponer a la organización a los mismos riesgos del ambiente anterior.

El caso involucraba varias aplicaciones legacy críticas para la operación interna de una organización. Eran sistemas antiguos, con dependencias históricas, runtime web heredado, base de datos relacional, archivos generados localmente, uploads de usuarios, librerías para documentos, assets estáticos y configuraciones que habían crecido durante años.

Como suele pasar con este tipo de plataformas, el reto no era únicamente “levantar la aplicación”. El verdadero reto era hacerlo de forma controlada, entendiendo qué se estaba restaurando, qué riesgos venían dentro de los respaldos y qué medidas temporales podían ayudar a estabilizar la operación sin vender la ilusión de que eso equivalía a una modernización.

El contexto: sistemas críticos, tecnología antigua y necesidad de volver a operar #

Después de un incidente, la primera tentación suele ser restaurar el último respaldo disponible y correr. Pero en entornos legacy eso puede ser peligroso.

No siempre sabes con certeza qué contiene el respaldo, qué archivos fueron modificados, qué rutas son realmente usadas por la aplicación, qué carpetas contienen datos de usuarios, qué dependencias son indispensables o qué comportamientos inseguros se han normalizado con el paso de los años.

En este caso, había que recuperar aplicaciones críticas desde respaldos, pero evitando reinstalar todo el stack antiguo directamente sobre el sistema operativo. La prioridad fue levantar un ambiente nuevo, limpio y más controlado, encapsulando las dependencias legacy en contenedores.

La decisión fue tratar la recuperación como una fase de contención y estabilización, no como una modernización completa.

La primera decisión importante: no revivir el servidor viejo #

Una de las mejores decisiones del proyecto fue no intentar reconstruir el ambiente anterior tal cual.

En lugar de reinstalar el stack web heredado directamente sobre el sistema operativo, se optó por una arquitectura temporal más contenida:

Usuario
   |
Borde administrado
   |
Servidor de recuperación
   |
Contenedor: aplicación legacy
   |
Contenedor: base relacional legacy

Esto permitió aislar mejor las dependencias, reducir cambios directos sobre el host y tener una ruta más clara para reiniciar, ajustar o reemplazar componentes sin contaminar toda la máquina.

La publicación pública quedó detrás de un borde administrado, mientras que las aplicaciones corrían en contenedores dentro de un servidor de recuperación. No era la arquitectura final ideal, pero sí una base razonable para recuperar operación de forma más segura que simplemente “revivir” el servidor anterior.

Restaurar código no es copiar todo #

Uno de los aprendizajes más importantes fue que restaurar una aplicación legacy no significa copiar todo el árbol de archivos sin criterio.

En la primera fase se separó el código fuente de los archivos subidos por usuarios. Esto es clave porque, en muchas aplicaciones antiguas, las carpetas públicas terminan mezclando imágenes, PDFs, documentos, HTML, scripts, respaldos comprimidos, logs y archivos temporales.

Esa mezcla es peligrosa.

Por eso se hizo una restauración más selectiva: el código por un lado, los uploads por otro, y con exclusiones explícitas para artefactos que no debían viajar automáticamente al nuevo ambiente, como respaldos, logs, archivos comprimidos o contenido potencialmente ejecutable.

También hubo un incidente interesante durante una copia inicial: una variable de origen quedó mal evaluada y el proceso de sincronización empezó a copiar más de lo esperado, generando una ruta recursiva dentro del destino. Lo que debía pesar algunos gigabytes empezó a crecer de forma anormal.

Ese tipo de error recuerda algo muy simple: en recuperación de incidentes, los comandos “normales” también necesitan guardrails.

Antes de volver a copiar, se agregaron validaciones para asegurar que las rutas no estuvieran vacías, que el origen no fuera /, que el destino no fuera / y que el destino no quedara dentro del origen. Es una pequeña protección, pero puede evitar un desastre operativo.

Los uploads son una frontera de seguridad #

Otra parte crítica fue el manejo de archivos subidos por usuarios.

En aplicaciones legacy es común encontrar carpetas públicas que originalmente estaban pensadas para imágenes o documentos, pero con el tiempo terminan aceptando más tipos de archivos. En este caso se detectaron imágenes, PDFs, videos y también archivos HTML.

Aunque no todos los HTML son maliciosos, publicar automáticamente contenido activo restaurado desde un respaldo no es buena idea, especialmente después de un incidente.

La estrategia fue separar físicamente los uploads del código y bloquear ejecución de extensiones peligrosas dentro de esas carpetas. También se evitó copiar ciertos tipos de archivo sin revisión manual.

Este punto parece menor, pero es enorme: muchas intrusiones web terminan sobreviviendo precisamente porque algún archivo ejecutable queda escondido dentro de una carpeta de uploads.

La recuperación no puede limitarse a que “la app cargue”. También debe reducir el riesgo de reintroducir el problema.

Compatibilidad antes que elegancia #

El stack tenía dependencias antiguas: runtime legacy, librerías históricas para generación de documentos, procesamiento de imágenes, componentes de framework y funciones deprecadas que ya no existen en versiones modernas.

La solución pragmática fue correr el runtime heredado dentro de contenedores, con las extensiones necesarias para que el sistema pudiera operar. No es lo que uno elegiría para un desarrollo nuevo, pero en una recuperación real hay que distinguir entre dos objetivos distintos:

Recuperar operación hoy y modernizar correctamente después.

Forzar una migración de versión de lenguaje durante una respuesta a incidente puede multiplicar el riesgo. Primero había que estabilizar, entender el comportamiento y dar tiempo a una modernización formal.

Publicar la aplicación no es solo abrir el puerto #

Una vez que los contenedores levantaron localmente, vino la parte de publicación.

La aplicación debía estar disponible mediante un dominio público detrás de un borde administrado. Ahí aparecieron problemas típicos: health probes que respondían correctamente, pero tráfico real que seguía fallando; configuraciones de host header; rutas; protocolos de forwarding; errores 502; y diferencias entre lo que respondía el origin localmente y lo que veía el usuario final.

También hubo ajustes de configuración interna de la aplicación. Por ejemplo, una app legacy puede tener una URL base hardcodeada o calculada bajo supuestos que ya no aplican cuando vive detrás de un reverse proxy o un borde administrado.

El síntoma puede verse como “no carga el CSS”, “los assets vienen de otra dirección”, “el login falla” o “la página se ve rota”. Pero la causa real suele estar en una combinación de base URL, host header, protocolo y rutas públicas.

En este tipo de restauraciones, probar con curl, revisar headers, validar el origin local y comparar contra la ruta pública ayuda muchísimo a separar problemas de aplicación, red, proxy y configuración.

El primer error 500 casi nunca es el último #

Al levantar aplicaciones legacy, los primeros errores suelen ser de permisos, sesiones, rutas o dependencias faltantes.

En este caso aparecieron problemas con carpetas de sesión no escribibles, logs, cache y directorios de uploads. Nada especialmente exótico, pero sí suficiente para provocar errores 500 y bloquear funcionalidad.

La solución fue ajustar permisos mínimos necesarios para que el proceso web pudiera escribir donde realmente debía escribir, sin abrir permisos de forma indiscriminada.

Este es otro punto importante: en una emergencia es tentador hacer chmod -R 777 y seguir. Pero cada permiso excesivo que se da durante una recuperación puede convertirse en deuda de seguridad. Aunque estés bajo presión, vale la pena corregir con criterio.

No siempre es la base de datos: a veces el cuello está en el runtime web #

Cuando los usuarios empezaron a probar ciertas funcionalidades, reportaron comportamiento intermitente: a veces respondía, a veces tardaba demasiado, a veces parecía congelarse.

La primera sospecha en muchos sistemas legacy suele ser la base de datos. Pero al revisar consumo de recursos, procesos y métricas de los contenedores, el patrón apuntó a otra cosa: CPU muy alta en el contenedor web, memoria presionada y la base relacional relativamente tranquila.

La causa probable era la generación de documentos pesados al vuelo.

Muchas aplicaciones antiguas generan documentos pesados dentro del request web. Eso significa que el mismo proceso que atiende al usuario también arma consultas, procesa datos, renderiza vistas, genera archivos, escribe al filesystem y responde al navegador.

Con pocos usuarios puede funcionar. Con concurrencia, puede saturar CPU, consumir RAM y tumbar la experiencia para todos.

La medida inmediata fue escalar recursos de la VM y limitar concurrencia para evitar que demasiados workers pesados agotaran la máquina al mismo tiempo. También se recomendaron logs con duración por request para identificar exactamente qué URLs estaban costando más.

La solución definitiva, sin embargo, no es simplemente “darle más RAM”. Lo correcto a futuro es rediseñar ese flujo: generar el PDF una vez, guardarlo, reutilizarlo, regenerarlo solo cuando cambien los datos fuente y mover procesos pesados a workers o colas.

El problema más simple: editar la carpeta equivocada #

Otro hallazgo clásico en ambientes con contenedores fue que un cambio en código no se veía reflejado.

Después de revisar el contenedor, se confirmó que el servidor web estaba sirviendo una ruta montada específica, pero los cambios se estaban haciendo en otra carpeta del servidor. Es decir: el archivo editado existía, pero no era el archivo que la aplicación estaba usando.

Este tipo de problema es muy común cuando hay respaldos, carpetas antiguas, copias temporales, montajes de contenedores y estructuras con nombres parecidos.

La solución fue identificar la ruta real montada dentro del contenedor y sincronizar los cambios ahí. Más importante todavía: documentar cuál es la carpeta fuente de verdad para evitar que el equipo siga trabajando sobre rutas muertas.

En restauraciones legacy, saber “dónde vive realmente la aplicación” es más importante de lo que parece.

Qué se logró #

Al final de esta fase se consiguió recuperar operación sobre infraestructura nueva. Las aplicaciones quedaron corriendo en contenedores, publicadas mediante un borde administrado, con bases de datos restauradas, ajustes de configuración aplicados, permisos corregidos y rutas reales identificadas.

También se detectaron riesgos importantes: dependencias antiguas, generación ineficiente de documentos, consumo alto de CPU, riesgo de falta de memoria, carpetas públicas con archivos sensibles, configuraciones manuales y deuda técnica acumulada.

El resultado fue positivo, pero con una aclaración importante: esto fue una recuperación controlada, no una modernización.

Y esa distinción importa.

Estabilizar no es modernizar #

Una recuperación exitosa puede dar una falsa sensación de cierre. La aplicación ya carga, los usuarios ya entran, el dominio ya responde y los errores más visibles desaparecen.

Pero debajo siguen existiendo los mismos problemas estructurales: versiones antiguas, lógica acoplada al filesystem, generación pesada dentro del request, secretos en archivos de configuración, falta de observabilidad formal, despliegues manuales y dependencias difíciles de mantener.

Por eso, la fase de recuperación debe verse como un puente. Sirve para volver a operar, contener el riesgo inmediato y ganar tiempo. Pero no reemplaza el trabajo de modernización.

En este caso, las recomendaciones posteriores fueron claras:

  • mover procesos pesados a workers;
  • cachear documentos generados;
  • separar almacenamiento de archivos;
  • externalizar secretos;
  • restringir el acceso al origin;
  • agregar monitoreo y alertas;
  • formalizar respaldos y snapshots;
  • implementar pipelines de despliegue;
  • avanzar con una modernización gradual usando un patrón tipo strangler.

Lecciones que me llevo #

La primera lección es que, después de un incidente, no basta con restaurar: hay que restaurar con intención. Cada respaldo puede traer código viejo, archivos peligrosos, configuraciones rotas y deuda acumulada.

La segunda es que los contenedores pueden ser muy útiles para contener legacy, siempre y cuando se documenten bien los montajes, rutas, volúmenes y límites operativos.

La tercera es que los problemas de rendimiento no siempre están donde uno espera. En este caso, la base de datos no era el principal cuello de botella durante las pruebas: el costo fuerte estaba en el runtime web generando documentos al vuelo.

La cuarta es que las carpetas de uploads merecen tratamiento especial. No son simplemente “archivos estáticos”; son una superficie de riesgo.

Y la quinta es que una buena recuperación debe terminar con dos cosas: operación restaurada y un mapa claro de lo que todavía debe modernizarse.

Porque levantar un sistema legacy después de un incidente es apenas el primer paso. El verdadero objetivo es que la organización no tenga que volver a pasar por lo mismo.