Google ADK: deploy a producción, Cloud Run, observabilidad y costos

Google ADK: deploy a producción, Cloud Run, observabilidad y costos

Este es el séptimo post de la serie sobre Google ADK. Los anteriores: introducción, setup, FunctionTool, arquitectura multi-agente, instructions como código, y dev experience.

Llegar a producción con un agente de IA tiene consideraciones que no aparecen en el desarrollo local: gestión de secretos, latencia percibida por el usuario, observabilidad de llamadas a LLMs, y —el tema que más se ignora— el modelo de costos que puede sorprenderte si no lo entendés bien.

En este post vamos a cubrir todo el camino desde el código local hasta un deployment listo para producción en Google Cloud.

Gemini API key vs Vertex AI — cuándo usar cada uno

Durante el desarrollo usamos GOOGLE_GENAI_USE_VERTEXAI=FALSE con una API key de Gemini. Para producción, la decisión depende de tus requisitos:

CriterioGemini API keyVertex AI
SetupMínimo — solo una API keyProyecto GCP, billing habilitado
CostoPay-per-use, sin mínimoPay-per-use + posible committed use
SLASin SLA formalSLA de GCP (99.9%+)
Región de datosGlobalConfigurable (GDPR compliance)
Rate limitsMás ajustados en free tierCuotas ampliables
AutenticaciónAPI keyIAM / Service Account
Ideal paraPrototipos, proyectos chicos, startupsProducción enterprise, compliance, alto volumen

Para habilitar Vertex AI solo cambiás las variables de entorno:

GOOGLE_GENAI_USE_VERTEXAI=TRUE
GOOGLE_CLOUD_PROJECT=tu-proyecto-id
GOOGLE_CLOUD_LOCATION=us-central1  # o southamerica-east1 para latency desde Argentina

El código del agente no cambia nada. ADK maneja la autenticación vía Application Default Credentials (ADC).

Containerización

ADK expone dos modos de servidor:

  1. HTTP server — para integraciones REST y webhooks
  2. gRPC server — para integraciones con otros servicios GCP

Para Cloud Run el más común es HTTP. El Dockerfile:

FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-slim AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package.json .

ENV PORT=8080
EXPOSE 8080

CMD ["node", "dist/server.js"]

El entrypoint para producción es diferente al de desarrollo:

// src/server.ts
import 'dotenv/config';
import { HttpServer } from '@google/adk/server';
import { rootAgent } from './agent.js';

const port = parseInt(process.env.PORT ?? '8080');

const server = new HttpServer({
  agent: rootAgent,
  port,
});

server.start();
console.log(`Servidor corriendo en puerto ${port}`);

Nota: La API de HttpServer puede variar entre versiones de ADK. Revisá la documentación actual del paquete para la importación exacta.

Deploy en Cloud Run

# Build y push de la imagen
docker build -t gcr.io/TU_PROYECTO/financial-advisor:latest .
docker push gcr.io/TU_PROYECTO/financial-advisor:latest

# Deploy a Cloud Run
gcloud run deploy financial-advisor \
  --image gcr.io/TU_PROYECTO/financial-advisor:latest \
  --region us-central1 \
  --platform managed \
  --allow-unauthenticated \
  --set-env-vars GOOGLE_GENAI_USE_VERTEXAI=TRUE,GOOGLE_CLOUD_PROJECT=TU_PROYECTO \
  --memory 512Mi \
  --cpu 1 \
  --min-instances 0 \
  --max-instances 10 \
  --concurrency 80

Una observación sobre --min-instances 0: con agentes de IA que hacen llamadas a Gemini, el cold start de la instancia de Node (~1-2s) es irrelevante comparado con la latencia de la llamada al LLM (2-10s). Empezá con min-instances 0 y ajustá si el cold start se vuelve perceptible.

Gestión de secretos

Nunca hardcodees API keys ni metas .env en la imagen Docker. El flujo correcto con Secret Manager:

# Crear el secreto
echo -n "tu_gemini_api_key" | gcloud secrets create GOOGLE_GENAI_API_KEY \
  --data-file=- \
  --replication-policy=automatic

# Dar acceso al service account de Cloud Run
gcloud secrets add-iam-policy-binding GOOGLE_GENAI_API_KEY \
  --member="serviceAccount:TU_SA@TU_PROYECTO.iam.gserviceaccount.com" \
  --role="roles/secretmanager.secretAccessor"

# Deploy con el secreto montado como variable de entorno
gcloud run deploy financial-advisor \
  --set-secrets GOOGLE_GENAI_API_KEY=GOOGLE_GENAI_API_KEY:latest

Con --set-secrets, Cloud Run inyecta el secreto como variable de entorno en runtime. El código accede a process.env.GOOGLE_GENAI_API_KEY exactamente igual que en desarrollo.

Estrategia de evaluación — más allá del vibe check

