Módulo 4: LocalStack — AWS Local Development

7. Debugging LocalStack

Descripción

En esta cápsula vas a aprender a diagnosticar y resolver los problemas más comunes cuando trabajas con LocalStack. No es teoría — es el kit de supervivencia práctico: leer logs, verificar disponibilidad de servicios, resolver errores de permisos y formato, y usar herramientas de diagnóstico. Cuando algo falla (y algo siempre falla), sabrás exactamente dónde mirar.

Contexto: En las cápsulas anteriores construiste un pipeline S3 + Lambda con environment switching. Todo funcionó cuando seguiste los pasos. Pero en la realidad, los errores aparecen: LocalStack no arranca, Lambda retorna timeout, S3 dice que el bucket no existe. Esta cápsula te prepara para esos momentos. Es la diferencia entre "no funciona y no sé por qué" y "no funciona, ya sé dónde buscar."


El Flujo de Diagnóstico

Cuando algo falla, sigue este orden

1. ¿LocalStack está corriendo?
   └── docker compose ps / curl health endpoint
       ↓
2. ¿El servicio que necesitas está disponible?
   └── curl /_localstack/health → revisar services
       ↓
3. ¿Tu request llega a LocalStack?
   └── docker compose logs localstack --tail 20
       ↓
4. ¿El error está en tu código o en LocalStack?
   └── Probar la misma operación con awslocal CLI
       ↓
5. ¿Es un problema conocido?
   └── Revisar la tabla de errores comunes abajo

Este flujo resuelve el 90% de los problemas. Memorízalo.


Nivel 1: ¿LocalStack Está Corriendo?

Verificar el container

# ¿Está el container up?
docker compose ps localstack

# Output posibles:
# localstack   Up (healthy)     ← Todo bien
# localstack   Up (health: starting)  ← Aún arrancando, espera
# localstack   Exited (1)       ← Crasheó
# (nada)                        ← No está definido en Compose

Si el container no está corriendo

# Ver por qué se detuvo
docker compose logs localstack --tail 30

# Causas comunes:
# 1. Puerto 4566 en uso
lsof -i :4566
# Si hay otro proceso, mátalo o cambia el puerto

# 2. Docker daemon no está corriendo
docker info
# Si falla, arranca Docker Desktop

# 3. Imagen no descargada
docker pull localstack/localstack:latest

Si el container está en "health: starting"

# LocalStack tarda 10-20 segundos en arrancar
# Espera y verifica:
for i in {1..10}; do
  STATUS=$(docker compose ps localstack --format json | python3 -c "import json,sys; data=json.load(sys.stdin); print(data.get('Health','unknown'))" 2>/dev/null || echo "checking")
  echo "Intento $i: $STATUS"
  if [ "$STATUS" = "healthy" ]; then
    echo "LocalStack ready"
    break
  fi
  sleep 3
done

Nivel 2: ¿Los Servicios Están Disponibles?

Health endpoint

# El health endpoint muestra el estado de cada servicio
curl -s http://localhost:4566/_localstack/health | python3 -m json.tool

# Output esperado:
# {
#     "services": {
#         "s3": "available",
#         "lambda": "available"
#     },
#     "version": "3.x.x"
# }

Interpretar los estados

"available"  → Servicio listo para usar
"running"    → Servicio arrancando (espera unos segundos)
"disabled"   → No incluido en SERVICES
"error"      → Problema al inicializar (ver logs)
(no aparece) → No está configurado en SERVICES

Script de health check completo

# health_check.py
import requests
import json
import sys

ENDPOINT = "http://localhost:4566"

