Contenido

Kubernetes en práctica: Desplegando apps Python y Java con métricas, HPA y Grafana

Actualizado en marzo 2026: Código y manifiestos verificados con Kubernetes v1.35, Prometheus 3.x y autoscaling/v2.

Pre-requisitos

Este es el capítulo práctico de la serie. Necesitas todo lo anterior funcionando:

Herramientas adicionales para este capítulo:

  • Docker para compilar las imágenes.
  • Git y una cuenta en GitHub para subir las imágenes al Container Registry.

¿Qué vamos a construir?

Vamos a desplegar dos APIs reales en nuestro cluster de Kind, cada una en un lenguaje diferente, ambas exponiendo métricas a Prometheus:

AppLenguajeFrameworkMétricasEndpoints
python-metrics-demoPython 3.12Flask + prometheus-clientCounter, Histogram, Gauge/api/users, /api/heavy, /api/cache
java-metrics-demoJava 21Spring Boot + MicrometerCounter, Timer, Gauge/api/products, /api/orders, /api/heavy

Ambas apps tienen:

  • Un endpoint /api/heavy que consume CPU — perfecto para disparar el HPA.
  • Métricas de requests, latencia y caché expuestas en formato Prometheus.
  • Health checks para los probes de Kubernetes.
  • Manifiestos completos: Deployment, Service, ServiceMonitor y HPA.
Arquitectura del demo

Dos APIs desplegadas en Kind con métricas, HPA y monitoreo en Grafana


El código fuente

El código completo de ambas apps está disponible en GitHub:

Clona ambos repos:

git clone https://github.com/pescarcena/python-metrics-demo.git
git clone https://github.com/pescarcena/java-metrics-demo.git

Vamos a recorrer las partes clave de cada app.


App Python: Flask + prometheus-client

Estructura del proyecto

python-metrics-demo/
├── app.py                 # Código de la API
├── requirements.txt       # Dependencias
├── Dockerfile             # Imagen Docker
├── k8s/
│   ├── deployment.yaml    # Deployment de K8s
│   ├── service.yaml       # Service
│   ├── servicemonitor.yaml # ServiceMonitor para Prometheus
│   └── hpa.yaml           # HPA por CPU y memoria
└── .github/
    └── workflows/
        └── build-push.yaml # CI/CD para GitHub Actions

Las métricas que expone

En app.py definimos tres tipos de métricas de Prometheus:

from prometheus_client import Counter, Histogram, Gauge

# Counter: cuenta requests totales (solo sube)
REQUEST_COUNT = Counter(
    "http_requests_total",
    "Total de requests HTTP",
    ["method", "endpoint", "status"],
)

# Histogram: mide la distribución de latencia
REQUEST_LATENCY = Histogram(
    "http_request_duration_seconds",
    "Duración de requests HTTP en segundos",
    ["method", "endpoint"],
    buckets=[0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0],
)

# Gauge: valor que sube y baja (items en caché)
ITEMS_IN_CACHE = Gauge(
    "app_cache_items_total",
    "Cantidad de items en caché",
)

Las métricas se instrumentan automáticamente con decoradores before_request y after_request de Flask — cada request que entra se cuenta y se mide su latencia sin tocar la lógica de negocio.

El endpoint heavy (para probar HPA)

@app.route("/api/heavy")
def heavy_endpoint():
    """Endpoint que consume CPU — útil para probar el HPA."""
    total = 0
    for i in range(random.randint(500_000, 2_000_000)):
        total += i * i
    return jsonify({"result": total, "message": "Heavy computation done"})

Este endpoint hace cálculos pesados a propósito. Cuando lo llames repetidamente, el uso de CPU subirá y el HPA escalará los Pods.

El endpoint /metrics

@app.route("/metrics")
def metrics():
    return Response(generate_latest(), mimetype=CONTENT_TYPE_LATEST)

Prometheus scrapea este endpoint. Si accedes a localhost:8080/metrics verás algo como:

# HELP http_requests_total Total de requests HTTP
# TYPE http_requests_total counter
http_requests_total{endpoint="/api/users",method="GET",status="200"} 42.0
http_requests_total{endpoint="/api/heavy",method="GET",status="200"} 15.0

