Tu primer microservicio cloud-native con Quarkus

Un microservicio Quarkus no es solo un endpoint REST en un contenedor. Para sobrevivir en Kubernetes necesita responder preguntas fundamentales: ¿estás vivo? ¿estás listo para recibir tráfico? ¿cómo te comportas bajo carga?

Es aquí donde MicroProfile entra en escena. Esta especificación define las respuestas estándar a estas preguntas, y Quarkus las implementa de forma optimizada. A lo largo de este artículo vamos a crear un microservicio con todo lo necesario para funcionar en un entorno cloud-native:

  1. Crear un microservicio con Health, Metrics, Config y OpenAPI
  2. Escribir casos de prueba para validar todos los endpoints
  3. Configurar logging estructurado para producción
  4. Containerizar con Podman/Docker usando el Dockerfile de Quarkus
  5. Compilar un ejecutable nativo con Mandrel (~25x más rápido de arrancar)
  6. Entender cómo Kubernetes usa cada capacidad

Al final tendrás un microservicio listo para producción y entenderás por qué cada pieza es importante.


¿Qué hace «cloud-native» a un microservicio?

Kubernetes (K8s) es un orquestador de contenedores: decide dónde ejecutar tu aplicación, cuántas instancias levantar, y qué hacer si algo falla. Pero para tomar esas decisiones, necesita que tu aplicación le informe su estado.

Un contenedor que solo expone un endpoint REST no es suficiente. Kubernetes necesita saber:

PreguntaEspecificaciónQué pasa si falla
¿Está vivo?Health (Liveness)K8s reinicia el pod
¿Está listo?Health (Readiness)K8s no le envía tráfico
¿Terminó de arrancar?Health (Startup)K8s espera antes de verificar

Además, para operar en producción necesitas:

CapacidadEspecificaciónPara qué
MonitoreoMetricsPrometheus alerta, Grafana muestra
Configuración externaConfigVariables de entorno, ConfigMaps
Logs estructurados(stdout + JSON)Loki/ELK indexan y buscan

Vamos a implementar todo esto en un microservicio simple.


Requisitos

  • Java 17 ó 21 (soportadas por Quarkus)
  • Maven 3.9 o superior
  • Podman o Docker funcionando
  • Quarkus CLI

¿No cumples todos los requisitos? Revisa Entorno de desarrollo ideal para Quarkus donde explicamos cómo instalar todo fácilmente.

Si usas Podman, te sugerimos revisar Usando Podman con Quarkus: la configuración esencial


Crear el proyecto

quarkus create app io.quarkiverso:microservicio-cloud \
  --extension=rest,smallrye-health,micrometer-registry-prometheus,smallrye-openapi,logging-json \
  --no-code

La opción --no-code evita que Quarkus genere código de ejemplo, así empezamos con un proyecto limpio.

Esto incluye:

  • REST: para exponer endpoints
  • SmallRye Health: implementación de MicroProfile Health
  • Micrometer Registry Prometheus: métricas en formato Prometheus
  • SmallRye OpenAPI: documentación automática de la API
  • Logging JSON: logs estructurados para producción

💡 ¿Por qué Micrometer y no SmallRye Metrics? Desde la versión 1.9, Quarkus recomienda Micrometer para la recolección de métricas. Aunque SmallRye Metrics implementa la especificación MicroProfile Metrics, Micrometer se alinea mejor con las necesidades actuales de entornos cloud y microservicios, ofreciendo mayor flexibilidad y compatibilidad con diferentes backends de monitoreo.

Entra al proyecto:

cd microservicio-cloud

El servicio: un saludo configurable

Ahora empecemos con algo simple: un endpoint que saluda. Sin embargo, no será un saludo cualquiera. En lugar de valores fijos, tendrá configuración externalizada y métricas automáticas.

Crea el archivo src/main/java/io/quarkiverso/SaludoResource.java:

package io.quarkiverso;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.QueryParam;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import io.micrometer.core.annotation.Counted;
import io.micrometer.core.annotation.Timed;

@Path("/saludo")
public class SaludoResource {

    @ConfigProperty(name = "saludo.mensaje", defaultValue = "Hola")
    String mensaje;

    @ConfigProperty(name = "saludo.version", defaultValue = "1.0")
    String version;

    @GET
    @Counted(value = "saludo_total", description = "Cuántas veces se llamó al endpoint")
    @Timed(value = "saludo_tiempo", description = "Tiempo de respuesta del endpoint")
    public String saludar(@QueryParam("nombre") String nombre) {
        String quien = (nombre != null) ? nombre : "viajero";
        return mensaje + ", " + quien + "! (v" + version + ")";
    }
}

