Kubernetes · Docker · Modernización · Arquitectura · Operación real

Modernización progresiva de una aplicación distribuida hacia Kubernetes

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.

1. Arquitectura inicial

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.

2. Por qué no empezar directamente por Kubernetes

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.

3. Primera fase: contenerización

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.

4. Docker Compose como fase intermedia realista

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}

5. Problemas reales que aparecen durante contenerización

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.

6. Segunda fase: Kubernetes

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.

7. Networking interno en Kubernetes

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.

8. Exposición externa

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.

9. El problema real: compatibilidad legacy

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.

10. La realidad de modernizar sistemas legacy

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

11. Adaptación mediante hostPort

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}

12. Por qué hostPort NO es magia

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

13. Validación real

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}

14. Arquitectura final

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}

15. Qué cambió realmente

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.

16. Qué enseñan realmente este tipo de proyectos

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.

17. Lecciones operativas

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

18. Tecnologías utilizadas

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}

19. Conclusión

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.

Volver a guías