El "vibe check" — probar el agente a mano y decir "parece que funciona bien" — falla en producción por cuatro razones:

  1. No determinismo: el mismo prompt puede producir respuestas distintas en cada ejecución
  2. Regresiones invisibles: mejorar el comportamiento en un área puede romper silenciosamente otra
  3. Sesgo humano: los evaluadores manuales son inconsistentes entre sesiones
  4. No escala: el review manual no puede correr en cada commit

La solución es evaluación sistemática con datasets dorados y métricas cuantitativas.

Golden datasets — la base de la evaluación

[
  {
    "prompt": "Analizá los fundamentos de MSFT",
    "expected_agent": "stock_research_agent",
    "expected_tools": ["analyze_stock_fundamentals", "get_analyst_ratings"],
    "criteria": "La respuesta debe incluir P/E ratio, precio objetivo de analistas, y una recomendación clara"
  },
  {
    "prompt": "Gano $5000/mes y gasto $4200. ¿Cómo mejoro?",
    "expected_agent": "budget_savings_agent",
    "expected_tools": ["analyze_budget", "find_savings_opportunities"],
    "criteria": "La respuesta debe calcular la tasa de ahorro, identificar al menos 2 oportunidades, y dar próximos pasos concretos"
  }
]

Mantenés este archivo en el repositorio y lo corrés en CI. El dataset es inmutable una vez en producción — nuevas capacidades se prueban con prompts adicionales, no modificando los existentes.

Shadow deployments con Cloud Run revision tags

# Deploy del nuevo código a una revisión oculta con cero tráfico público
gcloud run deploy financial-advisor \
  --image gcr.io/$PROJECT_ID/financial-advisor:$COMMIT_SHA \
  --region us-central1 \
  --no-traffic \
  --tag candidate

# Correr evaluación contra la URL de la revisión candidata
node eval/run_evaluation.js --url https://candidate---financial-advisor-xxxxx-uc.a.run.app

# Si pasa la evaluación → promover a 100% del tráfico
gcloud run services update-traffic financial-advisor \
  --to-tags candidate=100 \
  --region us-central1

Dos tipos de métricas

Basadas en código (determinísticas):

  • ¿El agente correcto fue invocado? (expected_agent === actual_agent)
  • ¿Las herramientas correctas fueron llamadas? (set intersection)
  • ¿La respuesta contiene los campos requeridos?

Basadas en modelo (LLM-as-judge):

  • ¿La respuesta es factualmente correcta?
  • ¿Contiene alucinaciones vs los datos de mercado?
  • ¿El consejo dado es apropiado para el perfil del usuario?
const judgePrompt = `
Evaluá la siguiente respuesta de un asesor financiero.

Pregunta del usuario: "${testCase.prompt}"
Respuesta del agente: "${agentResponse}"

Criterio: ${testCase.criteria}

¿La respuesta cumple el criterio? Responde SOLO con un JSON:
{ "passed": boolean, "reason": "explicación en una oración" }
`;

const judgement = await gemini.generate(judgePrompt);
const { passed, reason } = JSON.parse(judgement.text);

Observabilidad — qué monitorear en agentes de IA

Los agentes tienen métricas que el monitoreo tradicional no captura. Las importantes:

Latencia por paso del ciclo:

  • Tiempo de routing del orquestador
  • Tiempo de ejecución de herramientas (tu código)
  • Tiempo de llamada al LLM (Gemini)
  • Latencia total end-to-end

Métricas de comportamiento:

  • Tasa de tool calls por conversación (muy baja = el LLM no usa herramientas)
  • Distribución de agentes invocados (detecta si el routing es como esperás)
  • Errores en herramientas (retornos con status: "error")

El logging estructurado es la base:

// src/logger.ts
export const logger = {
  info: (message: string, data?: object) => {
    console.log(
      JSON.stringify({
        severity: 'INFO',
        message,
        timestamp: new Date().toISOString(),
        ...data,
      })
    );
  },
  error: (message: string, error?: unknown, data?: object) => {
    console.error(
      JSON.stringify({
        severity: 'ERROR',
        message,
        error:
          error instanceof Error
            ? { message: error.message, stack: error.stack }
            : error,
        timestamp: new Date().toISOString(),
        ...data,
      })
    );
  },
};

Cloud Logging parsea automáticamente JSON en stdout/stderr y hace los campos filtrables.

El modelo de costos de Gemini — sin sorpresas

Gemini cobra por tokens (entrada + salida). Una conversación típica con nuestro asesor financiero:

Sistema (instrucciones):      ~2,000 tokens  (fijas por conversación)
Historial de conversación:    ~1,000 tokens  (crece con cada turno)
Schemas de herramientas:      ~3,000 tokens  (fijos, los schemas de Zod)
Mensaje del usuario:          ~100 tokens
Tool calls + respuestas:      ~1,500 tokens
Respuesta del agente:         ~500 tokens
─────────────────────────────────────────────
Total por turno:              ~8,000 tokens
Costo estimado:               ~$0.005 por turno