Analicemos cada anotación:

  • @ConfigProperty: inyecta valores desde application.properties o variables de entorno
  • @Counted: cuenta cuántas veces se invoca el método
  • @Timed: mide el tiempo de ejecución

Crear el test

A continuación, crea el archivo src/test/java/io/quarkiverso/SaludoResourceTest.java:

package io.quarkiverso;

import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;

import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.containsString;

@QuarkusTest
class SaludoResourceTest {

    @Test
    void testSaludoEndpoint() {
        given()
          .when().get("/saludo?nombre=Quarkiverso")
          .then()
             .statusCode(200)
             .body(containsString("Quarkiverso"));
    }

    @Test
    void testHealthLive() {
        given()
          .when().get("/q/health/live")
          .then()
             .statusCode(200);
    }

    @Test
    void testHealthReady() {
        given()
          .when().get("/q/health/ready")
          .then()
             .statusCode(200);
    }

    @Test
    void testHealthStarted() {
        given()
          .when().get("/q/health/started")
          .then()
             .statusCode(200);
    }
}

Configuración externalizada

Ahora, edita src/main/resources/application.properties:

# Configuración del saludo
saludo.mensaje=Bienvenido al Quarkiverso
saludo.version=1.0

# En Kubernetes, estas propiedades vendrían de:
# - ConfigMap: SALUDO_MENSAJE, SALUDO_VERSION

Es importante entender cómo MicroProfile Config busca valores en múltiples fuentes. Si una propiedad está definida en varias, gana la de mayor precedencia:

PrecedenciaFuenteEjemplo
1 (mayor)System properties-Dsaludo.mensaje=...
2Variables de entornoSALUDO_MENSAJE=...
3application.propertiessaludo.mensaje=...

Gracias a este orden, en Kubernetes los ConfigMaps y Secrets se inyectan como variables de entorno, por lo que sobrescriben los valores de application.properties. Esto te permite empaquetar valores por defecto en tu aplicación y personalizarlos en cada entorno sin recompilar.


Health Checks

Por defecto, Quarkus expone automáticamente tres endpoints de salud:

EndpointPreguntaSi falla…
/q/health/live¿Está vivo?K8s reinicia el pod
/q/health/ready¿Está listo?K8s no le envía tráfico
/q/health/started¿Terminó de arrancar?K8s espera antes de verificar liveness

Por defecto, los tres responden «UP» si la aplicación arrancó correctamente.

¿Cuándo usa cada uno?

  • Liveness: detecta si la app está colgada (deadlock, loop infinito). Si falla, la reinicia.
  • Readiness: detecta si la app puede procesar requests (ej: conexión a DB lista). Si falla, no le envía tráfico pero no la reinicia.
  • Startup: para apps que tardan en arrancar. Evita que liveness las reinicie antes de tiempo.

Métricas

Quarkus expone métricas en /q/metrics en formato Prometheus/OpenMetrics gracias a Micrometer.

Las métricas incluyen:

  • JVM: memoria, threads, GC (solo en modo JVM)
  • HTTP: requests, tiempos de respuesta por endpoint
  • Personalizadas: tus métricas con @Counted@Timed

El formato Prometheus/OpenMetrics es compatible con:

  • Prometheus + Grafana (stack clásico)
  • OpenShift Monitoring (usa Prometheus internamente)
  • DatadogNew RelicDynatrace (scrapean formato Prometheus)

💡 ¿Y en modo nativo? Las métricas JVM reportan valores diferentes o cero (no hay carga dinámica de clases, por ejemplo). Sin embargo, tus métricas personalizadas con @Counted y @Timed funcionan exactamente igual.


OpenAPI y Swagger UI

La extensión SmallRye OpenAPI genera documentación automática:

  • /q/openapi → especificación OpenAPI en YAML
  • /q/swagger-ui → interfaz visual para probar la API

Lo mejor es que no necesitas escribir nada: la documentación se genera automáticamente desde tus endpoints REST.

ℹ️ Swagger UI no está habilitado en producción por seguridad. El endpoint /q/openapi sigue disponible (útil para API Gateways), pero también puedes desactivarlo con quarkus.smallrye-openapi.enable=false.


Logging

Otro aspecto importante es el logging. En Kubernetes, los logs deben ir a stdout (no a archivos). Afortunadamente, Quarkus ya lo hace por defecto.

Ya incluimos la extensión logging-json al crear el proyecto. Para activar logs JSON solo en producción (y mantener texto plano en desarrollo) lo configuramos de la siguiente manera:

# Desarrollo: logs en texto plano (más legibles)
%dev.quarkus.log.console.json.enabled=false