# HELP http_request_duration_seconds Duración de requests HTTP en segundos
# TYPE http_request_duration_seconds histogram
http_request_duration_seconds_bucket{endpoint="/api/users",le="0.1"} 38.0
http_request_duration_seconds_bucket{endpoint="/api/users",le="0.25"} 42.0
...

App Java: Spring Boot + Micrometer

Estructura del proyecto

java-metrics-demo/
├── pom.xml                         # Dependencias Maven
├── Dockerfile                      # Multi-stage build
├── src/main/java/com/demo/metricsapi/
│   ├── MetricsApiApplication.java  # Main class
│   ├── ApiController.java          # Endpoints de la API
│   └── MetricsConfig.java          # Configuración de Micrometer
├── src/main/resources/
│   └── application.yaml            # Config de Spring Boot
├── k8s/
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── servicemonitor.yaml
│   └── hpa.yaml
└── .github/
    └── workflows/
        └── build-push.yaml

Las métricas que expone

Spring Boot con Micrometer y el registry de Prometheus hace la mayor parte del trabajo automáticamente. Solo necesitas agregar la dependencia:

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

Y en application.yaml habilitar el endpoint:

management:
  endpoints:
    web:
      exposure:
        include: health,prometheus,info
  prometheus:
    metrics:
      export:
        enabled: true

Con esto, Spring Boot expone automáticamente métricas de JVM, HTTP, Tomcat, conexiones y más en /actuator/prometheus.

Para métricas custom, usamos anotaciones y el registry:

// Timer automático con @Timed
@GetMapping("/api/products")
@Timed(value = "http_request_duration_seconds", extraTags = {"endpoint", "/api/products"})
public List<Map<String, Object>> getProducts() { ... }

// Counter manual
this.ordersCounter = Counter.builder("app_orders_total")
        .description("Total de órdenes procesadas")
        .register(registry);

// Gauge que trackea el tamaño del cache
Gauge.builder("app_cache_items_total", cache, ConcurrentHashMap::size)
        .register(registry);

Diferencia clave: /metrics vs /actuator/prometheus

Python (prometheus-client)Java (Micrometer)
Endpoint: /metricsEndpoint: /actuator/prometheus
Métricas definidas manualmenteMuchas métricas automáticas (JVM, HTTP, etc.)
Formato Prometheus nativoFormato Prometheus vía Micrometer registry

Esto es importante para los ServiceMonitors: cada app tiene un path diferente donde Prometheus debe scrapear.


Compilar y subir las imágenes a GHCR

Vamos a compilar las imágenes Docker y subirlas al GitHub Container Registry (GHCR) para que Kind pueda usarlas.

Opción A: CI/CD automático con GitHub Actions

Ambos repos incluyen un workflow en .github/workflows/build-push.yaml que compila y sube la imagen automáticamente cuando haces push a main. Solo necesitas:

  1. Crear los repos en GitHub.
  2. Hacer push del código.
  3. El workflow se ejecuta solo.

Las imágenes quedarán en:

  • ghcr.io/<tu-usuario>/python-metrics-demo:latest
  • ghcr.io/<tu-usuario>/java-metrics-demo:latest

Opción B: Build y push manual

Si prefieres hacerlo a mano:

# Login en GHCR
echo $GITHUB_TOKEN | docker login ghcr.io -u <TU_USUARIO> --password-stdin

Python:

cd python-metrics-demo

# Build
docker build -t ghcr.io/<TU_USUARIO>/python-metrics-demo:latest .

# Push
docker push ghcr.io/<TU_USUARIO>/python-metrics-demo:latest

Java:

cd java-metrics-demo

# Build (multi-stage: compila con Maven + crea imagen ligera)
docker build -t ghcr.io/<TU_USUARIO>/java-metrics-demo:latest .

# Push
docker push ghcr.io/<TU_USUARIO>/java-metrics-demo:latest
Build y push de imágenes

Compilando y subiendo las imágenes Docker al GitHub Container Registry

Cargar imágenes en Kind (alternativa sin GHCR)

Si no quieres usar GHCR, puedes cargar las imágenes directamente en Kind:

# Build local
docker build -t python-metrics-demo:latest ./python-metrics-demo
docker build -t java-metrics-demo:latest ./java-metrics-demo