Para un sistema con 100 conversaciones de 5 turnos por día: ~4M tokens/día, ~$75/mes. Ese número varía enormemente según el largo de las instrucciones y cuántos sub-agentes participan.

El token cost killer en multi-agente: cada delegación a un sub-agente es una llamada adicional al LLM con su propio contexto. Esto multiplica el costo.

Estrategias para reducir costos:

  • Instrucciones concisas. Cada token en el system prompt se paga en cada llamada. Revisalas periódicamente y sacá lo que no aporta.
  • Modelo por agente según complejidad. Agentes que hacen tareas simples no necesitan el modelo más potente.
  • Caching de contexto. Para el mismo system prompt que se repite en cada llamada, Gemini ofrece context caching disponible en Vertex AI — un 75% de descuento en tokens cacheados.

Seguridad en capas

Capa 1: Validación en herramientas (inputs numéricos, rangos, tipos)

execute: ({ annualIncome, age }) => {
  if (annualIncome < 0 || annualIncome > 10_000_000) {
    return {
      status: 'error',
      message: 'Ingreso fuera de rango válido (0–10M)',
    };
  }
  if (age < 18 || age > 100) {
    return { status: 'error', message: 'Edad inválida' };
  }
};

Capa 2: Sanitización de input con beforeAgentCallback

import type { CallbackContext, Content } from '@google/adk';

const sanitizeInput = (context: CallbackContext): Content | undefined => {
  const userMessage = context.userContent?.parts?.[0]?.text ?? '';

  const injectionPatterns = [
    /ignore previous instructions/i,
    /disregard your system prompt/i,
  ];

  if (injectionPatterns.some(p => p.test(userMessage))) {
    return {
      role: 'model',
      parts: [{ text: 'No puedo procesar ese tipo de solicitud.' }],
    };
  }

  if (userMessage.length > 5000) {
    return {
      role: 'model',
      parts: [
        {
          text: 'El mensaje es demasiado largo. Por favor resumí tu consulta.',
        },
      ],
    };
  }

  return undefined; // Continuar con el procesamiento normal
};

Capa 3: Model Armor para producción enterprise

Para aplicaciones de alta escala, Google Cloud Model Armor actúa como firewall que analiza prompts y respuestas: detección de prompt injection, redacción de PII, filtros de Responsible AI, y logging para auditoría.

Checklist de producción

Infraestructura:
  [ ] API key / Vertex AI configurado
  [ ] Secretos en Secret Manager (no hardcodeados)
  [ ] Cloud Run con min-instances y max-instances apropiados
  [ ] Timeout configurado (--timeout=300 para conversaciones largas)
  [ ] CI/CD pipeline con typecheck + unit tests antes del deploy

Observabilidad:
  [ ] Logging estructurado (JSON a stdout)
  [ ] Alertas en Cloud Monitoring para error rate y latencia
  [ ] Presupuesto configurado en Billing para alertas de costo

Calidad y evaluación:
  [ ] Unit tests de todas las herramientas
  [ ] Golden dataset con prompts, agentes esperados, y criterios de aceptación
  [ ] Evaluación automática corriendo en CI con umbral de calidad (ej. 80% pass rate)
  [ ] Shadow deployment via revision tags antes de promover a producción
  [ ] Plan de rollback (Cloud Run soporta traffic splitting entre revisions)

Seguridad:
  [ ] beforeAgentCallback con sanitización básica de input
  [ ] Validación de rangos en todas las herramientas
  [ ] Red team tests incorporados al golden dataset

Cierre de la serie (parte 1 de 2)

En siete posts cubrimos el ciclo completo desde los conceptos de ADK hasta un sistema en producción. Los takeaways más importantes:

  1. Las herramientas son la unidad de composición — diseñalas bien, testealas en unitario, y el resto se vuelve más fácil.
  2. Las instrucciones son código — versionarlas, revisarlas, y mantenerlas concisas porque cada token cuesta en producción.
  3. La descripción del sub-agente determina el routing — el campo más crítico y el más fácil de ignorar.
  4. El modelo de costos requiere atención — especialmente en multi-agente donde cada delegación multiplica los tokens.
  5. La evaluación es un gate, no una revisión — golden datasets + shadow deployments + métricas automáticas en CI son lo que separa un prototipo de un sistema de producción.

En el próximo post cerramos la serie integrando el agente con una app web en Next.js.

Recursos:

Comments

Share your thoughts and join the discussion


Related Posts

Google ADK: dev experience, testing y debugging

Google ADK: dev experience, testing y debugging

11 min read

Desarrollar sistemas de agentes tiene un ciclo de feedback diferente al desarrollo tradicional. No podés hacer un expect(agent.response).toBe("...") — el output no es determinístico. Pero sí podés testear la capa determinística, observar el comportamiento sistemáticamente, y tener confianza razonable antes de deployar.