Cuando se habla de modernización tecnológica muchas veces se simplifica demasiado el proceso. Se habla de Kubernetes, cloud o contenedores como si bastase con desplegar unos pods para transformar completamente una plataforma.
La realidad suele ser bastante distinta.
En sistemas reales existen dependencias históricas, clientes legacy, protocolos rígidos, aplicaciones que llevan años funcionando de una manera concreta y equipos operando plataformas que crecieron más rápido de lo que pudieron documentarse.
Este caso técnico parte precisamente de ese escenario: una aplicación distribuida compuesta por múltiples servicios backend desarrollados en .NET, apoyados en base de datos relacional y sistema de mensajería interna, originalmente desplegados directamente sobre Linux sin aislamiento ni orquestación. :contentReference[oaicite:0]{index=0}
El objetivo no era simplemente “pasar a Kubernetes”.
El objetivo real era:
- desacoplar despliegue e infraestructura - mejorar reproducibilidad - reducir dependencia del host - permitir evolución futura - mantener compatibilidad existente - minimizar impacto operativo - evitar ruptura con clientes legacy
Y precisamente ahí es donde este tipo de proyectos dejan de ser un laboratorio y empiezan a convertirse en ingeniería real.
La arquitectura original estaba compuesta por varios servicios backend independientes con responsabilidades diferenciadas:
identity-service session-service core-api-service relational-db message-broker
Los servicios se ejecutaban directamente sobre el sistema operativo Linux y se comunicaban mediante red local y puertos TCP. :contentReference[oaicite:1]{index=1}
La estructura era relativamente clásica:
Aplicación backend ↓ Procesos ejecutándose directamente en host ↓ Base de datos y sistema de mensajería
Mientras la plataforma permaneció pequeña, el modelo funcionó razonablemente bien. Pero con el tiempo empezaron a aparecer limitaciones típicas:
- dependencias entre servicios difíciles de rastrear - contaminación de librerías - upgrades complejos - despliegues manuales - rollback poco fiable - dificultad para reproducir entornos - troubleshooting dependiente del host - diferencias entre servidores - dependencia excesiva del estado exacto del sistema
Uno de los mayores problemas de este tipo de arquitecturas no es el rendimiento, sino la acumulación progresiva de complejidad operativa.
El servidor deja de ser únicamente infraestructura y empieza a convertirse en parte de la aplicación.
Eso genera una dependencia peligrosa:
“la aplicación funciona porque ESTE servidor está exactamente así”
Y ese tipo de dependencia complica muchísimo mantenimiento, migraciones y recuperación ante fallos.
Uno de los errores más habituales en modernización es intentar saltar directamente desde procesos tradicionales a Kubernetes.
En teoría puede hacerse.
En la práctica suele generar:
- troubleshooting más complejo - demasiados cambios simultáneos - dificultad para aislar errores - falta de trazabilidad - problemas de red difíciles de diagnosticar - incompatibilidades inesperadas
Por eso la estrategia utilizada fue incremental.
No se intentó transformar todo el sistema de golpe.
La evolución siguió estas fases:
1. Contenerización 2. Docker Compose 3. Validación funcional 4. Kubernetes 5. Exposición externa 6. Resolución de compatibilidad legacy 7. Validación end-to-end
Eso permitió aislar problemas progresivamente y entender exactamente qué cambiaba en cada fase.
La primera gran transformación consistió en contenerizar completamente la aplicación utilizando Docker y Docker Compose. :contentReference[oaicite:2]{index=2}
Cada servicio pasó a ejecutarse dentro de su propio contenedor:
identity-service session-service core-api-service relational-db message-broker
Esto aportó beneficios inmediatos:
- aislamiento - reproducibilidad - control de dependencias - despliegues consistentes - portabilidad - versionado más limpio
Y probablemente el cambio más importante:
la aplicación dejó de depender completamente del host
Eso no significa que desaparezcan todos los problemas.
Pero sí reduce muchísimo el acoplamiento operativo.
Esto es algo que muchas guías técnicas ignoran completamente.
La evolución real de muchísimas plataformas no suele ser:
VM → Kubernetes
Suele ser:
Procesos tradicionales → contenedores → Docker Compose → automatización → orquestación → Kubernetes
Y tiene bastante sentido.
Docker Compose permite:
- entender networking entre servicios - validar aislamiento - desacoplar procesos - probar persistencia - trabajar con variables de entorno - separar configuración - introducir control declarativo
Todo eso antes de introducir la complejidad adicional de Kubernetes.
En esta fase la aplicación quedó completamente funcional en entorno contenerizado. :contentReference[oaicite:3]{index=3}
Cuando una aplicación tradicional entra en contenedores suelen aparecer problemas muy concretos:
- rutas hardcodeadas - dependencias implícitas - almacenamiento local - variables no documentadas - puertos asumidos - logs escritos en local - permisos inesperados - procesos auxiliares - inicializaciones manuales
Y además aparecen nuevas necesidades:
- healthchecks - persistencia clara - gestión de secretos - configuración externa - networking explícito
La contenerización obliga a entender realmente cómo funciona la aplicación.
Y eso suele sacar a la luz bastante deuda técnica oculta.
Una vez estabilizado el entorno contenerizado se desplegó un clúster ligero basado en K3s sobre Linux. :contentReference[oaicite:4]{index=4}
Cada servicio pasó a definirse mediante un Deployment independiente:
Pods Kubernetes ↓ Servicios ClusterIP ↓ Red interna del clúster
Esto permitió desacoplar completamente el despliegue del modelo Docker Compose. :contentReference[oaicite:5]{index=5}
Y aquí es donde aparecen cambios importantes:
- scheduling - red interna del clúster - resolución DNS - reconciliación declarativa - reinicio automático - separación infraestructura/aplicación - despliegues reproducibles
Pero Kubernetes no resuelve mágicamente todos los problemas.
De hecho, también introduce complejidad nueva:
- networking distribuido - persistencia desacoplada - exposición de servicios - debugging más complejo - troubleshooting multinodo
Por eso es importante llegar con una base razonablemente estabilizada desde la fase de contenedores.
La comunicación entre servicios pasó a realizarse mediante Services ClusterIP.
Esto permitió:
identity-service ↓ service DNS interno ↓ session-service ↓ core-api-service
La aplicación ya no dependía de IPs locales concretas ni de configuraciones estáticas entre procesos.
Kubernetes resolvía:
- descubrimiento interno - balanceo básico - abstracción de pods - reconexión ante recreación
Eso simplificó muchísimo la comunicación interna.
La siguiente fase consistió en publicar servicios para acceso desde cliente externo.
Inicialmente se utilizaron servicios NodePort. :contentReference[oaicite:6]{index=6}
La idea parecía correcta:
Cliente externo ↓ NodePort ↓ Servicio Kubernetes ↓ Pods
Y técnicamente funcionaba.
Se validó:
- conectividad TCP - disponibilidad - publicación correcta
Pero aquí apareció el problema real del proyecto.
El cliente externo utilizaba puertos fijos definidos dentro del propio protocolo de comunicación. :contentReference[oaicite:7]{index=7}
Eso generaba un problema importante:
Kubernetes NodePort usa puertos dinámicos cliente legacy esperaba puertos concretos
Y este tipo de incompatibilidades son muchísimo más comunes de lo que parece.
Muchas veces:
- protocolos antiguos - software industrial - clientes legacy - sistemas propietarios - aplicaciones embebidas
esperan:
IP concreta Puerto concreto Comportamiento exacto
Y ahí es donde muchas modernizaciones empiezan a complicarse.
Porque técnicamente Kubernetes funciona.
Pero funcionalmente el cliente deja de poder conectar.
Aquí aparece una de las diferencias más importantes entre laboratorio y producción.
En laboratorio puedes rediseñar.
En producción muchas veces debes adaptarte a:
- clientes antiguos - contratos de integración - protocolos rígidos - software no modificable - limitaciones externas
Y eso obliga a tomar decisiones pragmáticas.
No siempre existe una solución “perfecta”.
A veces existe la solución:
más compatible más estable menos disruptiva
Para resolver el problema de compatibilidad se decidió utilizar hostPort. :contentReference[oaicite:8]{index=8}
La arquitectura quedó:
Cliente externo ↓ IP pública del nodo ↓ hostPort ↓ Pod Kubernetes
Esto permitió mantener:
- puertos esperados - compatibilidad del protocolo - comportamiento del cliente
Y eliminó los errores previos de conectividad. :contentReference[oaicite:9]{index=9}
Es importante entender algo:
hostPort no es “la mejor práctica universal”.
Tiene limitaciones importantes:
- acoplamiento al nodo - limitación de scheduling - conflictos de puertos - menor flexibilidad - exposición más directa
Pero en este escenario concreto resolvía un problema real:
mantener compatibilidad legacy
Y eso es precisamente lo importante.
La arquitectura correcta no siempre es la más elegante teóricamente.
Muchas veces es:
la que permite evolucionar sin romper producción
Una vez adaptada la exposición se realizaron validaciones completas:
- conectividad TCP - disponibilidad - pruebas funcionales - validación end-to-end - cliente real
Resultados obtenidos:
- conexión estable con identity-service - eliminación de errores previos - comunicación correcta extremo a extremo
Todo ello quedó validado sobre el entorno Kubernetes final. :contentReference[oaicite:10]{index=10}
La arquitectura final quedó compuesta por:
Cliente externo ↓ Nodo Kubernetes K3s ↓ Pods: - identity-service - session-service - core-api-service - relational-db - message-broker
Todos los servicios quedaron completamente operativos en entorno orquestado. :contentReference[oaicite:11]{index=11}
El cambio importante no fue simplemente usar Kubernetes.
Lo importante fue:
- desacoplar aplicación e infraestructura - eliminar dependencia del host - estandarizar despliegue - mejorar reproducibilidad - permitir evolución futura - habilitar portabilidad - reducir complejidad operativa
Y además:
sin romper compatibilidad existente
Eso probablemente fue la parte más compleja del proyecto.
Este tipo de migraciones dejan varias conclusiones bastante claras:
1. Kubernetes no sustituye entender la aplicación. 2. La contenerización previa simplifica muchísimo la transición. 3. Los clientes legacy condicionan arquitectura más de lo esperado. 4. El networking suele ser más complejo que el despliegue. 5. Modernizar no significa reescribir todo. 6. La compatibilidad real pesa más que la teoría.
Desde el punto de vista operativo, probablemente las lecciones más importantes fueron:
- validar siempre extremo a extremo - aislar cambios progresivamente - no introducir demasiadas capas simultáneamente - documentar decisiones “raras” - entender el comportamiento del cliente real - separar evolución técnica de ruptura funcional
Y especialmente:
no asumir que Kubernetes arregla automáticamente arquitectura antigua
Linux Ubuntu Docker Docker Compose Kubernetes K3s .NET backend Base de datos relacional Sistema de mensajería Networking TCP kubectl netcat
Todas ellas utilizadas como piezas de una evolución progresiva y no como tecnologías aisladas. :contentReference[oaicite:12]{index=12}
Modernizar una aplicación distribuida real rara vez consiste en reemplazar tecnología de golpe.
Normalmente consiste en:
- reducir dependencias - aislar componentes - desacoplar despliegues - introducir reproducibilidad - automatizar progresivamente - mantener compatibilidad - reducir riesgo operativo
La parte difícil no suele ser desplegar pods.
La parte difícil suele ser:
evolucionar arquitectura sin romper el sistema existente
Y precisamente ahí es donde este tipo de proyectos dejan de parecer laboratorios y empiezan a convertirse en modernización real.