# Cargar en Kind
kind load docker-image python-metrics-demo:latest --name mi-cluster
kind load docker-image java-metrics-demo:latest --name mi-cluster

Si usas esta opción, cambia la imagen en los Deployments a python-metrics-demo:latest y java-metrics-demo:latest (sin el prefijo ghcr.io) y agrega imagePullPolicy: Never.


Desplegando en Kubernetes

Ahora viene lo divertido. Vamos a desplegar todo en nuestro cluster.

Paso 1: Actualizar las imágenes en los manifiestos

Edita los k8s/deployment.yaml de cada app y reemplaza <TU_USUARIO> con tu usuario de GitHub:

# En ambos deployment.yaml, cambia:
image: ghcr.io/<TU_USUARIO>/python-metrics-demo:latest
image: ghcr.io/<TU_USUARIO>/java-metrics-demo:latest

Paso 2: Desplegar la app Python

cd python-metrics-demo

kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml
kubectl apply -f k8s/servicemonitor.yaml
kubectl apply -f k8s/hpa.yaml

Paso 3: Desplegar la app Java

cd java-metrics-demo

kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml
kubectl apply -f k8s/servicemonitor.yaml
kubectl apply -f k8s/hpa.yaml

Paso 4: Verificar que todo está corriendo

kubectl get deployments
NAME                    READY   UP-TO-DATE   AVAILABLE   AGE
python-metrics-demo     2/2     2            2           30s
java-metrics-demo       2/2     2            2           25s
kubectl get pods
NAME                                    READY   STATUS    RESTARTS   AGE
python-metrics-demo-6b8f9c7d5-abc12     1/1     Running   0          35s
python-metrics-demo-6b8f9c7d5-def34     1/1     Running   0          35s
java-metrics-demo-7c9g0d8e6-ghi56       1/1     Running   0          30s
java-metrics-demo-7c9g0d8e6-jkl78       1/1     Running   0          30s
kubectl get svc
NAME                    TYPE        CLUSTER-IP      PORT(S)    AGE
python-metrics-demo     ClusterIP   10.96.50.10     8080/TCP   40s
java-metrics-demo       ClusterIP   10.96.50.11     8080/TCP   35s
kubectl get hpa
NAME                        REFERENCE                      TARGETS           MINPODS   MAXPODS   REPLICAS
python-metrics-demo-hpa     Deployment/python-metrics-demo 5%/50%, 20%/70%   2         8         2
java-metrics-demo-hpa       Deployment/java-metrics-demo   8%/50%, 35%/70%   2         8         2
Todos los recursos desplegados

Deployments, Services, HPAs y Pods corriendo en el cluster


Probando las APIs

Usemos port-forward para probar ambas apps.

App Python

kubectl port-forward svc/python-metrics-demo 8081:8080

En otra terminal:

# Listar usuarios
curl http://localhost:8081/api/users

# Respuesta:
[
  {"id": 1, "name": "Alice", "email": "alice@example.com"},
  {"id": 2, "name": "Bob", "email": "bob@example.com"},
  {"id": 3, "name": "Charlie", "email": "charlie@example.com"}
]

# Guardar en caché
curl -X POST http://localhost:8081/api/cache/mikey/mivalue

# Ver métricas
curl http://localhost:8081/metrics

App Java

kubectl port-forward svc/java-metrics-demo 8082:8080
# Listar productos
curl http://localhost:8082/api/products

# Respuesta:
[
  {"id": 1, "name": "Laptop Pro", "price": 1299.99},
  {"id": 2, "name": "Wireless Mouse", "price": 29.99},
  {"id": 3, "name": "USB-C Hub", "price": 49.99}
]

# Crear una orden
curl -X POST http://localhost:8082/api/orders -H "Content-Type: application/json"

# Ver métricas (nota el path diferente)
curl http://localhost:8082/actuator/prometheus
Respuestas de las APIs

Ambas APIs respondiendo correctamente vía port-forward


Revisando los logs

Los logs son tu primera línea de debugging. Veamos cómo consultarlos:

# Logs de un Pod específico
kubectl logs python-metrics-demo-6b8f9c7d5-abc12

# Logs de todos los Pods de un Deployment
kubectl logs -l app=python-metrics-demo

# Seguir los logs en tiempo real
kubectl logs -l app=java-metrics-demo -f

