Construyendo un Microservicio de IA con Quarkus y AWS Bedrock: Una Guía Completa
Introducción
En este artículo te mostraré cómo construir un microservicio de IA generativa desde cero usando Java 21, Quarkus 3, y AWS Bedrock. A diferencia de otras soluciones que usan abstracciones como LangChain4j, aquí usaremos el AWS SDK directamente para tener control total sobre las llamadas a la API y entender exactamente qué está pasando bajo el capó.
GitHub Repo: https://github.com/joedayz/aws-genai-quarkus-microservice
¿Por qué AWS SDK directo y no LangChain4j?
LangChain4j es excelente cuando necesitas:
Abstracciones multi-proveedor (OpenAI, Anthropic, AWS, etc.)
Funcionalidades avanzadas como RAG, agents, chains
Cambiar de proveedor fácilmente
Sin embargo, para esta demo elegimos el AWS SDK directamente porque:
✅ Control total: Sabemos exactamente qué se envía y recibe
✅ Menos dependencias: Solo necesitamos el AWS SDK
✅ Más ligero: Sin capas de abstracción adicionales
✅ Mejor para demos: Más fácil de entender y seguir
✅ Ideal para casos de uso específicos: Cuando solo necesitas AWS Bedrock
Stack Tecnológico
Java 21: La última versión LTS de Java
Quarkus 3.29.2: Framework Java moderno y rápido
AWS SDK for Java 2.21.29: SDK oficial de AWS para Bedrock
RESTEasy Reactive: Para endpoints REST
Jackson: Para serialización JSON
Arquitectura del Proyecto