def check_localstack():
    """Diagnóstico completo de LocalStack."""
    print("=== LocalStack Health Check ===\n")

    # 1. Conectividad
    try:
        resp = requests.get(f"{ENDPOINT}/_localstack/health", timeout=5)
        health = resp.json()
        print(f"Versión: {health.get('version', 'unknown')}")
    except requests.ConnectionError:
        print("ERROR: No se puede conectar a LocalStack")
        print(f"  Verificar: docker compose ps localstack")
        print(f"  Verificar: puerto 4566 accesible")
        sys.exit(1)
    except Exception as e:
        print(f"ERROR: {e}")
        sys.exit(1)

    # 2. Servicios
    services = health.get("services", {})
    print(f"\nServicios ({len(services)}):")
    all_ok = True
    for name, status in services.items():
        icon = "✅" if status == "available" else "❌"
        print(f"  {icon} {name}: {status}")
        if status != "available":
            all_ok = False

    # 3. Verificar S3
    print("\nVerificación S3:")
    try:
        import boto3
        s3 = boto3.client(
            "s3", endpoint_url=ENDPOINT,
            aws_access_key_id="test", aws_secret_access_key="test",
            region_name="us-east-1",
        )
        buckets = s3.list_buckets()
        print(f"  ✅ S3 responde — {len(buckets['Buckets'])} buckets")
    except Exception as e:
        print(f"  ❌ S3 error: {e}")
        all_ok = False

    # 4. Verificar Lambda
    print("\nVerificación Lambda:")
    try:
        lam = boto3.client(
            "lambda", endpoint_url=ENDPOINT,
            aws_access_key_id="test", aws_secret_access_key="test",
            region_name="us-east-1",
        )
        functions = lam.list_functions()
        print(f"  ✅ Lambda responde — {len(functions['Functions'])} funciones")
    except Exception as e:
        print(f"  ❌ Lambda error: {e}")
        all_ok = False

    # Resultado
    print(f"\n{'='*30}")
    if all_ok:
        print("Estado: HEALTHY — todo funcionando")
    else:
        print("Estado: DEGRADED — revisar servicios con errores")

    return all_ok


check_localstack()

Nivel 3: Leer Logs del Container

Logs básicos

# Últimas 50 líneas
docker compose logs localstack --tail 50

# Seguir en tiempo real (útil mientras debuggeas)
docker compose logs localstack -f

# Solo errores
docker compose logs localstack 2>&1 | grep -i "error\|exception\|failed"

# Logs con timestamp
docker compose logs localstack --timestamps --tail 20

Habilitar DEBUG mode

Si los logs normales no dan suficiente información, habilita DEBUG:

# docker-compose.yml
localstack:
  environment:
    - DEBUG=1  # Cambiar de 0 a 1
# Reiniciar para aplicar
docker compose restart localstack

# Ahora los logs muestran detalles de cada request
docker compose logs localstack -f
# Verás cada request HTTP que llega a LocalStack

Vuelve a DEBUG=0 cuando termines — DEBUG genera muchos logs.

Logs de Lambda específicamente

# Cuando invocas una Lambda, LocalStack loguea la ejecución
# Busca las líneas relevantes:
docker compose logs localstack 2>&1 | grep -A5 "lambda.*invoke\|lambda.*create"

# Si LAMBDA_EXECUTOR=docker, las Lambdas corren en containers separados
# Lista containers de Lambda:
docker ps | grep "lambda"

# Ver logs de un container Lambda específico:
docker logs <container-id>

Nivel 4: Errores Comunes y Soluciones

Error 1: "Unable to connect to endpoint URL"

botocore.exceptions.EndpointConnectionError:
Could not connect to the endpoint URL: "http://localhost:4566/"

Causa: LocalStack no está corriendo o el puerto no es accesible.

# Diagnóstico:
docker compose ps localstack
curl http://localhost:4566/_localstack/health

# Solución:
docker compose up -d localstack
# Esperar al health check
sleep 15
curl http://localhost:4566/_localstack/health

Error 2: "NoSuchBucket"

botocore.exceptions.ClientError: An error occurred (NoSuchBucket)
when calling the PutObject operation

Causa: El bucket no existe. En LocalStack Community, los buckets se pierden al reiniciar.

# Diagnóstico:
awslocal s3 ls
# ¿Tu bucket aparece?

# Solución:
awslocal s3 mb s3://ai-input
awslocal s3 mb s3://ai-output

# Prevención: usa init scripts
# init-scripts/setup.sh crea los buckets al arrancar

Error 3: "ResourceNotFoundException" (Lambda)

botocore.exceptions.ClientError: An error occurred (ResourceNotFoundException)
when calling the Invoke operation: Function not found

Causa: La función Lambda no existe o el nombre no coincide.

# Diagnóstico:
awslocal lambda list-functions --query 'Functions[].FunctionName'