# Logs de los últimos 5 minutos
kubectl logs -l app=python-metrics-demo --since=5m

# Logs con timestamps
kubectl logs -l app=java-metrics-demo --timestamps
Logs de los Pods

Consultando logs de los Pods con kubectl — tu primera línea de debugging

Tip: Si necesitas logs más avanzados (búsqueda, filtrado, retención), el siguiente paso natural es agregar Loki al stack de observabilidad. Prometheus es para métricas, Loki es para logs.


Verificando métricas en Prometheus

Vamos a confirmar que Prometheus está scrapeando ambas apps.

kubectl port-forward svc/kube-prometheus-stack-prometheus 9090:9090 -n monitoring

Abre http://localhost:9090/targets y busca los targets de tus apps. Deberías ver:

  • serviceMonitor/monitoring/python-metrics-demoUP
  • serviceMonitor/monitoring/java-metrics-demoUP
Targets de las apps en Prometheus

Ambas apps aparecen como targets UP en Prometheus

Prueba algunas queries en la barra de PromQL:

# Requests totales de la app Python
http_requests_total{job="python-metrics-demo"}

# Latencia p95 de la app Java (últimos 5 minutos)
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket{job="java-metrics-demo"}[5m]))

# Órdenes creadas en Java
app_orders_total{job="java-metrics-demo"}

# Items en caché de ambas apps
app_cache_items_total

# Uso de CPU de los Pods de las demos
rate(container_cpu_usage_seconds_total{pod=~"python-metrics-demo.*|java-metrics-demo.*"}[5m])
Queries PromQL

Consultando métricas de las apps en la UI de Prometheus


Visualizando en Grafana

Ahora vamos a Grafana para ver todo de forma visual.

kubectl port-forward svc/kube-prometheus-stack-grafana 3000:80 -n monitoring

Dashboard de recursos del cluster

Ve a Dashboards → Kubernetes / Compute Resources / Namespace (Pods) y selecciona el namespace default. Verás el consumo de CPU y memoria de tus apps.

Dashboard de namespace en Grafana

Vista de recursos por namespace: CPU y memoria de las apps Python y Java

Dashboard por Pod

Ve a Kubernetes / Compute Resources / Pod y selecciona uno de los Pods. Verás el detalle individual:

Dashboard por Pod en Grafana

Detalle de un Pod individual: CPU, memoria, network I/O y filesystem

Dashboard custom para las apps

Crea un nuevo dashboard con estos paneles:

Panel 1 — Request Rate (ambas apps):

sum(rate(http_requests_total{job=~"python-metrics-demo|java-metrics-demo"}[5m])) by (job, endpoint)

Panel 2 — Latencia p95:

histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{job=~"python-metrics-demo|java-metrics-demo"}[5m])) by (job, le))

Panel 3 — Pods activos (Gauge):

count by (job) (up{job=~"python-metrics-demo|java-metrics-demo"})

Panel 4 — Items en caché:

app_cache_items_total
Dashboard custom de las apps

Dashboard custom mostrando request rate, latencia p95, pods activos y caché


Probando el autoescalado con carga

Ahora la prueba de fuego: vamos a generar carga contra el endpoint /api/heavy para que el HPA escale automáticamente.

Generando carga contra Python

kubectl run load-python --image=busybox --rm -it -- /bin/sh -c \
  "while true; do wget -q -O- http://python-metrics-demo:8080/api/heavy; done"

Generando carga contra Java

En otra terminal:

kubectl run load-java --image=busybox --rm -it -- /bin/sh -c \
  "while true; do wget -q -O- http://java-metrics-demo:8080/api/heavy; done"

Observando el escalado

En otra terminal, observa los HPAs en tiempo real:

kubectl get hpa --watch
NAME                        TARGETS            MINPODS   MAXPODS   REPLICAS   AGE
python-metrics-demo-hpa     5%/50%, 20%/70%    2         8         2          10m
java-metrics-demo-hpa       8%/50%, 35%/70%    2         8         2          10m
python-metrics-demo-hpa     72%/50%, 25%/70%   2         8         2          11m
python-metrics-demo-hpa     72%/50%, 25%/70%   2         8         3          11m30s
java-metrics-demo-hpa       65%/50%, 40%/70%   2         8         2          11m30s
java-metrics-demo-hpa       65%/50%, 40%/70%   2         8         3          12m
python-metrics-demo-hpa     58%/50%, 28%/70%   2         8         4          12m30s
...
HPA escalando bajo carga