Configuración del Proyecto
1. Dependencias Maven (pom.xml)
Lo primero es configurar las dependencias. Aquí está la parte clave del pom.xml:
<properties>
<quarkus.platform.version>3.29.2</quarkus.platform.version>
<aws-java-sdk.version>2.21.29</aws-java-sdk.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- Quarkus BOM -->
<dependency>
<groupId>io.quarkus.platform</groupId>
<artifactId>quarkus-bom</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- AWS SDK BOM -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>bom</artifactId>
<version>${aws-java-sdk.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Quarkus -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<!-- AWS SDK for Bedrock - Esta es la dependencia clave -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>bedrockruntime</artifactId>
<version>${aws-java-sdk.version}</version>
</dependency>
</dependencies>
Nota importante: El artefacto se llama bedrockruntime (sin guión), no bedrock-runtime. Este es un error común que puede causar problemas de resolución de dependencias.
2. Configuración (application.properties)
# Server Configuration
quarkus.http.port=8080
quarkus.http.cors.enabled=true
# AWS Configuration
AWS_REGION=us-east-1
# Model Configuration
# Usa variable de entorno AWS_BEDROCK_MODEL_ID o este valor por defecto
aws.bedrock.model.id=${AWS_BEDROCK_MODEL_ID:anthropic.claude-3-5-haiku-20241022-v1:0}
Implementación del Servicio
El Servicio Principal: AwsGenAIService
Este es el corazón de nuestra aplicación. Veamos cómo funciona paso a paso:
1. Inicialización del Cliente AWS
@ApplicationScoped
public class AwsGenAIService {
@ConfigProperty(name = "aws.bedrock.model.id",
defaultValue = "anthropic.claude-3-5-haiku-20241022-v1:0")
String modelId;
@ConfigProperty(name = "AWS_REGION", defaultValue = "us-east-1")
String awsRegion;
private BedrockRuntimeClient bedrockClient;
private synchronized void initializeBedrockClient() {
if (bedrockClient == null) {
Region region = (awsRegion != null && !awsRegion.isEmpty())
? Region.of(awsRegion)
: Region.US_EAST_1;
bedrockClient = BedrockRuntimeClient.builder()
.region(region)
.credentialsProvider(DefaultCredentialsProvider.create())
.build();
LOG.info("AWS Bedrock client initialized successfully");
}
}
}
Puntos clave:
@ApplicationScoped: El servicio es un singleton que se crea una vezDefaultCredentialsProvider: Busca credenciales automáticamente en:Variables de entorno (
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY)Archivo
~/.aws/credentialsIAM roles (si corre en EC2/ECS/Lambda)
synchronized: Asegura que solo se inicialice una vez en entornos multi-threaded
2. Construcción del Request para Claude
Claude usa un formato específico de mensajes. Aquí construimos el JSON manualmente:
private String buildClaudeRequestBody(String question, Integer maxTokens, Double temperature) {
int maxTokensValue = maxTokens != null ? maxTokens : 1024;
double temperatureValue = temperature != null ? temperature : 0.7;
// Usamos ObjectMapper para construir el JSON de forma segura
ObjectNode requestBody = objectMapper.createObjectNode();
requestBody.put("anthropic_version", "bedrock-2023-05-31");
requestBody.put("max_tokens", maxTokensValue);
requestBody.put("temperature", temperatureValue);
// Construimos el array de mensajes
ArrayNode messages = objectMapper.createArrayNode();
ObjectNode userMessage = objectMapper.createObjectNode();
userMessage.put("role", "user");
userMessage.put("content", question);
messages.add(userMessage);
requestBody.set("messages", messages);
return objectMapper.writeValueAsString(requestBody);
}
Estructura del JSON generado:
{
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 1024,
"temperature": 0.7,
"messages": [
{
"role": "user",
"content": "¿Qué es AWS Bedrock?"
}
]
}
3. Invocación del Modelo
Aquí es donde hacemos la llamada real a AWS Bedrock usando el SDK:
private ChatResponse invokeClaudeModel(String question, Integer maxTokens, Double temperature) {
// Construimos el request body
String requestBody = buildClaudeRequestBody(question, maxTokens, temperature);
// Creamos el request usando el builder del SDK
InvokeModelRequest request = InvokeModelRequest.builder()
.modelId(modelId) // Puede ser un model ID o un ARN de inference profile
.body(SdkBytes.fromString(requestBody, StandardCharsets.UTF_8))
.build();
// Invocamos el modelo - esta es la llamada real a AWS
InvokeModelResponse response = bedrockClient.invokeModel(request);
// Parseamos la respuesta
String responseBody = response.body().asString(StandardCharsets.UTF_8);
JsonNode jsonResponse = objectMapper.readTree(responseBody);
// Extraemos la respuesta y los tokens usados
String answer = extractClaudeResponse(jsonResponse);
Integer tokensUsed = extractTokensUsed(jsonResponse);
// Sanitizamos el model ID para no exponer account IDs
String safeModelId = sanitizeModelId(modelId);
return new ChatResponse(answer, safeModelId, tokensUsed);
}
Flujo de la llamada:
Construimos el JSON del request
Creamos un
InvokeModelRequestcon el model ID y el bodyEl SDK se encarga de la autenticación, firma de requests, y comunicación HTTP
Recibimos un
InvokeModelResponsecon la respuestaParseamos el JSON de respuesta
4. Extracción de la Respuesta
Claude devuelve la respuesta en un formato específico:
private String extractClaudeResponse(JsonNode jsonResponse) {
JsonNode content = jsonResponse.path("content");
if (content.isArray() && content.size() > 0) {
JsonNode firstContent = content.get(0);
JsonNode text = firstContent.path("text");
if (text.isTextual()) {
return text.asText();
}
}
return "No response generated";
}
Estructura de la respuesta de Claude:
{
"content": [
{
"type": "text",
"text": "AWS Bedrock es un servicio completamente gestionado..."
}
],
"usage": {
"input_tokens": 10,
"output_tokens": 50
}
}
5. Sanitización de Información Sensible
Una práctica importante de seguridad: nunca exponer account IDs en las respuestas de la API:
private String sanitizeModelId(String modelId) {
if (modelId == null) {
return null;
}
// Si es un ARN, ocultamos el account ID
if (modelId.startsWith("arn:aws:bedrock:")) {
// Reemplazamos el account ID (12 dígitos) con ***
return modelId.replaceAll(":([0-9]{12}):", ":***:");
}
// Para model IDs normales, los devolvemos tal cual
return modelId;
}
Ejemplo:
Input:
arn:aws:bedrock:us-east-1:071173831616:inference-profile/claudeOutput:
arn:aws:bedrock:us-east-1:***:inference-profile/claude
El Endpoint REST: ChatResource
El endpoint REST es simple y delega toda la lógica al servicio:
@Path("/api/chat")
public class ChatResource {
@Inject
AwsGenAIService genAIService;
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public Response chat(ChatRequest request) {
// Validación
if (request == null || request.getQuestion() == null ||
request.getQuestion().trim().isEmpty()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(new ErrorResponse("Question is required"))
.build();
}
// Llamada al servicio
ChatResponse response = genAIService.chat(
request.getQuestion(),
request.getMaxTokens(),
request.getTemperature()
);
return Response.ok(response).build();
}
}
DTOs (Data Transfer Objects)
ChatRequest
public class ChatRequest {
@JsonProperty("question")
private String question;
@JsonProperty("max_tokens")
private Integer maxTokens;
@JsonProperty("temperature")
private Double temperature;
// Getters y setters...
}
ChatResponse
public class ChatResponse {
@JsonProperty("answer")
private String answer;
@JsonProperty("model")
private String model; // Sanitizado (sin account ID)
@JsonProperty("tokens_used")
private Integer tokensUsed;
// Getters y setters...
}
Configuración y Uso
1. Configurar Credenciales de AWS
Opción A: Archivo .env (Recomendado para desarrollo)
Crea un archivo .env en la raíz del proyecto:
AWS_REGION=us-east-1
AWS_BEDROCK_MODEL_ID=anthropic.claude-3-5-haiku-20241022-v1:0
# Opcional: Si no usas ~/.aws/credentials
# AWS_ACCESS_KEY_ID=tu_access_key
# AWS_SECRET_ACCESS_KEY=tu_secret_key
Luego carga las variables:
source .env
mvn quarkus:dev
Opción B: Variables de entorno
export AWS_REGION=us-east-1
export AWS_BEDROCK_MODEL_ID=anthropic.claude-3-5-haiku-20241022-v1:0
export AWS_ACCESS_KEY_ID=tu_access_key
export AWS_SECRET_ACCESS_KEY=tu_secret_key
Opción C: Archivo ~/.aws/credentials
[default]
aws_access_key_id = tu_access_key
aws_secret_access_key = tu_secret_key
region = us-east-1
2. Habilitar Modelos en AWS Bedrock
Antes de usar el servicio, necesitas habilitar los modelos:
Ve a AWS Bedrock Console
Navega a Model Catalog
Solicita acceso a los modelos que necesites
Espera la aprobación (puede tomar unos minutos)
3. Ejecutar el Servicio
mvn quarkus:dev
El servicio estará disponible en http://localhost:8080
4. Probar el Endpoint
curl -X POST http://localhost:8080/api/chat \
-H "Content-Type: application/json" \
-d '{
"question": "¿Qué es AWS Bedrock?",
"max_tokens": 500,
"temperature": 0.7
}'
Respuesta esperada:
{
"answer": "AWS Bedrock es un servicio completamente gestionado...",
"model": "anthropic.claude-3-5-haiku-20241022-v1:0",
"tokens_used": 150
}
Manejo de Errores
Hemos implementado un manejo de errores robusto que proporciona mensajes útiles:
Errores Comunes y Soluciones
1. Método de Pago Requerido
if (errorMessage.contains("INVALID_PAYMENT_INSTRUMENT")) {
errorMessage = String.format(
"Payment method issue for model '%s'%n%n" +
"Even if you have a payment method configured, this error can occur if:%n" +
"1. The payment method is not fully verified/activated%n" +
"2. You need to accept AWS Marketplace terms for this model%n" +
"3. The inference profile requires a Marketplace subscription%n%n" +
"To fix:%n" +
"1. Verify payment method at: https://console.aws.amazon.com/billing/home#/paymentmethods%n" +
"2. Check AWS Marketplace subscriptions%n" +
"3. Wait 15-30 minutes after making changes",
modelId
);
}
2. Model ID Inválido
if (errorMessage.contains("model identifier is invalid")) {
errorMessage = String.format(
"Invalid model identifier: '%s'%n" +
"Possible causes:%n" +
"1. Model ID format is incorrect%n" +
"2. Model is not enabled in your AWS region (%s)%n" +
"3. Model doesn't exist or has been deprecated%n%n" +
"To fix:%n" +
"1. Go to AWS Bedrock Console → Model access%n" +
"2. Enable the model for your region",
modelId, awsRegion
);
}
Modelos Disponibles
Modelos que Funcionan con On-Demand (Sin Inference Profile)
anthropic.claude-3-5-haiku-20241022-v1:0- Rápido y económico (recomendado para desarrollo)anthropic.claude-3-sonnet-20240229-v1:0- Balanceadoanthropic.claude-3-opus-20240229-v1:0- Más capazamazon.titan-text-express-v1- Modelo de AWSmeta.llama2-70b-chat-v1- Modelo de Meta
Modelos que Requieren Inference Profile
Algunos modelos más nuevos como Claude 3.5 Sonnet pueden requerir un inference profile. En ese caso, usa el ARN del profile:
export AWS_BEDROCK_MODEL_ID=arn:aws:bedrock:us-east-1:ACCOUNT_ID:inference-profile/PROFILE_NAME
Seguridad
Protección de Información Sensible
No hardcodear ARNs en código: Usa variables de entorno
Sanitizar respuestas: Ocultar account IDs en las respuestas de la API
Gitignore: Asegúrate de que
.envesté en.gitignore
Verificación Antes de Commit
# Verificar que no hay ARNs con account IDs
git diff | grep -i "arn:aws:bedrock.*[0-9]\{12\}" && echo "⚠️ ARN encontrado!" || echo "✅ OK"
# Verificar que no hay credenciales
git diff | grep -i "AWS_ACCESS_KEY_ID\|AWS_SECRET_ACCESS_KEY" && echo "⚠️ Credenciales encontradas!" || echo "✅ OK"
Ventajas de Usar AWS SDK Directamente
1. Control Total
Sabemos exactamente qué JSON se envía y cómo se parsea la respuesta:
// Construimos el request manualmente
ObjectNode requestBody = objectMapper.createObjectNode();
requestBody.put("anthropic_version", "bedrock-2023-05-31");
requestBody.put("max_tokens", maxTokensValue);
// ... más campos
// Parseamos la respuesta manualmente
JsonNode content = jsonResponse.path("content");
String text = content.get(0).path("text").asText();
2. Sin Dependencias Extra
Solo necesitamos:
AWS SDK (
bedrockruntime)Jackson (ya incluido en Quarkus)
Quarkus (framework)
No necesitamos:
LangChain4j
Otras abstracciones
Dependencias adicionales
3. Fácil de Debuggear
Podemos ver exactamente qué se envía y recibe:
LOG.infof("Sending request to AWS Bedrock model: %s", modelId);
LOG.infof("Request body: %s", requestBody);
LOG.infof("Response: %s", responseBody);
4. Optimización Específica
Podemos optimizar para el caso de uso específico de AWS Bedrock sin preocuparnos por compatibilidad con otros proveedores.
Comparación: AWS SDK vs LangChain4j
| Aspecto | AWS SDK Directo | LangChain4j |
| Dependencias | Mínimas (solo AWS SDK) | Más dependencias |
| Control | Total control sobre requests/responses | Abstracción oculta |
| Multi-proveedor | No (solo AWS) | Sí (OpenAI, Anthropic, AWS, etc.) |
| Funcionalidades avanzadas | No (solo llamadas básicas) | Sí (RAG, agents, chains) |
| Complejidad | Baja | Media-Alta |
| Ideal para | Casos de uso específicos, demos | Aplicaciones complejas, multi-proveedor |
Despliegue
Docker
FROM quay.io/quarkus/quarkus-micro-image:2.0
WORKDIR /work/
COPY target/*-runner.jar /work/application.jar
RUN chmod 775 /work/application.jar
EXPOSE 8080
CMD ["java", "-jar", "/work/application.jar"]
docker build -f Dockerfile -t aws-genai-quarkus:latest .
docker run -i --rm -p 8080:8080 \
-e AWS_ACCESS_KEY_ID=tu_key \
-e AWS_SECRET_ACCESS_KEY=tu_secret \
-e AWS_REGION=us-east-1 \
-e AWS_BEDROCK_MODEL_ID=anthropic.claude-3-5-haiku-20241022-v1:0 \
aws-genai-quarkus:latest
AWS Lambda
El servicio puede desplegarse en Lambda usando Quarkus Funqy o Amazon Lambda Runtime Interface.
Conclusión
En este artículo hemos visto cómo construir un microservicio de IA generativa usando:
✅ AWS SDK directamente (sin abstracciones)
✅ Quarkus para un framework moderno y rápido
✅ Java 21 para aprovechar las últimas características
✅ Control total sobre las llamadas a la API
✅ Código simple y fácil de entender
Esta aproximación es ideal para:
Demos y prototipos
Casos de uso específicos de AWS
Cuando necesitas control total
Aprendizaje y entendimiento profundo
Si necesitas funcionalidades más avanzadas como RAG, agents, o soporte multi-proveedor, entonces LangChain4j sería una mejor opción. Pero para muchos casos de uso, el AWS SDK directo es más que suficiente y te da un mejor entendimiento de lo que está pasando.
Recursos
Autor: José Amadeo Díaz Díaz
Fecha: Noviembre 2025