# ¿Tu función aparece? ¿El nombre coincide exactamente?
# Lambda names son case-sensitive

# Solución:
# Si no existe, despliégala:
awslocal lambda create-function --function-name ai-processor ...

# Si el nombre no coincide, usa el nombre correcto

Error 4: "Lambda timeout"

Task timed out after X seconds

Causa: Tu handler tarda más que el timeout configurado.

# Diagnóstico:
awslocal lambda get-function-configuration \
  --function-name ai-processor \
  --query 'Timeout'
# ¿Es suficiente para tu AI workload?

# Solución:
awslocal lambda update-function-configuration \
  --function-name ai-processor \
  --timeout 120

Error 5: "ModuleNotFoundError en Lambda"

[ERROR] Runtime.ImportModuleError:
Unable to import module 'handler': No module named 'openai'

Causa: Las dependencias no están en el zip de deployment.

# Diagnóstico:
unzip -l lambda/handler.zip | head -20
# ¿Aparecen los módulos de openai?

# Solución: re-empaquetar con dependencias
pip install -r lambda/requirements.txt -t lambda/package/
cp lambda/handler.py lambda/package/
cd lambda/package && zip -r ../handler.zip . && cd ../..

# Actualizar:
awslocal lambda update-function-code \
  --function-name ai-processor \
  --zip-file fileb://lambda/handler.zip

Error 6: "Lambda no puede conectar a S3"

botocore.exceptions.EndpointConnectionError:
Could not connect to the endpoint URL: "http://localhost:4566/"

Causa: Lambda corre en un container Docker separado. localhost dentro de ese container no es tu máquina.

# Diagnóstico:
awslocal lambda get-function-configuration \
  --function-name ai-processor \
  --query 'Environment.Variables.AWS_ENDPOINT_URL'

# Si dice "http://localhost:4566" y LAMBDA_EXECUTOR=docker → ese es el problema

# Solución:
awslocal lambda update-function-configuration \
  --function-name ai-processor \
  --environment "Variables={
    AWS_ENDPOINT_URL=http://host.docker.internal:4566,
    OPENAI_API_KEY=${OPENAI_API_KEY},
    MODEL_NAME=gpt-4o-mini
  }"
# host.docker.internal resuelve al host desde dentro de containers

# Si LAMBDA_EXECUTOR=local, localhost:4566 sí funciona

Error 7: "Port 4566 already in use"

Error starting container: port is already allocated

Causa: Otro proceso o container usa el puerto 4566.

# Diagnóstico:
lsof -i :4566

# Solución 1: matar el proceso que ocupa el puerto
kill -9 <PID>

# Solución 2: cambiar el puerto en Compose
localstack:
  ports:
    - "4567:4566"
# Y actualizar AWS_ENDPOINT_URL a http://localhost:4567

# Solución 3: si es otro container LocalStack
docker stop localstack-test && docker rm localstack-test

Herramientas de Diagnóstico

Script de diagnóstico completo

#!/bin/bash
# scripts/diagnose.sh — Diagnóstico completo de LocalStack

echo "=== DIAGNÓSTICO LOCALSTACK ==="
echo ""

echo "1. Docker"
docker --version
docker compose version
echo ""

echo "2. Container Status"
docker compose ps
echo ""

echo "3. Health Check"
HEALTH=$(curl -s http://localhost:4566/_localstack/health 2>/dev/null)
if [ $? -ne 0 ]; then
  echo "   ERROR: No se puede conectar a LocalStack"
  echo "   → Verificar: docker compose up -d localstack"
  exit 1
fi
echo "$HEALTH" | python3 -m json.tool
echo ""

echo "4. S3 Buckets"
awslocal s3 ls 2>/dev/null || echo "   S3 no disponible"
echo ""

echo "5. Lambda Functions"
awslocal lambda list-functions --query 'Functions[].{Name:FunctionName,Runtime:Runtime,Memory:MemorySize}' --output table 2>/dev/null || echo "   Lambda no disponible"
echo ""

echo "6. Últimos errores en logs"
docker compose logs localstack --tail 50 2>&1 | grep -i "error\|exception\|failed" | tail -5
if [ $? -ne 0 ]; then
  echo "   No se encontraron errores recientes"
fi
echo ""

echo "7. Recursos Docker"
docker stats --no-stream --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}" 2>/dev/null | grep -E "NAME|localstack"
echo ""

