Día 29 de 30 Fase 8: Contenedores, Kubernetes / OpenShift y Observabilidad LKA / LGTM

Contenedores y Kubernetes / OpenShift para Java Spring Boot

Docker multi-stage, JRE headless, Pods, Deployments, Services, ConfigMaps, Secrets, Ingress/Routes y Probes

Visión de conjunto. Sales entendiendo cómo funciona y cuándo usarlo; dominarlo pide más práctica en un proyecto real.

  • 4.5h de estudio
  • 6 conceptos
  • 3 preguntas
  • 1 katas
DockerKubernetesOpenShiftJVM FlagsLivenessReadinessConfigMapsSeguridad de imágenes
Al terminar este día sabrás 4
  • Construir imágenes de contenedor para Spring Boot rápidas de arrancar y de desplegar.
  • Configurar memoria y CPU de un pod Java sin provocar OOMKilled ni throttling.
  • Desplegar un servicio con Deployment, Service, ConfigMap y Secret.
  • Configurar probes con Actuator y desplegar sin cortes, también en OpenShift.

Dockerfile Multi-Stage optimizado para Spring Boot y JVM

Una imagen de contenedor para Java nunca debe incluir Maven ni el código fuente en la imagen final. Se usa una fase de build (con Maven/Gradle y JDK completo) y una fase de ejecución (con JRE mínimo tipo Eclipse Temurin / BellSoft Liberica Alpine/Distroless). Se extraen las capas del jar con layertools (java -Djarmode=layertools -jar app.jar extract) para aprovechar la caché de capas de Docker.

En una línea FROM eclipse-temurin:21-jre-alpine WORKDIR /app COPY --from=builder /app/dependencies/ ./ COPY --from=builder /app/application/ ./ ENTRYPOINT ["java", "-XX:+UseContainerSupport", "-XX:MaxRAMPercentage=75.0", "org.springframework.boot.loader.launch.JarLauncher"]

Buildpacks y arranque con CDS bash
# Imagen OCI sin Dockerfile (requiere Docker en local)
./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=registry.acme.com/taskflow:1.4.0

# Con Jib: construye y publica sin demonio de Docker
./mvnw com.google.cloud.tools:jib-maven-plugin:build -Dimage=registry.acme.com/taskflow:1.4.0

# CDS con Spring Boot 3.3+ (en el Dockerfile, tras extraer el JAR)
java -XX:ArchiveClassesAtExit=app.jsa -Dspring.context.exit=onRefresh -jar app.jar   # entrenamiento
java -XX:SharedArchiveFile=app.jsa -jar app.jar                                       # arranques posteriores

# Inspeccionar capas y vulnerabilidades
dive registry.acme.com/taskflow:1.4.0
trivy image registry.acme.com/taskflow:1.4.0
Cómo funciona por dentro

Además del Dockerfile multi-etapa escrito a mano (día 15), hay dos alternativas sin Dockerfile. Cloud Native Buildpacks, integrados en Spring Boot con ./mvnw spring-boot:build-image, detectan la aplicación, eligen un JRE, crean capas optimizadas, configuran la memoria de la JVM y generan una imagen reproducible con usuario sin privilegios. Jib, de Google, construye y sube la imagen directamente desde Maven sin necesitar Docker.

Para el arranque, Spring Boot 3.3 facilita usar CDS (Class Data Sharing): en un primer arranque de entrenamiento la JVM vuelca un archivo con las clases ya analizadas, y los siguientes arranques lo reutilizan, recortando el tiempo de arranque de forma notable. Las imágenes nativas con GraalVM arrancan en milisegundos a cambio de un build mucho más lento y algunas restricciones de reflexión.

Sea cual sea la vía, comprueba el resultado: tamaño, usuario, capas (con la herramienta dive) y vulnerabilidades (Trivy, Grype).

Reduce la imagen de ~800MB a menos de 150MB, acelera los despliegues en CI/CD y minimiza vectores de vulnerabilidad (CVEs).

Se conecta con Docker LayersAlpine / DistrolessSpring Boot Layertools