El HPA detecta alta carga de CPU y crea nuevos Pods automáticamente

Verifica que se crearon más Pods:

kubectl get pods -l app=python-metrics-demo
NAME                                    READY   STATUS    RESTARTS   AGE
python-metrics-demo-6b8f9c7d5-abc12     1/1     Running   0          12m
python-metrics-demo-6b8f9c7d5-def34     1/1     Running   0          12m
python-metrics-demo-6b8f9c7d5-ghi56     1/1     Running   0          1m
python-metrics-demo-6b8f9c7d5-jkl78     1/1     Running   0          30s

Viendo el escalado en Grafana

Abre el dashboard de namespace en Grafana y verás en tiempo real cómo sube el uso de CPU y cómo aparecen nuevos Pods:

Escalado en vivo en Grafana

Grafana mostrando en tiempo real: CPU subiendo → HPA creando Pods → CPU bajando

Detener la carga

Cuando detengas los generadores de carga (Ctrl+C en cada terminal), después de la ventana de estabilización de 5 minutos, el HPA bajará las réplicas de vuelta a 2.


Resumen de todo lo que usamos

En este capítulo pusimos a prueba todo lo aprendido en la serie. Vamos a recapitular qué recurso de Kubernetes usamos y para qué:

RecursoPara qué lo usamos
DeploymentDesplegar las apps con réplicas, rolling updates y rollback
ServiceDar un punto de acceso estable a las apps dentro del cluster
ServiceMonitorDecirle a Prometheus que scrapee nuestras apps
HPAEscalar automáticamente por CPU y memoria
Port-forwardProbar las APIs desde nuestra máquina local
Metrics ServerProveer métricas de CPU/memoria al HPA
PrometheusRecolectar y almacenar métricas de las apps
GrafanaVisualizar métricas en dashboards interactivos
kubectl logsDebugging y troubleshooting de los Pods
Probes (liveness/readiness)Verificar que las apps están sanas
Resources (requests/limits)Controlar cuánta CPU/memoria puede usar cada Pod

Lo que construimos

                    ┌────────────────────────┐
                    │    Grafana (:3000)      │
                    │   Dashboards + Alertas  │
                    └───────────┬────────────┘
                                │ consulta
                    ┌───────────▼────────────┐
                    │   Prometheus (:9090)    │
                    │   Scraping + TSDB       │
                    └──┬──────────────────┬──┘
                       │ scrape           │ scrape
            ┌──────────▼──────┐  ┌────────▼──────────┐
            │ Python Demo     │  │ Java Demo          │
            │ Flask + prom    │  │ Spring Boot + Micr │
            │ /metrics        │  │ /actuator/prom     │
            │ HPA: 2-8 pods  │  │ HPA: 2-8 pods     │
            └─────────────────┘  └────────────────────┘

Código fuente

Todo el código está disponible en GitHub:

Cada repo incluye:

  • Código fuente de la API
  • Dockerfile
  • Manifiestos de Kubernetes (k8s/)
  • GitHub Actions para CI/CD (.github/workflows/)

Referencias


Resumen

Hoy pusimos todo junto:

  • Creamos dos APIs reales (Python + Java) que exponen métricas Prometheus.
  • Las compilamos y subimos al GitHub Container Registry.
  • Las desplegamos en Kubernetes con Deployments, Services y health probes.
  • Configuramos ServiceMonitors para que Prometheus las scrapee.
  • Configuramos HPAs para escalar automáticamente por CPU y memoria.
  • Verificamos las métricas en Prometheus con queries PromQL.
  • Creamos dashboards en Grafana para visualizar todo.
  • Generamos carga y vimos en tiempo real cómo el HPA escalaba los Pods.
  • Revisamos logs con kubectl para debugging.

Este es el ciclo completo de un workload en Kubernetes: deploy → expose → monitor → autoscale. Con estos conocimientos ya puedes desplegar y operar aplicaciones reales en un cluster.


¿Te gustó este artículo? Compártelo con tu equipo. Y si tienes dudas, ¡déjame un comentario!