echo "8. Puerto 4566"
lsof -i :4566 2>/dev/null | head -5
echo ""

echo "=== FIN DIAGNÓSTICO ==="

Verificar una operación específica

# Si una operación falla, pruébala aislada con awslocal:

# S3: crear bucket
awslocal s3 mb s3://test-debug 2>&1
# Si funciona aquí pero no en Python → problema en tu código
# Si falla aquí también → problema en LocalStack

# Lambda: invocar
awslocal lambda invoke \
  --function-name ai-processor \
  --payload '{"document_key": "test.txt"}' \
  --cli-binary-format raw-in-base64-out \
  /tmp/debug.json 2>&1
cat /tmp/debug.json | python3 -m json.tool

# Esto aísla si el problema es tu código o LocalStack

Monitorear requests en tiempo real

# Con DEBUG=1, LocalStack loguea cada request
# En una terminal:
docker compose logs localstack -f

# En otra terminal, ejecuta tu operación:
awslocal s3 ls

# En los logs verás la request HTTP exacta
# Esto ayuda a entender qué está enviando tu código

Problemas de Rendimiento

LocalStack consume mucha RAM

# Verificar uso de memoria
docker stats --no-stream localstack

# Si consume >2GB:
# 1. Reduce los servicios habilitados
environment:
  - SERVICES=s3,lambda  # Solo lo necesario

# 2. Si LAMBDA_EXECUTOR=docker, cada Lambda crea un container
# Limpia containers viejos:
docker container prune -f

LocalStack tarda mucho en arrancar

# Causa: muchos servicios habilitados
# Solución: solo los que usas
environment:
  - SERVICES=s3,lambda
  # No: SERVICES= (todos)

# El arranque con s3,lambda tarda ~10s
# El arranque con todos los servicios puede tardar ~30-60s

Lambda tarda mucho en ejecutar

# Con LAMBDA_EXECUTOR=docker, la primera invocación es lenta
# porque crea un container Docker para la función

# Para iteración rápida, usa LAMBDA_EXECUTOR=local
environment:
  - LAMBDA_EXECUTOR=local
# Ejecuta en el proceso de LocalStack — más rápido, menos aislado

Ejercicios

Ejercicio 1: Crear el script de diagnóstico

Copia el script diagnose.sh de arriba, ejecútalo, e interpreta cada sección del output. Si algo falla, resuélvelo.

Ver solución
# Crear el script
mkdir -p scripts
cat > scripts/diagnose.sh << 'SCRIPT'
#!/bin/bash
echo "=== DIAGNÓSTICO LOCALSTACK ==="

echo "1. Container Status"
docker compose ps

echo "2. Health Check"
curl -s http://localhost:4566/_localstack/health | python3 -m json.tool 2>/dev/null || echo "LocalStack no responde"

echo "3. S3"
awslocal s3 ls 2>/dev/null || echo "S3 no disponible"

echo "4. Lambda"
awslocal lambda list-functions --query 'Functions[].FunctionName' --output text 2>/dev/null || echo "Lambda no disponible"

echo "5. Errores recientes"
docker compose logs localstack --tail 20 2>&1 | grep -i "error" | tail -3 || echo "Sin errores"

echo "=== FIN ==="
SCRIPT

chmod +x scripts/diagnose.sh
./scripts/diagnose.sh

Interpretación:

  • Si Container Status no muestra localstack → docker compose up -d
  • Si Health Check falla → LocalStack no arrancó, ver logs
  • Si S3/Lambda no disponible → verificar SERVICES en Compose
  • Si hay errores → leer el mensaje y buscar en la tabla de esta cápsula

Ejercicio 2: Provocar y resolver cada error

Provoca intencionalmente 3 errores comunes y resuélvelos:

  1. Invocar una Lambda que no existe
  2. Hacer put_object a un bucket que no existe
  3. Hacer una request con el endpoint incorrecto
Ver solución
import boto3
import json

endpoint = "http://localhost:4566"
kwargs = {
    "endpoint_url": endpoint,
    "aws_access_key_id": "test",
    "aws_secret_access_key": "test",
    "region_name": "us-east-1",
}