# Producción: la extensión logging-json habilita JSON por defecto

Así en quarkus dev verás logs legibles, pero en contenedores (que corren en modo prod por defecto) tendrás JSON estructurado. Cada línea de log será un objeto JSON que se puede indexar y buscar. Esto funciona con:

  • Loki (stack por defecto en OpenShift)
  • ELK/EFK (Elasticsearch + Fluentd/Filebeat + Kibana)
  • SplunkDatadog, y otros

Sin esta configuración, los logs son texto plano (útil para desarrollo, pero menos práctico para producción).


Ejecutar y probar

Vamos a poner en marcha el microservicio con:

quarkus dev

Con la aplicación corriendo, es momento de explorar lo que construimos. Para ello, abre otra terminal y prueba cada capacidad.

El endpoint principal

curl "http://localhost:8080/saludo?nombre=Quarkiverso"

Respuesta:

Bienvenido al Quarkiverso, Quarkiverso! (v1.0)

Health Checks

# Liveness: ¿está vivo?
curl http://localhost:8080/q/health/live

# Readiness: ¿está listo?
curl http://localhost:8080/q/health/ready

# Startup: ¿terminó de arrancar?
curl http://localhost:8080/q/health/started

# Todos juntos
curl http://localhost:8080/q/health

Respuesta de ejemplo:

{
    "status": "UP",
    "checks": []
}

Métricas

# Todas las métricas (formato Prometheus)
curl http://localhost:8080/q/metrics

Una vez que llames al endpoint varias veces, verás métricas como:

# HELP saludo Cuántas veces se llamó al endpoint
# TYPE saludo counter
saludo_total{class="io.quarkiverso.SaludoResource",method="saludar",result="success"} 5.0

# HELP saludo_tiempo_seconds Tiempo de respuesta del endpoint
# TYPE saludo_tiempo_seconds summary
saludo_tiempo_seconds_count{class="io.quarkiverso.SaludoResource",method="saludar"} 5.0
saludo_tiempo_seconds_sum{class="io.quarkiverso.SaludoResource",method="saludar"} 1.91333E-4

OpenAPI

Abre en el navegador en http://localhost:8080/q/swagger-ui

Allí verás tu API documentada con la opción de probarla directamente. Ejecuta el siguiente comando para verla en formato YAML.

curl http://localhost:8080/q/openapi

Configuración con variables de entorno

Una de las ventajas de MicroProfile Config es que puedes cambiar el comportamiento de tu aplicación sin tocar una línea de código. Veamos cómo funciona esto en la práctica. Detén quarkus dev (presiona ‘q’ en la terminal) y ejecuta esta línea:

SALUDO_MENSAJE="Hola desde K8s" SALUDO_VERSION="2.0" quarkus dev

De esta manera, el endpoint responde con los nuevos valores de configuración. Así es exactamente como Kubernetes la inyectaría.


Containerizar con Podman

Hasta ahora ejecutamos en modo desarrollo (Dev Mode). No obstante, es hora de empaquetar nuestra aplicación como imagen de contenedor. Quarkus genera automáticamente varios Dockerfiles en src/main/docker/:

ArchivoPara quéTamaño aprox.
Dockerfile.jvmJAR en JVM (rápido de construir)~400MB
Dockerfile.nativeEjecutable nativo sobre UBI minimal~150MB
Dockerfile.native-microNativo sobre imagen micro (sin shell ni utilidades)~50MB

Construir la imagen

# Primero, construir el JAR
quarkus build

# Luego, construir la imagen con Podman
podman build -f src/main/docker/Dockerfile.jvm \
  -t quarkiverso/microservicio-cloud .

Ejecutar el contenedor

podman run -d --name microservicio-jvm \
  -p 8081:8080 \
  -e SALUDO_MENSAJE="Hola desde JVM" \
  quarkiverso/microservicio-cloud

💡 Usamos el puerto 8081 para no interferir con quarkus dev que usa 8080.

Probar el contenedor

En este punto, puedes verificar que todo funciona como lo haría Kubernetes:

# El endpoint principal
curl "http://localhost:8081/saludo?nombre=Podman"

# Health checks (lo que K8s consultaría)
curl http://localhost:8081/q/health/live
curl http://localhost:8081/q/health/ready
curl http://localhost:8081/q/health/started

# Métricas (lo que Prometheus scrapearía)
curl http://localhost:8081/q/metrics

Ver los logs

podman logs microservicio-jvm

Verás cada línea como JSON estructurado (el modo prod activa logging-json por defecto).


Ejecutable nativo con Mandrel

