# 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](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**

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1763091090244/1d39752f-083f-4e15-9676-f586bd769df7.png align="center")

## **Configuración del Proyecto**

### **1\. Dependencias Maven (pom.xml)**

Lo primero es configurar las dependencias. Aquí está la parte clave del `pom.xml`:

```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**](http://application.properties)**)**

```plaintext
# 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**

```java
@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 vez
    
* `DefaultCredentialsProvider`: Busca credenciales automáticamente en:
    
    1. Variables de entorno (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
        
    2. Archivo `~/.aws/credentials`
        
    3. IAM 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:

```java
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**:

```json
{
  "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:

```java
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**:

1. Construimos el JSON del request
    
2. Creamos un `InvokeModelRequest` con el model ID y el body
    
3. El SDK se encarga de la autenticación, firma de requests, y comunicación HTTP
    
4. Recibimos un `InvokeModelResponse` con la respuesta
    
5. Parseamos el JSON de respuesta
    

#### **4\. Extracción de la Respuesta**

Claude devuelve la respuesta en un formato específico:

```java
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**:

```json
{
  "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:

```java
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/claude`
    
* Output: `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:

```java
@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**

```java
public class ChatRequest {
    @JsonProperty("question")
    private String question;
    
    @JsonProperty("max_tokens")
    private Integer maxTokens;
    
    @JsonProperty("temperature")
    private Double temperature;
    
    // Getters y setters...
}
```

### **ChatResponse**

```java
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:

```bash
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:

```bash

source .env
mvn quarkus:dev
```

**Opción B: Variables de entorno**

```bash
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`

```ini
[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:

1. Ve a [AWS Bedrock Console](https://console.aws.amazon.com/bedrock/)
    
2. Navega a **Model Catalog**
    
3. Solicita acceso a los modelos que necesites
    
4. Espera la aprobación (puede tomar unos minutos)
    

### **3\. Ejecutar el Servicio**

```bash
mvn quarkus:dev
```

El servicio estará disponible en [`http://localhost:8080`](http://localhost:8080)

### **4\. Probar el Endpoint**

```bash

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**:

```plaintext
{
  "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**

```java
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**

```java
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` - Balanceado
    
* `anthropic.claude-3-opus-20240229-v1:0` - Más capaz
    
* `amazon.titan-text-express-v1` - Modelo de AWS
    
* `meta.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:

```plaintext
export AWS_BEDROCK_MODEL_ID=arn:aws:bedrock:us-east-1:ACCOUNT_ID:inference-profile/PROFILE_NAME
```

## **Seguridad**

### **Protección de Información Sensible**

1. **No hardcodear ARNs en código**: Usa variables de entorno
    
2. **Sanitizar respuestas**: Ocultar account IDs en las respuestas de la API
    
3. **Gitignore**: Asegúrate de que `.env` esté en `.gitignore`
    

### **Verificación Antes de Commit**

```plaintext
# 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:

```plaintext
// 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:

```plaintext
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**

```plaintext
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"]
```

```plaintext
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**

* [Código completo del proyecto](https://github.com/tu-usuario/aws-genai-quarkus-microservice)
    
* [Documentación de AWS Bedrock](https://docs.aws.amazon.com/bedrock/)
    
* [AWS SDK for Java](https://github.com/aws/aws-sdk-java-v2)
    
* [Quarkus Documentation](https://quarkus.io/)
    

**Autor**: José Amadeo Díaz Díaz  
**Fecha**: Noviembre 2025