# Error 1: Lambda que no existe
print("--- Error 1: Lambda inexistente ---")
lam = boto3.client("lambda", **kwargs)
try:
    lam.invoke(FunctionName="no-existe", Payload=b'{}')
except Exception as e:
    print(f"  Error: {type(e).__name__}: {e}")
    print("  Solución: verificar nombre con awslocal lambda list-functions")

# Error 2: Bucket que no existe
print("\n--- Error 2: Bucket inexistente ---")
s3 = boto3.client("s3", **kwargs)
try:
    s3.put_object(Bucket="no-existe", Key="test.txt", Body=b"data")
except Exception as e:
    print(f"  Error: {type(e).__name__}: {e}")
    print("  Solución: crear bucket con awslocal s3 mb s3://no-existe")

# Error 3: Endpoint incorrecto
print("\n--- Error 3: Endpoint incorrecto ---")
bad_s3 = boto3.client(
    "s3",
    endpoint_url="http://localhost:9999",
    aws_access_key_id="test",
    aws_secret_access_key="test",
    region_name="us-east-1",
)
try:
    bad_s3.list_buckets()
except Exception as e:
    print(f"  Error: {type(e).__name__}: {e}")
    print("  Solución: verificar que AWS_ENDPOINT_URL=http://localhost:4566")

Ejercicio 3: Health check con alertas

Extiende el script health_check.py para que verifique:

  • Que los buckets ai-input y ai-output existen
  • Que la función ai-processor está desplegada
  • Que una invocación de prueba retorna status 200
Ver solución
import boto3
import json
import sys

ENDPOINT = "http://localhost:4566"
KWARGS = {
    "endpoint_url": ENDPOINT,
    "aws_access_key_id": "test",
    "aws_secret_access_key": "test",
    "region_name": "us-east-1",
}

checks = {}

# 1. Conectividad
try:
    import requests
    resp = requests.get(f"{ENDPOINT}/_localstack/health", timeout=5)
    checks["connectivity"] = "PASS"
except Exception:
    checks["connectivity"] = "FAIL — LocalStack no responde"
    for name, result in checks.items():
        print(f"  {'✅' if result == 'PASS' else '❌'} {name}: {result}")
    sys.exit(1)

# 2. Buckets requeridos
s3 = boto3.client("s3", **KWARGS)
for bucket in ["ai-input", "ai-output"]:
    try:
        s3.head_bucket(Bucket=bucket)
        checks[f"bucket_{bucket}"] = "PASS"
    except Exception:
        checks[f"bucket_{bucket}"] = f"FAIL — bucket '{bucket}' no existe"

# 3. Lambda function
lam = boto3.client("lambda", **KWARGS)
try:
    lam.get_function(FunctionName="ai-processor")
    checks["lambda_ai_processor"] = "PASS"
except Exception:
    checks["lambda_ai_processor"] = "FAIL — función 'ai-processor' no existe"

# 4. Invocación de prueba (solo si la función existe)
if checks.get("lambda_ai_processor") == "PASS":
    try:
        resp = lam.invoke(
            FunctionName="ai-processor",
            InvocationType="RequestResponse",
            Payload=json.dumps({"document_key": "test-health.txt"}),
        )
        result = json.loads(resp["Payload"].read())
        status = result.get("statusCode", 0)
        checks["lambda_invoke"] = f"PASS (status={status})" if status in [200, 400, 404] else f"FAIL (status={status})"
    except Exception as e:
        checks["lambda_invoke"] = f"FAIL — {e}"

# Report
print("=== Pipeline Health Check ===\n")
all_pass = True
for name, result in checks.items():
    icon = "✅" if "PASS" in result else "❌"
    print(f"  {icon} {name}: {result}")
    if "FAIL" in result:
        all_pass = False

print(f"\nEstado: {'HEALTHY' if all_pass else 'NEEDS ATTENTION'}")

Ejercicio 4: Automatizar recovery

Crea un script que detecte problemas y los resuelva automáticamente: si los buckets no existen, los crea. Si la Lambda no está desplegada, la despliega. Si LocalStack no responde, lo reinicia.

Ver solución
#!/bin/bash
# scripts/auto-recover.sh
set -e

