Tecnología

Cómo diseñé despliegues selectivos por servicio con Jenkins y Kubernetes

AAnonymous
10 min de lectura

Introducción

Este texto continúa Cómo convertí el backend monolítico de Omnilude en MSA con ayuda de IA. Si en el artículo anterior conté por qué dividí los límites entre servicios, aquí quiero explicar cómo esos límites se convirtieron en unidades reales de despliegue.

Cuando se habla de MSA, es fácil quedarse en cuántos servicios existen. Pero el momento en que la operación realmente se siente distinta no llega cuando cambia la carpeta del código, sino cuando cambia la unidad de despliegue.

En este post quiero dejar ordenado qué problemas redujo la estructura actual, cómo funciona y qué sigue faltando.

Por qué sentí antes la separación del despliegue que la separación del código

Cuando todo vivía en un backend grande, lo más incómodo no era solo que las funciones estuvieran mezcladas. El problema real era que incluso un cambio pequeño terminaba convirtiéndose en presión para volver a desplegar todo.

Aunque solo tocara una parte del blog, tenía que reconstruir toda la aplicación, volver a generar la imagen y subir de nuevo el mismo bloque completo. En una estructura así, las áreas que cambian con frecuencia pierden velocidad de experimentación primero. Un problema todavía mayor era que los servicios que necesitan conexiones largas también quedaban dentro del mismo radio de impacto del despliegue.

Por ejemplo, una zona como backbone-service, cercana a WebSocket, SSE y flujos de eventos, empeora de inmediato en operación si se la sacude con la misma presión que un cambio funcional común. Por eso, para mí, separar servicios terminó siendo menos una cuestión de quién posee qué API y más una cuestión de qué puede desplegarse por separado.

Cómo Jenkins decide qué desplegar

La operación de los servicios corre sobre Kubernetes y Jenkins se encarga del CI/CD.

Aquí me interesa menos el detalle de implementación y más mostrar cómo definí los límites de despliegue desde la perspectiva de CI/CD.

En el pipeline actual, el punto de partida es BUILD_MODE junto con parámetros booleanos por servicio. Con all puedo desplegar todo. En auto, puedo desplegar solo los servicios que marque manualmente o dejar que el pipeline detecte los objetivos a partir de los cambios en Git cuando no selecciono nada. En otras palabras, puedo hacer despliegues completos o parciales de forma manual, y en los disparadores automáticos el objetivo se reduce según los archivos modificados.

Los servicios seleccionables son auth-service, backbone-service, storage-service, ai-service, game-service, blog-service y legacy-service. El repositorio sigue siendo uno solo, pero los objetivos de despliegue se eligen desde el inicio a nivel de servicio.

La lógica de detección automática también es sencilla. Revisa git diff y relaciona los cambios bajo modules/{service} con cada servicio. Pero aquí hay una excepción importante. Si cambia el módulo compartido common(jspring), el pipeline lo interpreta como un cambio en la base común y vuelve a construir todos los servicios. Esa regla evita que los cambios en common queden escondidos detrás de un despliegue parcial.

Ese punto me parece muy importante. El atractivo del despliegue selectivo es mover solo lo necesario y hacerlo rápido, pero si se subestiman los cambios en contratos compartidos, el resultado puede ser más riesgoso.

Los artefactos también se separan por servicio

El flujo real de construcción sigue la misma idea. Primero publico common en el Maven local y después construyo solo los servicios seleccionados, en secuencia, hasta bootJar. Elegir builds secuenciales en lugar de paralelos fue una decisión práctica. En esta etapa era más estable reducir conflictos en la generación de clases Q y choques del Kotlin daemon.

La construcción de imágenes ocurre dentro de un contenedor kaniko. Esa etapa también es secuencial. Si solo miramos velocidad, el paralelismo puede parecer más vistoso, pero en la etapa actual de Omnilude me convenía más reducir conflictos de recursos y hacer más legibles los puntos de falla.

