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:
- Cluster de Kind activo — Capítulo 1: Montar el cluster
- Saber crear Deployments, Services y manifiestos YAML — Capítulo 2: Recursos de K8s
- Entender el HPA — Capítulo 3: Autoescalado
- Prometheus y Grafana instalados en el cluster — Capítulo 4: Observabilidad
- Metrics Server instalado (lo hicimos en el capítulo 3)
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:
| App | Lenguaje | Framework | Métricas | Endpoints |
|---|---|---|---|---|
| python-metrics-demo | Python 3.12 | Flask + prometheus-client | Counter, Histogram, Gauge | /api/users, /api/heavy, /api/cache |
| java-metrics-demo | Java 21 | Spring Boot + Micrometer | Counter, Timer, Gauge | /api/products, /api/orders, /api/heavy |
Ambas apps tienen:
- Un endpoint
/api/heavyque 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.

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.gitVamos 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 ActionsLas 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.yamlLas 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: trueCon 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: /metrics | Endpoint: /actuator/prometheus |
| Métricas definidas manualmente | Muchas métricas automáticas (JVM, HTTP, etc.) |
| Formato Prometheus nativo | Formato 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:
- Crear los repos en GitHub.
- Hacer push del código.
- El workflow se ejecuta solo.
Las imágenes quedarán en:
ghcr.io/<tu-usuario>/python-metrics-demo:latestghcr.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-stdinPython:
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:latestJava:
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
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-clusterSi 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:latestPaso 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.yamlPaso 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.yamlPaso 4: Verificar que todo está corriendo
kubectl get deploymentsNAME READY UP-TO-DATE AVAILABLE AGE
python-metrics-demo 2/2 2 2 30s
java-metrics-demo 2/2 2 2 25skubectl get podsNAME 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 30skubectl get svcNAME 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 35skubectl get hpaNAME 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
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:8080En 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/metricsApp 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
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
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 monitoringAbre http://localhost:9090/targets y busca los targets de tus apps. Deberías ver:
serviceMonitor/monitoring/python-metrics-demo— UPserviceMonitor/monitoring/java-metrics-demo— UP

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])
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 monitoringDashboard 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.

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:

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 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 --watchNAME 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
...
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-demoNAME 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 30sViendo 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:

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é:
| Recurso | Para qué lo usamos |
|---|---|
| Deployment | Desplegar las apps con réplicas, rolling updates y rollback |
| Service | Dar un punto de acceso estable a las apps dentro del cluster |
| ServiceMonitor | Decirle a Prometheus que scrapee nuestras apps |
| HPA | Escalar automáticamente por CPU y memoria |
| Port-forward | Probar las APIs desde nuestra máquina local |
| Metrics Server | Proveer métricas de CPU/memoria al HPA |
| Prometheus | Recolectar y almacenar métricas de las apps |
| Grafana | Visualizar métricas en dashboards interactivos |
| kubectl logs | Debugging 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:
- Python Demo: github.com/pescarcena/python-metrics-demo
- Java Demo: github.com/pescarcena/java-metrics-demo
Cada repo incluye:
- Código fuente de la API
- Dockerfile
- Manifiestos de Kubernetes (
k8s/) - GitHub Actions para CI/CD (
.github/workflows/)
Referencias
- prometheus-client (Python) — Librería oficial de Prometheus para Python
- Micrometer — Librería de métricas para Java/Spring
- Spring Boot Actuator + Prometheus — Guía oficial de Spring Boot
- GitHub Container Registry — Documentación de GHCR
- Kind: Loading an Image — Cargar imágenes en Kind
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!