Cuatro bloques cronometrados. No pases al siguiente sin tener el resultado esperado.

  1. Bloque 1: Dockerfile Multi-Stage y JVM Flags en Contenedores (0 - 25 min)

    Qué estudiar. Eclipse Temurin JRE, layertools de Spring Boot, MaxRAMPercentage.

    Pregunta a responder. ¿Por qué layertools optimiza la velocidad del CI/CD?

    Resultado esperado. Dockerfile multi-stage completo y testeado localmente.

  2. Bloque 2: Manifiestos de Kubernetes (Deployment, Service, ConfigMap) (25 - 50 min)

    Qué estudiar. Estructura YAML de Deployment, Service ClusterIP, variables desde ConfigMap/Secret.

    Pregunta a responder. ¿Cómo conectar un microservicio Java con PostgreSQL usando Services internos?

    Resultado esperado. Fichero deployment.yaml con health probes configuradas.

  3. Bloque 3: Sondas de Salud y Despliegue en OpenShift (50 - 80 min)

    Qué estudiar. Actuator liveness/readiness groups, permisos OpenShift GID 0, Route vs Ingress.

    Pregunta a responder. ¿Qué diferencia hay entre una Route de OpenShift y un Ingress de K8s?

    Resultado esperado. Configuración de Graceful Shutdown y probes en application.yml.

  4. Cierre y Consolidación (80 - 90 min)

    Qué estudiar. Checklist de producción para contenedores Java.

    Pregunta a responder. ¿Qué 3 errores causan el 90% de los fallos de pods Java en producción?

    Resultado esperado. Notas de mejores prácticas guardadas.

Tres formas de comprobar si lo has entendido, de menos a más exigente: explicarlo con tus palabras, responder preguntas trampa y resolver el reto del tutor.

Explícalo desde cero

Lee cada ficha y explícala en voz alta antes de seguir: si puedes contárselo a alguien que empieza, lo has entendido.

Por qué Liveness y Readiness deben ser endpoints separados

Lo que debes entender

Readiness decide si el Pod recibe tráfico. Liveness decide si el Pod debe ser asesinado y reiniciado. Si apuntas Liveness a una base de datos externa y la base de datos se ralentiza, Kubernetes matará todos tus Pods a la vez, creando una caída generalizada del sistema.

Analogía para contarlo

Readiness es como un médico diciéndole al recepcionista: "Estoy atendiendo a un paciente complejo, no me envíes más gente a la sala de espera por 5 minutos". Liveness es comprobar si el médico tiene pulso; si no tiene pulso, llamas a emergencias (reinicio).

Lo que suele explicarse mal

Usar un único endpoint /actuator/health genérico para ambas sondas.

Señal de que aún no lo dominas

Si la sonda de liveness comprueba la conexión a Kafka o a PostgreSQL.

Preguntas trampa

Responde de memoria, en voz alta o por escrito, y después abre la respuesta razonada: lo importante es el porqué, no la palabra exacta.

Nivel 1 (Básico) ¿Por qué ocurre el error OOMKilled (Exit Code 137) en un Pod Java de Kubernetes si configuraste -Xmx512m y el Pod tiene 512Mi de límite? Ver respuesta

Respuesta razonada

Porque -Xmx512m solo limita el heap, y el proceso Java consume bastante más memoria: metaspace con las clases cargadas, la caché de código del JIT, una pila por cada hilo, buffers directos, estructuras del recolector y memoria nativa de librerías. En una aplicación Spring Boot eso suma fácilmente 200-300 MB. Con 512 MB de heap más ese extra, el proceso supera los 512 Mi del límite y el kernel lo mata: OOMKilled con código 137, sin rastro de OutOfMemoryError en los logs.

La solución es dejar margen: no fijar -Xmx igual al límite sino usar -XX:MaxRAMPercentage (60-75 % según el tamaño), subir el límite del pod, reducir hilos de plataforma (los hilos virtuales ayudan) y medir el consumo real con Native Memory Tracking (-XX:NativeMemoryTracking=summary y jcmd <pid> VM.native_memory summary).

Qué está evaluando la pregunta

La JVM consume más memoria que solo el Heap: necesita espacio para Metaspace, threads stack (1MB por hilo), buffers I/O nativos y compilador JIT. 512m de heap + 150m de memoria no-heap supera los 512Mi del límite de Kubernetes, activando el OOM Killer del kernel.

Nivel 2 (Intermedio) ¿Por qué un contenedor Docker que funciona en Kubernetes Vanilla puede fallar con Permission Denied al desplegarse en OpenShift? Ver respuesta

Respuesta razonada

Porque OpenShift, con su SCC restricted-v2, no ejecuta el contenedor con el usuario declarado en la imagen sino con un UID aleatorio (por ejemplo, 1000680000) y el grupo 0. Si los ficheros de la imagen pertenecen a root o a un usuario concreto como spring:spring con permisos solo para el propietario, ese UID aleatorio no puede escribir en ellos, y cualquier escritura (logs, ficheros temporales, caché) falla con Permission denied. En Kubernetes estándar funcionaba porque usaba el UID de la imagen.

La solución es preparar la imagen para el grupo 0: chgrp -R 0 /app && chmod -R g=u /app, escribir en directorios montados como volúmenes emptyDir (/tmp) y no depender de un UID fijo ni de entradas en /etc/passwd. No se debe resolver dando a la aplicación la SCC anyuid, que relaja la seguridad.

Qué está evaluando la pregunta

OpenShift aplica SCC (Security Context Constraints) por defecto, ejecutando el contenedor con un UID aleatorio y sin permisos de root. Si la aplicación intenta escribir en directorios sin permisos del GID 0 o asume ser root, fallará.

