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 |
|---|---|---|---|---|
| kubernetes-demo-apps-01 (Python) | Python 3.12 | Flask + prometheus-client | Counter, Histogram, Gauge | /api/users, /api/heavy, /api/cache |
| kubernetes-demo-apps-02 (Java) | 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:
- Python: kubernetes-demo-apps-01
- Java: kubernetes-demo-apps-02
Clona el repo y entra a la carpeta de cada app:
git clone https://github.com/pescarcena/blog-pescarcena-code.git
cd blog-pescarcena-code
# Python
cd kubernetes-demo-apps-01
# Java
cd kubernetes-demo-apps-02Vamos a recorrer las partes clave de cada app.
App Python: Flask + prometheus-client
Estructura del proyecto
kubernetes-demo-apps-01/
├── 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
kubernetes-demo-apps-02/
├── 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 kubernetes-demo-apps-01
# Build
docker build -t ghcr.io/<TU_USUARIO>/python-metrics-demo:latest .
# Push
docker push ghcr.io/<TU_USUARIO>/python-metrics-demo:latestJava:
cd kubernetes-demo-apps-02
# 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 ./kubernetes-demo-apps-01
docker build -t java-metrics-demo:latest ./kubernetes-demo-apps-02
# 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 kubernetes-demo-apps-01
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 kubernetes-demo-apps-02
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": "[email protected]"},
{"id": 2, "name": "Bob", "email": "[email protected]"},
{"id": 3, "name": "Charlie", "email": "[email protected]"}
]
# 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
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
Código fuente
Todo el código está disponible en GitHub:
- Python Demo: kubernetes-demo-apps-01
- Java Demo: kubernetes-demo-apps-02
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!