Skip to main content

Command Palette

Search for a command to run...

Construyendo un Microservicio de IA con Quarkus y AWS Bedrock: Una Guía Completa

Updated
10 min read

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

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:

  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:

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

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

  1. Ve a AWS Bedrock Console

  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

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

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

# 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

AspectoAWS SDK DirectoLangChain4j
DependenciasMínimas (solo AWS SDK)Más dependencias
ControlTotal control sobre requests/responsesAbstracción oculta
Multi-proveedorNo (solo AWS)Sí (OpenAI, Anthropic, AWS, etc.)
Funcionalidades avanzadasNo (solo llamadas básicas)Sí (RAG, agents, chains)
ComplejidadBajaMedia-Alta
Ideal paraCasos de uso específicos, demosAplicaciones 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

More from this blog

JoeDayz

52 posts

Community Guy | Java Champion | AWS Architect | Software Architect