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:

  1. Recibe un prompt via POST /ask con API Gateway (HTTP API v2)
  2. Invoca GPT-4o-mini con timeout y memory optimizados
  3. Retorna respuesta estructurada con metadata (tokens, duración, cold start)
  4. Tiene un endpoint GET /health que verifica configuración
  5. Maneja errores gracefully (timeout, LLM errors, input inválido)
  6. Incluye CORS para invocación desde browsers
  7. Tiene autenticación por API key (configurable)
  8. Logging estructurado para CloudWatch Insights

Recap del Módulo

CápsulaConceptoLo usas en el proyecto
02Lambda fundamentalsHandler, event, context
03Container vs zip, cold startsPackage optimization, IS_COLD_START
04Environment variables, secretsAPI keys, config via env vars
05Timeout y memory768MB, 60s timeout, client timeout
06API GatewayHTTP API v2, CORS, auth, routes
07Cost estimationEstimació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ámetroAsk FunctionHealth Function
RuntimePython 3.11Python 3.11
Architecturearm64arm64
Memory768 MB128 MB
Timeout60s5s
Handlerhandler.handlerhealth.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.py implementa POST /ask con validación completa
  • src/health.py implementa GET /health con checks de configuración
  • src/requirements.txt contiene solo openai>=1.0.0
  • template.yaml define ambas funciones con API Gateway HTTP API v2
  • samconfig.toml configura deploy parameters
  • env.json existe con variables locales (NO en Git)
  • events/ask.json y events/health.json existen 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 invoke funciona para ambas funciones
  • sam local start-api levanta y responde correctamente
  • (Opcional) Deploy a AWS funciona sin errores
  • (Opcional) LocalStack preview funciona

Optimización

  • Solo openai como 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

  1. AWS SAM CLI Documentation — Referencia completa de SAM CLI
  2. SAM Template Specification — Especificación del template
  3. Lambda Python Handler — Handler en Python
  4. HTTP API Payload Format — Formato del evento v2.0
  5. OpenAI Python SDK — SDK oficial
  6. LocalStack SAM Integration — Usar SAM con LocalStack
  7. Lambda Best Practices — Best practices oficiales
  8. Lambda Pricing Calculator — Calcular costes