Nivel 3 (Entrevista Senior) ¿Cómo diseñarías una estrategia de Zero-Downtime Deployment para un microservicio Spring Boot con conexiones de base de datos activas en Kubernetes? Ver respuesta

Respuesta razonada

Combinando varias piezas. En el Deployment, estrategia RollingUpdate con maxUnavailable: 0 y maxSurge: 1, para que siempre haya capacidad completa. Una readiness probe que solo dé éxito cuando la aplicación puede atender, de modo que el nuevo pod no recibe tráfico antes de tiempo. Un PodDisruptionBudget para que mantenimientos del clúster no tiren demasiados pods a la vez.

En el apagado: server.shutdown=graceful y spring.lifecycle.timeout-per-shutdown-phase para terminar las peticiones en curso; un preStop con una breve espera (sleep 5) porque los endpoints del Service tardan unos segundos en dejar de enviar tráfico tras la señal; y terminationGracePeriodSeconds mayor que la suma. HikariCP cierra sus conexiones limpiamente al cerrar el contexto. Por último, las migraciones de base de datos deben ser compatibles con la versión anterior (patrón expand/contract), porque durante el despliegue conviven ambas versiones.

Qué está evaluando la pregunta

Usa terminationGracePeriodSeconds (e.g. 30-60s) en el Pod spec, habilita Graceful Shutdown en Spring Boot (server.shutdown=graceful), configura un preStop hook con sleep 5 para dar tiempo a que los Services y endpoints actualicen su tabla de enrutamiento iptables/ipvs antes de que el proceso reciba SIGTERM, y asegura que la sonda de readiness marque al Pod como no listo inmediatamente.

Reto del tutor

Un Pod de Spring Boot tarda 45 segundos en arrancar por la inicialización de cachés y beans. Kubernetes lo mata repetidamente a los 30 segundos porque la sonda de Liveness falla con timeout. ¿Cómo lo resuelves de forma profesional?

Lo que una respuesta suele tener bien

Menciona ajustar tiempos de espera o usar Startup Probe.

Lo que suele quedarse en la superficie

Decir que aumente el initialDelaySeconds de la livenessProbe a 100 segundos.

El concepto de fondo que suele faltar

Aumentar simplemente el initialDelaySeconds de la liveness retrasa la detección de cuelgues reales en pods que ya estaban vivos. El patrón moderno de Kubernetes es añadir una startupProbe dedicada.

Cómo lo respondería alguien senior en una entrevista

La solución arquitectónica correcta es implementar una startupProbe de Kubernetes que apunte a /actuator/health/liveness con failureThreshold: 30 y periodSeconds: 2 (dándole hasta 60 segundos para arrancar). Mientras la startupProbe esté evaluándose, Kubernetes deshabilita por completo las sondas de liveness y readiness. Una vez que la startup probe pasa con éxito por primera vez, se activan las comprobaciones de liveness y readiness con intervalos estándar (e.g. periodSeconds: 5).

Proyecto «Pedidos» · paso 29 de 30

Contenedores y Kubernetes

Empaquetar los servicios y desplegarlos en un clúster local.

De qué va el proyecto

El backend de una tienda online: clientes, catálogo de productos y pedidos con sus líneas, pagos y envíos. Empieza como un proyecto Maven con records y termina como un sistema con API REST, seguridad, mensajería, contenedores y observabilidad. Cada paso construye sobre el anterior, así que trabaja siempre en el mismo repositorio.

Qué hacer

  1. Escribe un Dockerfile multi-etapa para pedidos, con usuario sin privilegios y las capas de Spring Boot.
  2. Despliega pedidos y PostgreSQL en kind o minikube con Deployment, Service y ConfigMap.
  3. Configura las probes de liveness y readiness con Actuator y -XX:MaxRAMPercentage.

Está listo cuando

  • Cambiar solo el código reconstruye la imagen en segundos gracias a la caché de capas.
  • Si la base de datos cae, el pod deja de recibir tráfico pero no se reinicia en bucle.

Katas: ejercicios cortos

Reglas de oro

  • Usa siempre -XX:MaxRAMPercentage=75.0 en lugar de -Xmx fijo dentro de contenedores.
  • Separa la Startup Probe de la Liveness Probe para aplicaciones con arranque pesado.
  • La Liveness Probe NUNCA debe comprobar bases de datos o servicios externos.
  • En OpenShift, da permisos de escritura al GID 0 para compatibilidad con SCC.
  • Usa layertools de Spring Boot para crear imágenes Docker con capas cacheadas eficientes.
  • Ejecuta el contenedor con un usuario sin privilegios (USER), nunca como root.
Se guarda automáticamente

Fuentes oficiales

Revisado el . Este día en Markdown