Los nombres de imagen también se mapean por servicio. Por ejemplo, auth-service se convierte en jongmin-auth, ai-service en jongmin-ai y blog-service en jongmin-blog. Así la unidad de despliegue también se puede leer directamente en el registro y luego es más fácil seguir un servicio concreto.

La estructura de carpetas también sigue la unidad operativa

Si Jenkins decide qué construir, Kubernetes fija ese resultado como una unidad operativa real. La estructura actual es kubernetes/services/{service}/{dev|prd}. Cada servicio tiene su propio Deployment y Service, y el ingress se administra por entorno bajo ingress/{env}.

La ventaja de esta estructura es que se puede explicar fácilmente. Si quiero ver cómo se despliega un servicio, basta con mirar la carpeta de ese servicio. Como el límite del código y el límite de los manifiestos coinciden en gran parte, la propiedad también se vuelve menos difusa desde el punto de vista operativo.

El enrutamiento sigue la misma lógica. Bajo dev-api.omnilude.com y api.omnilude.com, cada servicio se conecta por path prefix. Es decir, la separación entre servicios no se queda solo dentro del repositorio, sino que llega hasta la propiedad real de la API y al enrutamiento del ingress.

Las protecciones básicas de estabilidad también entran aquí. Cada Deployment de servicio ya incluye readinessProbe y livenessProbe, y algunos servicios de producción también tienen HPA, PDB y topology spread constraint. Todavía no todos los servicios cuentan con el mismo nivel de guardrails operativos, pero al menos ahora puedo gestionar por separado hasta dónde llegó cada servicio y qué mecanismos de estabilidad ya fueron aplicados.

Mantener simple el flujo de despliegue

El flujo actual de despliegue es más lineal de lo que parece.

  • Jenkins decide qué servicios se desplegarán en esa ejecución.
  • Solo se construyen los servicios seleccionados y se convierten en imágenes.
  • kubectl apply refleja los manifiestos de cada servicio.
  • kubectl rollout restart sube la nueva versión.
  • kubectl rollout status espera hasta que cada servicio quede arriba.
  • Los estados de inicio, éxito y fallo se envían a Slack.

Lo importante aquí no es lo vistoso, sino lo legible. En esta etapa prefiero un flujo donde cualquiera pueda ver con claridad qué se desplegó, dónde falló y qué está esperando el sistema. Las estrategias más complejas se pueden sumar después, pero si la estructura actual ya es difícil de explicar, la operación puede volverse más inestable en lugar de más estable.

Qué cambió después de aplicarlo

El cambio más grande con esta estructura de despliegue selectivo fue que una modificación pequeña dejó de forzar un redeploy completo. ai-service, donde importa la velocidad de experimentación, blog-service, donde quiero pulir rápido la funcionalidad del producto, y el trabajo del lado de juegos, que es relativamente independiente, ahora pueden avanzar con ritmos distintos.

La otra ganancia fue la capacidad de explicarlo. Si alguien pregunta cómo se opera hoy Omnilude, ahora puedo conectar en una sola línea la lista de servicios, los nombres de imagen, las carpetas de Kubernetes, las rutas de ingress y el flujo de notificaciones de Slack. Incluso en un proyecto personal, creo que esta capacidad de explicación importa bastante. La operación no debería terminar en simplemente funcionar. También debería poder explicarse por qué está separada así, para que la siguiente mejora tenga continuidad.

Dicho eso, la estructura actual no está terminada. Todavía faltan rollback automático, smoke test después del despliegue y gates de despliegue basados en métricas. Por ahora, el centro sigue estando en revisar el rollout y resumir el resultado en Slack.

Cierre

La estructura actual no llega al nivel de una gran plataforma de CI/CD, pero al menos el criterio de un solo repositorio con despliegue dividido por servicio ya está conectado de forma consistente desde los parámetros de Jenkins hasta los nombres de imagen, las carpetas de Kubernetes, las rutas de ingress y las notificaciones de Slack.

El siguiente paso es claro. Sobre esta base tengo que agregar guardrails reales, como rollback automático, smoke tests y mejores alertas operativas. Esa parte sigue en progreso, así que la dejaré para otro post cuando la implementación esté terminada.