Módulo 3: Serverless & Lambda for AI
8. Proyecto: Lambda AI Endpoint
Descripción del proyecto
Este es el proyecto integrador del Módulo 3. Vas a construir un Lambda AI Endpoint completo: una función Lambda que recibe un prompt via API Gateway, invoca GPT-4o-mini, y retorna una respuesta estructurada. Incluye un endpoint de health, configuración de timeout y memoria optimizada para AI, manejo de cold starts, CORS, autenticación por API key, y estimación de costes. Al terminar, tienes un endpoint serverless listo para producción.
Por qué importa: Este Lambda es el artefacto central de la Phase 1. En el Módulo 4, lo desplegarás en LocalStack — ejecutando este mismo código localmente sin cuenta AWS ni coste. En el Módulo 5, lo integrarás con S3 para persistir prompts y responses. Y en el Módulo 8 (Proyecto Integrador), formará parte de tu sistema desplegado en producción. Construirlo bien aquí significa que los próximos módulos se construyen sobre una base sólida.
Objetivo del proyecto
Producir un Lambda AI Endpoint funcional que:
- Recibe un prompt via POST /ask con API Gateway (HTTP API v2)
- Invoca GPT-4o-mini con timeout y memory optimizados
- Retorna respuesta estructurada con metadata (tokens, duración, cold start)
- Tiene un endpoint GET /health que verifica configuración
- Maneja errores gracefully (timeout, LLM errors, input inválido)
- Incluye CORS para invocación desde browsers
- Tiene autenticación por API key (configurable)
- Logging estructurado para CloudWatch Insights
Recap del Módulo
| Cápsula | Concepto | Lo usas en el proyecto |
|---|---|---|
| 02 | Lambda fundamentals | Handler, event, context |
| 03 | Container vs zip, cold starts | Package optimization, IS_COLD_START |
| 04 | Environment variables, secrets | API keys, config via env vars |
| 05 | Timeout y memory | 768MB, 60s timeout, client timeout |
| 06 | API Gateway | HTTP API v2, CORS, auth, routes |
| 07 | Cost estimation | Estimación de coste del endpoint |
Especificaciones Técnicas
Arquitectura
┌─────────────────────┐
POST /ask ──────────→│ │
GET /health ────────→│ API Gateway │
│ (HTTP API v2) │
│ CORS + Throttling │
└──────────┬──────────┘
│
┌──────────┴──────────┐
│ │
┌────┴────┐ ┌─────┴────┐
│ Ask │ │ Health │
│ Function│ │ Function │
│ 768MB │ │ 128MB │
│ 60s │ │ 5s │
└────┬────┘ └──────────┘
│
┌────┴────┐
│ OpenAI │
│ API │
└─────────┘
Endpoints requeridos
POST /ask → Recibe prompt, invoca LLM, retorna respuesta
GET /health → Status del endpoint y configuración
Request/Response format
# POST /ask
# Request:
{
"prompt": "¿Qué es serverless?",
"max_tokens": 500,
"system_prompt": "Responde de forma concisa y útil." # Opcional
}
# Response (200):
{
"answer": "Serverless es un modelo de ejecución...",
"model": "gpt-4o-mini",
"tokens_used": 142,
"duration_ms": 2345,
"cold_start": false
}
# Response (400):
{
"error": "prompt is required"
}
# Response (502):
{
"error": "LLM call failed: timeout"
}
# GET /health
# Response (200):
{
"status": "healthy",
"checks": {
"lambda": "up",
"openai_key": "configured"
},
"config": {
"model": "gpt-4o-mini",
"memory_mb": "768",
"timeout_s": "60",
"region": "us-east-1"
}
}
Configuración Lambda
| Parámetro | Ask Function | Health Function |
|---|---|---|
| Runtime | Python 3.11 | Python 3.11 |
| Architecture | arm64 | arm64 |
| Memory | 768 MB | 128 MB |
| Timeout | 60s | 5s |
| Handler | handler.handler | health.handler |
Archivos requeridos
lambda-ai-endpoint/
├── src/
│ ├── handler.py # POST /ask handler
│ ├── health.py # GET /health handler
│ └── requirements.txt # Dependencias (solo openai)
├── template.yaml # SAM template
├── samconfig.toml # SAM deploy config
├── env.json # Variables locales para sam local
├── events/
│ ├── ask.json # Evento de prueba para /ask
│ └── health.json # Evento de prueba para /health
├── tests/
│ └── test_handler.py # Tests unitarios
└── README.md # Documentación del endpoint
Código Completo
src/handler.py
import json
import logging
import os
import time
from openai import OpenAI
logger = logging.getLogger()
logger.setLevel(os.environ.get("LOG_LEVEL", "INFO"))
LAMBDA_TIMEOUT = int(os.environ.get("LAMBDA_TIMEOUT", "60"))
MODEL_NAME = os.environ.get("MODEL_NAME", "gpt-4o-mini")
MAX_PROMPT_LENGTH = int(os.environ.get("MAX_PROMPT_LENGTH", "10000"))
MAX_TOKENS_LIMIT = int(os.environ.get("MAX_TOKENS_LIMIT", "2000"))
DEFAULT_MAX_TOKENS = int(os.environ.get("DEFAULT_MAX_TOKENS", "500"))
DEFAULT_SYSTEM_PROMPT = os.environ.get(
"DEFAULT_SYSTEM_PROMPT",
"Responde de forma concisa y útil."
)
MIN_REMAINING_MS = 10000
client = OpenAI(
api_key=os.environ.get("OPENAI_API_KEY", ""),
timeout=LAMBDA_TIMEOUT - 10,
max_retries=1,
)
CORS_HEADERS = {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": os.environ.get("ALLOWED_ORIGIN", "*"),
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, X-Api-Key",
}
IS_COLD_START = True
def _response(status_code, body):
"""Construye un response HTTP con CORS headers."""
return {
"statusCode": status_code,
"headers": CORS_HEADERS,
"body": json.dumps(body) if isinstance(body, dict) else body,
}
def _validate_request(body):
"""Valida el request body y retorna errores o None."""
prompt = body.get("prompt", "")
if not isinstance(prompt, str) or not prompt.strip():
return "prompt is required and must be a non-empty string"
if len(prompt) > MAX_PROMPT_LENGTH:
return f"prompt too long: {len(prompt)} chars (max {MAX_PROMPT_LENGTH})"
max_tokens = body.get("max_tokens", DEFAULT_MAX_TOKENS)
if not isinstance(max_tokens, int) or max_tokens < 1 or max_tokens > MAX_TOKENS_LIMIT:
return f"max_tokens must be integer between 1 and {MAX_TOKENS_LIMIT}"
return None
def _check_auth(event):
"""Verifica API key si está configurada."""
api_keys_raw = os.environ.get("API_KEYS", "")
if not api_keys_raw:
return True
valid_keys = set(api_keys_raw.split(","))
request_key = event.get("headers", {}).get("x-api-key", "")
return request_key in valid_keys
def handler(event, context):
global IS_COLD_START
was_cold = IS_COLD_START
IS_COLD_START = False
start_time = time.time()
method = event.get("requestContext", {}).get("http", {}).get("method", "")
if method == "OPTIONS":
return _response(200, "")
if not _check_auth(event):
return _response(401, {"error": "Unauthorized: invalid or missing API key"})
remaining_ms = context.get_remaining_time_in_millis()
if remaining_ms < MIN_REMAINING_MS:
logger.warning(f"Insufficient time: {remaining_ms}ms remaining")
return _response(408, {
"error": "Insufficient time remaining for LLM call",
"remaining_ms": remaining_ms,
})
try:
body = json.loads(event.get("body", "{}"))
except (json.JSONDecodeError, TypeError):
return _response(400, {"error": "Invalid JSON body"})
validation_error = _validate_request(body)
if validation_error:
return _response(400, {"error": validation_error})
prompt = body["prompt"].strip()
max_tokens = min(body.get("max_tokens", DEFAULT_MAX_TOKENS), MAX_TOKENS_LIMIT)
system_prompt = body.get("system_prompt", DEFAULT_SYSTEM_PROMPT)
model = MODEL_NAME
if remaining_ms < 20000:
model = "gpt-4o-mini"
max_tokens = min(max_tokens, 200)
try:
llm_start = time.time()
response = client.chat.completions.create(
model=model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": prompt},
],
max_tokens=max_tokens,
)
llm_ms = round((time.time() - llm_start) * 1000)
except Exception as e:
error_ms = round((time.time() - start_time) * 1000)
logger.error(json.dumps({
"event": "llm_error",
"error": str(e),
"duration_ms": error_ms,
"cold_start": was_cold,
}))
return _response(502, {"error": f"LLM call failed: {str(e)}"})
total_ms = round((time.time() - start_time) * 1000)
answer = response.choices[0].message.content
tokens = response.usage.total_tokens
logger.info(json.dumps({
"event": "ask_success",
"cold_start": was_cold,
"llm_duration_ms": llm_ms,
"total_duration_ms": total_ms,
"overhead_ms": total_ms - llm_ms,
"tokens_used": tokens,
"model": model,
"prompt_length": len(prompt),
"remaining_ms": context.get_remaining_time_in_millis(),
}))
return _response(200, {
"answer": answer,
"model": response.model,
"tokens_used": tokens,
"duration_ms": total_ms,
"cold_start": was_cold,
})
src/health.py
import json
import os
import time
def handler(event, context):
start = time.time()
checks = {"lambda": "up"}
openai_key = os.environ.get("OPENAI_API_KEY", "")
if not openai_key:
checks["openai_key"] = "missing"
elif not openai_key.startswith("sk-"):
checks["openai_key"] = "invalid_format"
else:
checks["openai_key"] = "configured"
overall = "healthy" if checks["openai_key"] == "configured" else "degraded"
status_code = 200 if overall == "healthy" else 503
return {
"statusCode": status_code,
"headers": {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": os.environ.get("ALLOWED_ORIGIN", "*"),
},
"body": json.dumps({
"status": overall,
"checks": checks,
"config": {
"model": os.environ.get("MODEL_NAME", "gpt-4o-mini"),
"memory_mb": os.environ.get("AWS_LAMBDA_FUNCTION_MEMORY_SIZE", "unknown"),
"timeout_s": os.environ.get("LAMBDA_TIMEOUT", "60"),
"region": os.environ.get("AWS_REGION", "unknown"),
"function": os.environ.get("AWS_LAMBDA_FUNCTION_NAME", "unknown"),
"architecture": "arm64",
},
"latency_ms": round((time.time() - start) * 1000, 1),
}),
}
src/requirements.txt
openai>=1.0.0
template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Lambda AI Endpoint — Module 3 Project
Globals:
Function:
Runtime: python3.11
Architectures:
- arm64
Parameters:
OpenAiApiKey:
Type: String
NoEcho: true
Description: OpenAI API key
AllowedOrigin:
Type: String
Default: "*"
Description: CORS allowed origin
ApiKeys:
Type: String
Default: ""
Description: Comma-separated valid API keys (empty = no auth)
ModelName:
Type: String
Default: "gpt-4o-mini"
AllowedValues: ["gpt-4o-mini", "gpt-4o"]
Resources:
# --- API Gateway ---
AiApi:
Type: AWS::Serverless::HttpApi
Properties:
StageName: prod
Description: AI Endpoint HTTP API
CorsConfiguration:
AllowOrigins:
- !Ref AllowedOrigin
AllowMethods:
- POST
- GET
- OPTIONS
AllowHeaders:
- Content-Type
- X-Api-Key
MaxAge: 3600
RouteSettings:
"POST /ask":
ThrottlingBurstLimit: 50
ThrottlingRateLimit: 100
"GET /health":
ThrottlingBurstLimit: 200
ThrottlingRateLimit: 500
# --- Ask Function ---
AskFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: ai-endpoint-ask
Handler: handler.handler
CodeUri: ./src/
MemorySize: 768
Timeout: 60
Environment:
Variables:
OPENAI_API_KEY: !Ref OpenAiApiKey
MODEL_NAME: !Ref ModelName
LAMBDA_TIMEOUT: "60"
ALLOWED_ORIGIN: !Ref AllowedOrigin
API_KEYS: !Ref ApiKeys
LOG_LEVEL: INFO
DEFAULT_MAX_TOKENS: "500"
MAX_TOKENS_LIMIT: "2000"
MAX_PROMPT_LENGTH: "10000"
DEFAULT_SYSTEM_PROMPT: "Responde de forma concisa y útil."
Events:
AskRoute:
Type: HttpApi
Properties:
ApiId: !Ref AiApi
Path: /ask
Method: POST
# --- Health Function ---
HealthFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: ai-endpoint-health
Handler: health.handler
CodeUri: ./src/
MemorySize: 128
Timeout: 5
Environment:
Variables:
OPENAI_API_KEY: !Ref OpenAiApiKey
MODEL_NAME: !Ref ModelName
LAMBDA_TIMEOUT: "60"
ALLOWED_ORIGIN: !Ref AllowedOrigin
Events:
HealthRoute:
Type: HttpApi
Properties:
ApiId: !Ref AiApi
Path: /health
Method: GET
Outputs:
ApiUrl:
Description: Base URL of the API Gateway
Value: !Sub "https://${AiApi}.execute-api.${AWS::Region}.amazonaws.com/prod"
AskEndpoint:
Description: POST /ask endpoint
Value: !Sub "https://${AiApi}.execute-api.${AWS::Region}.amazonaws.com/prod/ask"
HealthEndpoint:
Description: GET /health endpoint
Value: !Sub "https://${AiApi}.execute-api.${AWS::Region}.amazonaws.com/prod/health"
AskFunctionArn:
Description: ARN of the Ask function
Value: !GetAtt AskFunction.Arn
samconfig.toml
version = 0.1
[default.deploy.parameters]
stack_name = "ai-endpoint"
resolve_s3 = true
s3_prefix = "ai-endpoint"
region = "us-east-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
parameter_overrides = "ModelName=gpt-4o-mini AllowedOrigin=*"
env.json (para sam local)
{
"AskFunction": {
"OPENAI_API_KEY": "sk-proj-your-key-here",
"MODEL_NAME": "gpt-4o-mini",
"LAMBDA_TIMEOUT": "60",
"ALLOWED_ORIGIN": "*",
"API_KEYS": "",
"LOG_LEVEL": "DEBUG",
"DEFAULT_MAX_TOKENS": "500",
"MAX_TOKENS_LIMIT": "2000",
"MAX_PROMPT_LENGTH": "10000",
"DEFAULT_SYSTEM_PROMPT": "Responde de forma concisa y útil."
},
"HealthFunction": {
"OPENAI_API_KEY": "sk-proj-your-key-here",
"MODEL_NAME": "gpt-4o-mini",
"LAMBDA_TIMEOUT": "60",
"ALLOWED_ORIGIN": "*"
}
}
events/ask.json
{
"version": "2.0",
"routeKey": "POST /ask",
"rawPath": "/prod/ask",
"headers": {
"content-type": "application/json",
"x-api-key": ""
},
"requestContext": {
"http": {
"method": "POST",
"path": "/prod/ask",
"sourceIp": "127.0.0.1"
},
"time": "08/Mar/2026:12:00:00 +0000",
"requestId": "test-request-001"
},
"body": "{\"prompt\": \"¿Qué es serverless computing? Explica en 3 puntos.\", \"max_tokens\": 300}",
"isBase64Encoded": false
}
events/health.json
{
"version": "2.0",
"routeKey": "GET /health",
"rawPath": "/prod/health",
"headers": {},
"requestContext": {
"http": {
"method": "GET",
"path": "/prod/health",
"sourceIp": "127.0.0.1"
},
"time": "08/Mar/2026:12:00:00 +0000",
"requestId": "test-request-002"
},
"isBase64Encoded": false
}
tests/test_handler.py
import json
import os
import pytest
os.environ["OPENAI_API_KEY"] = "sk-test-fake-key-for-testing"
os.environ["MODEL_NAME"] = "gpt-4o-mini"
os.environ["LAMBDA_TIMEOUT"] = "60"
os.environ["ALLOWED_ORIGIN"] = "*"
os.environ["API_KEYS"] = ""
from src.handler import _validate_request, _check_auth, _response
class TestValidation:
def test_empty_prompt_rejected(self):
error = _validate_request({"prompt": ""})
assert error is not None
assert "required" in error
def test_missing_prompt_rejected(self):
error = _validate_request({})
assert error is not None
def test_long_prompt_rejected(self):
error = _validate_request({"prompt": "x" * 10001})
assert error is not None
assert "too long" in error
def test_valid_prompt_accepted(self):
error = _validate_request({"prompt": "Hello"})
assert error is None
def test_max_tokens_validation(self):
error = _validate_request({"prompt": "Hello", "max_tokens": 5000})
assert error is not None
assert "max_tokens" in error
def test_valid_max_tokens(self):
error = _validate_request({"prompt": "Hello", "max_tokens": 500})
assert error is None
class TestAuth:
def test_no_auth_configured(self):
os.environ["API_KEYS"] = ""
assert _check_auth({"headers": {}}) is True
def test_valid_key(self):
os.environ["API_KEYS"] = "key1,key2"
assert _check_auth({"headers": {"x-api-key": "key1"}}) is True
def test_invalid_key(self):
os.environ["API_KEYS"] = "key1,key2"
assert _check_auth({"headers": {"x-api-key": "wrong"}}) is False
def test_missing_key(self):
os.environ["API_KEYS"] = "key1"
assert _check_auth({"headers": {}}) is False
class TestResponse:
def test_response_format(self):
resp = _response(200, {"answer": "test"})
assert resp["statusCode"] == 200
assert "Content-Type" in resp["headers"]
assert "Access-Control-Allow-Origin" in resp["headers"]
body = json.loads(resp["body"])
assert body["answer"] == "test"
def test_cors_headers_present(self):
resp = _response(200, {"ok": True})
assert "Access-Control-Allow-Origin" in resp["headers"]
assert "Access-Control-Allow-Methods" in resp["headers"]
Paso a Paso para Construir
1. Crear estructura del proyecto (2 min)
mkdir -p lambda-ai-endpoint/src
mkdir -p lambda-ai-endpoint/events
mkdir -p lambda-ai-endpoint/tests
cd lambda-ai-endpoint
2. Crear archivos de código (10 min)
Copia los archivos de las secciones anteriores:
# Crear handler.py, health.py, requirements.txt en src/
# Crear template.yaml, samconfig.toml, env.json en raíz
# Crear ask.json, health.json en events/
# Crear test_handler.py en tests/
3. Instalar dependencias (2 min)
# Para desarrollo local
pip install openai pytest
# SAM CLI (si no lo tienes)
pip install aws-sam-cli
4. Configurar environment (2 min)
# Edita env.json con tu OPENAI_API_KEY real
# Este archivo es para `sam local invoke` — NO lo subas a Git
echo "env.json" >> .gitignore
5. Test unitarios (3 min)
# Ejecuta los tests de validación y auth
cd lambda-ai-endpoint
PYTHONPATH=. pytest tests/test_handler.py -v
# Resultado esperado:
# test_empty_prompt_rejected PASSED
# test_missing_prompt_rejected PASSED
# test_long_prompt_rejected PASSED
# test_valid_prompt_accepted PASSED
# test_max_tokens_validation PASSED
# test_valid_max_tokens PASSED
# test_no_auth_configured PASSED
# test_valid_key PASSED
# test_invalid_key PASSED
# test_missing_key PASSED
# test_response_format PASSED
# test_cors_headers_present PASSED
6. Test local con SAM (5 min)
# Test individual del handler
sam local invoke AskFunction \
--event events/ask.json \
--env-vars env.json
# Test del health check
sam local invoke HealthFunction \
--event events/health.json \
--env-vars env.json
# Test con API Gateway local
sam local start-api --env-vars env.json
# → API running at http://127.0.0.1:3000
# En otra terminal:
curl http://127.0.0.1:3000/health
curl -X POST http://127.0.0.1:3000/ask \
-H "Content-Type: application/json" \
-d '{"prompt": "¿Qué es serverless?", "max_tokens": 200}'
7. Preview con LocalStack (5 min)
En el Módulo 4 harás esto en detalle, pero puedes probarlo ahora como preview:
# Levantar LocalStack
docker run -d --name localstack \
-p 4566:4566 \
-e SERVICES=lambda,apigateway \
localstack/localstack
# Esperar a que arranque
sleep 10
curl http://localhost:4566/_localstack/health
# Deployar el Lambda en LocalStack
samlocal deploy \
--stack-name ai-endpoint \
--resolve-s3 \
--parameter-overrides \
OpenAiApiKey=sk-proj-your-key \
AllowedOrigin=* \
--no-confirm-changeset
# samlocal es SAM configurado para usar LocalStack
# Si no lo tienes: pip install aws-sam-cli-local
8. Build y package (3 min)
# Build (instala dependencias en un container)
sam build
# Verificar que el build es correcto
ls .aws-sam/build/AskFunction/
# handler.py health.py requirements.txt openai/ ...
9. Deploy config (solo si tienes cuenta AWS)
# Si tienes cuenta AWS y quieres deployar:
sam deploy --guided
# Te preguntará:
# Stack Name: ai-endpoint
# AWS Region: us-east-1
# Parameter OpenAiApiKey: sk-proj-...
# Parameter AllowedOrigin: *
# Parameter ApiKeys: (enter para sin auth)
# Parameter ModelName: gpt-4o-mini
# Confirm changes? [y/N]: y
# Si NO tienes cuenta AWS: usa LocalStack (Módulo 4)
10. Verificar deployment (3 min)
# Obtener URL del API
aws cloudformation describe-stacks \
--stack-name ai-endpoint \
--query 'Stacks[0].Outputs'
# O si deployaste con sam deploy:
# La URL aparece en los Outputs al final del deploy
# Test health
curl https://YOUR-API-ID.execute-api.us-east-1.amazonaws.com/prod/health
# Test ask
curl -X POST https://YOUR-API-ID.execute-api.us-east-1.amazonaws.com/prod/ask \
-H "Content-Type: application/json" \
-d '{"prompt": "¿Qué es Lambda?", "max_tokens": 200}'
Cold Start Mitigation Aplicada
Lo que ya hicimos en el código
# 1. Imports fuera del handler (se ejecutan en init, una sola vez)
import json, logging, os, time
from openai import OpenAI
# 2. Client inicializado fuera del handler (reutilizado entre invocaciones)
client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY", ""))
# 3. Variables globales calculadas una vez
CORS_HEADERS = {...}
IS_COLD_START = True
# 4. Solo dependencia necesaria en requirements.txt (openai)
# No incluimos langchain, pandas, numpy — reducen cold start
arm64 para menor cold start
# template.yaml — arm64 reduce cold start y coste
Globals:
Function:
Architectures:
- arm64 # ~5-10% menos cold start + 20% menos coste
Provisioned Concurrency (opcional)
# Solo si cold starts son inaceptables para tu caso
# Agrega esto al AskFunction en template.yaml:
AskFunction:
Properties:
ProvisionedConcurrencyConfig:
ProvisionedConcurrentExecutions: 2
# Mantiene 2 instancias warm 24/7
# Coste adicional: ~$16/mes (2 × 768MB × 30 días)
Timeout y Memory Configurados para AI
Justificación de los valores elegidos
Ask Function: 768MB, 60s
├── Memory: 768MB
│ ├── Suficiente CPU para TLS handshake y JSON parsing
│ ├── No desperdicia en CPU que no usa (I/O bound)
│ └── Cold start: ~2-3s (vs ~8s con 128MB)
├── Timeout: 60s
│ ├── Client timeout OpenAI: 50s (60 - 10 buffer)
│ ├── Permite respuestas de hasta ~2000 tokens en gpt-4o-mini
│ └── API Gateway limit es 29s → Lambda tiene margen
└── arm64: 20% más barato, rendimiento equivalente
Health Function: 128MB, 5s
├── Memory: 128MB (no hace trabajo pesado)
├── Timeout: 5s (solo lee env vars y retorna JSON)
└── Mínimo coste posible
El constraint de API Gateway: 29 segundos
API Gateway timeout: 29s (hard limit, no configurable)
Lambda timeout: 60s (lo que configuramos)
¿Por qué configuramos Lambda a 60s si API Gateway corta a 29s?
1. Para invocación directa (sin API Gateway), el timeout de 60s aplica
2. Para invocaciones async, Lambda puede correr los 60s completos
3. El client timeout de OpenAI es 50s como safety net
4. En práctica, gpt-4o-mini responde en 1-8s → muy dentro del límite
Si una invocación tarda >29s via API Gateway:
├── API Gateway retorna 504 al cliente
├── Lambda SIGUE ejecutando hasta su timeout o hasta terminar
└── El response se pierde (Lambda no sabe que Gateway ya cortó)
Mitigación: el handler chequea remaining_ms y usa modelo rápido
si queda poco tiempo.
Cost Estimation para Este Endpoint
Escenario: endpoint de desarrollo/aprendizaje
Parámetros:
├── 50 invocaciones/día (testing manual + demos)
├── 768MB, 4s promedio (gpt-4o-mini responde rápido)
├── arm64
Lambda:
├── 1,500 inv/mes (free tier: 1M gratis) → $0.00
├── 4,500 GB-s (free tier: 400K gratis) → $0.00
└── Total Lambda: $0.00
OpenAI:
├── 1,500 × 100 input tokens × $0.15/1M = $0.02
├── 1,500 × 300 output tokens × $0.60/1M = $0.27
└── Total OpenAI: $0.29/mes
Otros: API Gateway $0.00, CloudWatch $0.50
═══════════════════════════════════
TOTAL: ~$0.79/mes
═══════════════════════════════════
Escenario: endpoint en producción ligera
Parámetros:
├── 5,000 invocaciones/día
├── 768MB, 5s promedio
├── arm64
Lambda:
├── 150,000 inv/mes → requests: $0.00 (free tier)
├── 562,500 GB-s → billable: 162,500 × $0.0000133334 = $2.17
└── Total Lambda: $2.17
OpenAI:
├── 150,000 × 100 × $0.15/1M = $2.25
├── 150,000 × 300 × $0.60/1M = $27.00
└── Total OpenAI: $29.25/mes
Otros: API Gateway $0.15, CloudWatch $1.50
═══════════════════════════════════
TOTAL: ~$33.07/mes
├── Lambda + infra: $3.82 (12%)
├── OpenAI: $29.25 (88%)
═══════════════════════════════════
Checklist de Completitud
Código y estructura
-
src/handler.pyimplementa POST /ask con validación completa -
src/health.pyimplementa GET /health con checks de configuración -
src/requirements.txtcontiene soloopenai>=1.0.0 -
template.yamldefine ambas funciones con API Gateway HTTP API v2 -
samconfig.tomlconfigura deploy parameters -
env.jsonexiste con variables locales (NO en Git) -
events/ask.jsonyevents/health.jsonexisten para testing
Funcionalidad
- POST /ask recibe prompt y retorna respuesta del LLM
- POST /ask valida input (prompt vacío, max_tokens fuera de rango, prompt largo)
- POST /ask retorna metadata: model, tokens_used, duration_ms, cold_start
- GET /health retorna status de Lambda y configuración
- CORS headers presentes en todos los responses
- API key auth funciona cuando API_KEYS está configurado
- Sin auth cuando API_KEYS está vacío (development)
- Errores del LLM retornan 502 con mensaje descriptivo
- Timeout del LLM retorna error antes de que Lambda muera
Configuración
- Ask Function: 768MB memory, 60s timeout, arm64
- Health Function: 128MB memory, 5s timeout, arm64
- Client OpenAI timeout = Lambda timeout - 10s
- API Gateway throttling configurado (50 burst, 100 rate para /ask)
- CORS configurado para AllowedOrigin configurable
- IS_COLD_START tracking implementado
- Logging estructurado (JSON) para CloudWatch Insights
Testing
- Tests unitarios pasan: validación, auth, response format
-
sam local invokefunciona para ambas funciones -
sam local start-apilevanta y responde correctamente - (Opcional) Deploy a AWS funciona sin errores
- (Opcional) LocalStack preview funciona
Optimización
- Solo
openaicomo dependencia (mínimo cold start) - Client e imports fuera del handler (reutilizados entre invocaciones)
- arm64 configurado (20% más barato)
- Handler adapta modelo/tokens si queda poco tiempo (remaining_ms)
Troubleshooting del Proyecto
"sam local invoke falla con ModuleNotFoundError: openai"
# sam local invoke ejecuta en un container Docker
# Necesita hacer build primero para instalar dependencias
sam build
sam local invoke AskFunction --event events/ask.json --env-vars env.json
"El handler retorna null o se queda colgado"
# Verifica que env.json tiene las variables correctas
# Especialmente OPENAI_API_KEY con una key real
cat env.json | python3 -m json.tool
# Verifica que el evento tiene el formato correcto
cat events/ask.json | python3 -m json.tool
# Si se queda colgado: el SDK de OpenAI está esperando respuesta
# Verifica tu API key y conexión a internet
"CORS error al llamar desde el browser"
# Verifica que CorsConfiguration está en template.yaml
# Verifica que AllowedOrigin incluye tu origin
# Si pruebas desde localhost:3000, AllowedOrigin debe ser
# "http://localhost:3000" o "*"
# Test manual:
curl -v -X OPTIONS http://127.0.0.1:3000/ask \
-H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: POST"
"sam deploy falla con 'Unable to upload artifact'"
# SAM necesita un bucket S3 para subir el código
# Con --guided, SAM te pregunta si quieres que lo cree
sam deploy --guided
# Si ya tienes un bucket:
sam deploy --s3-bucket tu-bucket --no-confirm-changeset
Conexión con la Guía
Lo que construiste
Lambda AI Endpoint
├── Ask Function (POST /ask)
│ ├── Validación de input
│ ├── Auth por API key
│ ├── Invocación a GPT-4o-mini
│ ├── Timeout adaptativo
│ ├── Cold start tracking
│ └── Logging estructurado
├── Health Function (GET /health)
│ └── Status y configuración
├── API Gateway HTTP API v2
│ ├── CORS configurado
│ └── Throttling por ruta
└── SAM Template completo
Lo que sigue en los próximos módulos
Módulo 4 (LocalStack — AWS Local Development):
├── Este Lambda se despliega en LocalStack
├── Mismo código, mismo template, pero local y gratis
├── Agregas LocalStack como servicio en tu Docker Compose (de M2)
└── Desarrollas y testeas Lambda sin cuenta AWS
Módulo 5 (AWS Services — S3 + Lambda):
├── Agregas S3 para persistir prompts y responses
├── Lambda trigger: S3 event → procesa archivo → guarda resultado
└── Integras con el endpoint existente
Módulo 6 (Cloud Migration):
├── Tu Lambda funciona igual en LocalStack y en AWS
├── Aprendes a migrar de LocalStack a AWS con confianza
└── Environment abstraction: mismo código, diferente infraestructura
Módulo 8 (Proyecto Integrador):
├── Este Lambda puede ser parte de tu sistema final
├── O puedes elegir otra estrategia (VPS, Render, etc.)
└── La decision matrix del M1 te guía
Resumen
- Construiste un Lambda AI Endpoint completo con POST /ask y GET /health, invocando GPT-4o-mini y retornando respuestas estructuradas.
- Configuraste API Gateway HTTP API v2 con CORS, throttling y autenticación opcional por API key.
- Implementaste mitigación de cold starts: imports y client fuera del handler, arm64, y tracking de IS_COLD_START.
- Desplegaste con un template SAM que incluye ambas funciones, sus configuraciones de timeout (768MB, 60s) y memoria.
- Documentaste la estimación de costes para escenarios de desarrollo (
$0.79/mes) y producción ($33/mes). - Integraste validación de input, timeout adaptativo según remaining_ms y logging estructurado para CloudWatch Insights.
Recursos para el Proyecto
- AWS SAM CLI Documentation — Referencia completa de SAM CLI
- SAM Template Specification — Especificación del template
- Lambda Python Handler — Handler en Python
- HTTP API Payload Format — Formato del evento v2.0
- OpenAI Python SDK — SDK oficial
- LocalStack SAM Integration — Usar SAM con LocalStack
- Lambda Best Practices — Best practices oficiales
- Lambda Pricing Calculator — Calcular costes