echo "=== Auto-Recovery ==="

# 1. ¿LocalStack responde?
echo "Verificando LocalStack..."
if ! curl -s http://localhost:4566/_localstack/health > /dev/null 2>&1; then
  echo "  LocalStack no responde — reiniciando..."
  docker compose restart localstack
  echo "  Esperando 20 segundos..."
  sleep 20
  if ! curl -s http://localhost:4566/_localstack/health > /dev/null 2>&1; then
    echo "  FATAL: LocalStack no arranca. Revisar docker compose logs localstack"
    exit 1
  fi
fi
echo "  OK"

# 2. ¿Buckets existen?
echo "Verificando buckets..."
for BUCKET in ai-input ai-output; do
  if ! awslocal s3 ls "s3://$BUCKET" > /dev/null 2>&1; then
    echo "  Creando bucket: $BUCKET"
    awslocal s3 mb "s3://$BUCKET"
  else
    echo "  Bucket $BUCKET: OK"
  fi
done

# 3. ¿Lambda existe?
echo "Verificando Lambda..."
if ! awslocal lambda get-function --function-name ai-processor > /dev/null 2>&1; then
  echo "  Lambda ai-processor no existe"
  if [ -f "lambda/processor.zip" ]; then
    echo "  Desplegando desde lambda/processor.zip..."
    awslocal lambda create-function \
      --function-name ai-processor \
      --runtime python3.11 \
      --handler processor.handler \
      --zip-file fileb://lambda/processor.zip \
      --role arn:aws:iam::000000000000:role/lambda-role \
      --timeout 90 --memory-size 768 \
      --environment "Variables={OPENAI_API_KEY=${OPENAI_API_KEY},MODEL_NAME=gpt-4o-mini,INPUT_BUCKET=ai-input,OUTPUT_BUCKET=ai-output,AWS_ENDPOINT_URL=http://host.docker.internal:4566}" > /dev/null
    echo "  Lambda desplegada"
  else
    echo "  WARN: lambda/processor.zip no encontrado — despliega manualmente"
  fi
else
  echo "  Lambda ai-processor: OK"
fi

echo ""
echo "=== Recovery completo ==="

Troubleshooting Adicional

"LocalStack funciona pero mi app en Docker Compose no conecta"

# Dentro de Compose, usa el hostname del servicio, no localhost
# Tu app debe usar: http://localstack:4566
# NO: http://localhost:4566

# Verificar:
docker compose exec api env | grep AWS_ENDPOINT
# Debe mostrar: http://localstack:4566

"Los datos se pierden al reiniciar"

# Community Edition: persistence limitada
# Solución 1: volumen Docker
volumes:
  - localstack_data:/var/lib/localstack

# Solución 2: init scripts (recomendado)
# Recrea todo al arrancar automáticamente
volumes:
  - ./init-scripts:/etc/localstack/init/ready.d

"awslocal retorna 'command not found'"

# Instalar
pip install awscli-local

# Si pip no está en PATH:
python -m pip install awscli-local

# Alternativa: usar aws con --endpoint-url
aws --endpoint-url=http://localhost:4566 s3 ls

Resumen

  • El flujo de diagnóstico es: container corriendo → servicio disponible → request llega → error en código vs LocalStack → error conocido.
  • curl /_localstack/health es tu primer comando cuando algo falla.
  • docker compose logs localstack muestra qué está pasando internamente.
  • DEBUG=1 activa logs detallados de cada request (útil pero verbose).
  • Los errores más comunes: bucket no existe, Lambda no desplegada, endpoint incorrecto (localhost vs host.docker.internal), dependencias no empaquetadas.
  • Scripts de diagnóstico automatizan la verificación y ahorran tiempo.
  • Auto-recovery detecta y resuelve problemas comunes sin intervención manual.

Recursos Adicionales

  1. LocalStack Troubleshooting Guide — Guía oficial de troubleshooting
  2. LocalStack GitHub Issues — Issues conocidos y soluciones
  3. Docker Compose Logs — Referencia de docker compose logs
  4. LocalStack Internal Endpoints — Endpoints de diagnóstico
  5. Docker Debug Commands — Comandos de debugging Docker
  6. boto3 Error Handling — Manejo de errores en boto3