Hasta ahora usamos el modo JVM, donde tu aplicación corre sobre la máquina virtual de Java. Si bien funciona correctamente, hay otra forma de viajar por el Quarkiverso: como un ejecutable nativo.

Un binario nativo no necesita JVM. Como resultado, arranca en milisegundos, consume menos memoria, y resulta perfecto para entornos donde los recursos son preciados.

Mandrel es una distribución de GraalVM optimizada para Quarkus, mantenida por Red Hat. A diferencia de GraalVM, Mandrel incluye solo lo necesario para compilar ejecutables nativos de aplicaciones Quarkus, sin componentes extras.

Compilar nativo

quarkus build --native -Dquarkus.native.container-build=true

💡 La opción container-build=true compila dentro de un contenedor Linux usando Mandrel. De esta manera, no necesitas instalar GraalVM o Mandrel localmente y el ejecutable será compatible con Docker/Podman.

Construir la imagen nativa

podman build -f src/main/docker/Dockerfile.native-micro \
  -t quarkiverso/microservicio-cloud:native .

Como resultado, la imagen native-micro usa una base minimalista (~50MB total).

Ejecutar el contenedor nativo

podman run -d --name microservicio-native \
  -p 8082:8080 \
  -e SALUDO_MENSAJE="Hola desde nativo" \
  quarkiverso/microservicio-cloud:native

Comparar tiempos de arranque

Con ambos contenedores corriendo (JVM en 8081, nativo en 8082), ejecuta:

podman logs microservicio-jvm | grep started
# JVM: started in 0.256s

podman logs microservicio-native | grep started
# Nativo: started in 0.010s  (¡25 veces más rápido!)

Probar ambos microservicios

curl "http://localhost:8081/saludo"   # JVM
curl "http://localhost:8082/saludo"   # Nativo

Cómo Kubernetes usa todo esto

Hasta este punto, hemos construido un microservicio con health checks, métricas, configuración externalizada y logging estructurado. Ahora bien, veamos cómo todas estas piezas encajan cuando tu aplicación viaja al Quarkiverso de producción.

A continuación, verás cómo en un deployment de Kubernetes cada capacidad que implementamos tiene un propósito concreto:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: microservicio-cloud
spec:
  template:
    spec:
      containers:
      - name: app
        image: quarkiverso/microservicio-cloud
        
        # Configuración desde ConfigMap
        envFrom:
        - configMapRef:
            name: saludo-config
        
        # Startup: espera a que arranque antes de verificar liveness
        startupProbe:
          httpGet:
            path: /q/health/started
            port: 8080
          failureThreshold: 30
          periodSeconds: 2
        
        # Liveness: si falla, reinicia el pod
        livenessProbe:
          httpGet:
            path: /q/health/live
            port: 8080
          periodSeconds: 10
        
        # Readiness: si falla, no le envía tráfico
        readinessProbe:
          httpGet:
            path: /q/health/ready
            port: 8080
          periodSeconds: 5
        
        # Puerto para métricas (Prometheus las scrapea)
        ports:
        - containerPort: 8080

Además, necesitarás un ConfigMap:

apiVersion: v1
kind: ConfigMap
metadata:
  name: saludo-config
data:
  SALUDO_MENSAJE: "Hola desde producción"
  SALUDO_VERSION: "1.5"

Resumen

Has recorrido un largo camino. Tu microservicio ya no es un simple endpoint: ahora es un ciudadano del Quarkiverso, preparado para sobrevivir y prosperar en Kubernetes.

CapacidadEndpoint / ConfigUso
¿Terminó de arrancar?/q/health/startedK8s startupProbe
¿Está vivo?/q/health/liveK8s livenessProbe
¿Está listo?/q/health/readyK8s readinessProbe
Monitoreo/q/metricsPrometheus scrape
ConfiguraciónVariables de entornoK8s ConfigMap/Secret
Logsstdout + JSONLoki/ELK/Splunk
Ejecutable nativoMandrelArranque en 0.010s
Documentación/q/openapi Especificación OpenApi

Cada una de estas capacidades es una estrella en la constelación cloud-native. Juntas, permiten que tu microservicio navegue el Quarkiverso de producción con total confianza.


Próximos pasos

Como usamos puertos diferentes, puedes tener todo corriendo en paralelo:

  • 8080quarkus dev (Swagger UI, Dev UI, hot reload)
  • 8081: contenedor JVM
  • 8082: contenedor nativo

Para seguir explorando en modo desarrollo:

💡 Swagger UI y Dev UI solo están disponibles en modo desarrollo (puerto 8080).


Código fuente: github.com/quarkiverso/microservicio-cloud