Módulo 4: LocalStack — AWS Local Development

4. Lambda Local con LocalStack

Descripción

En esta cápsula vas a desplegar y ejecutar funciones Lambda en LocalStack. Tomas el Lambda AI Endpoint que construiste en el Módulo 3, lo despliegas en LocalStack con mínimos cambios, lo invocas, y lees los logs — todo local, todo gratis. La promesa es real: el mismo código Lambda que correría en AWS funciona en tu laptop.

Contexto: En la cápsula anterior dominaste S3 local para almacenar assets AI. Ahora agregas la segunda pieza del pipeline: Lambda. Cuando termines, tendrás tanto almacenamiento (S3) como computación (Lambda) corriendo localmente en LocalStack. La cápsula 05 conecta ambos en un pipeline completo.


Lambda en LocalStack vs AWS

Qué cambia y qué no

Lo que NO cambia:
├── Tu código handler.py — idéntico
├── La firma: handler(event, context)
├── El formato del response (statusCode, headers, body)
├── Las APIs de invocación (invoke, list-functions)
└── Las variables de entorno

Lo que SÍ cambia:
├── El endpoint: localhost:4566 en vez de lambda.us-east-1.amazonaws.com
├── El deploy: awslocal en vez de aws
├── IAM: LocalStack Community no enforce permisos
├── Cold starts: más rápidos en LocalStack (no es infra real)
└── Logs: docker compose logs en vez de CloudWatch

La diferencia clave es el endpoint. Tu Lambda es el mismo código. Lo despliegas con el mismo comando. Solo apuntas a un lugar diferente.

Cómo ejecuta Lambda LocalStack

Cuando invocas una Lambda en LocalStack, esto es lo que pasa:

1. Tu invocación llega a localhost:4566
    ↓
2. LocalStack identifica que es una invocación Lambda
    ↓
3. Si LAMBDA_EXECUTOR=docker:
   LocalStack crea un container Docker temporal
   con tu código y lo ejecuta
    ↓
4. Si LAMBDA_EXECUTOR=local:
   LocalStack ejecuta tu handler en su propio proceso
    ↓
5. El resultado se retorna a tu cliente

docker es más fiel a AWS (cada Lambda en su container aislado) pero más lento. local es más rápido pero menos fiel. Para desarrollo, ambos funcionan.


Preparar el Lambda del M3

Estructura del handler

Tu Lambda del Módulo 3 ya tiene todo lo necesario. Este es el handler simplificado que vamos a desplegar:

# lambda/handler.py
import json
import os
import time
from openai import OpenAI

client = OpenAI(
    api_key=os.environ.get("OPENAI_API_KEY", ""),
    timeout=50,
    max_retries=1,
)

MODEL_NAME = os.environ.get("MODEL_NAME", "gpt-4o-mini")
DEFAULT_MAX_TOKENS = int(os.environ.get("DEFAULT_MAX_TOKENS", "500"))

IS_COLD_START = True


def handler(event, context):
    global IS_COLD_START
    was_cold = IS_COLD_START
    IS_COLD_START = False
    start_time = time.time()

    try:
        body = json.loads(event.get("body", "{}"))
    except (json.JSONDecodeError, TypeError):
        return _response(400, {"error": "Invalid JSON body"})

    prompt = body.get("prompt", "").strip()
    if not prompt:
        return _response(400, {"error": "prompt is required"})

    max_tokens = body.get("max_tokens", DEFAULT_MAX_TOKENS)

    try:
        response = client.chat.completions.create(
            model=MODEL_NAME,
            messages=[
                {"role": "system", "content": "Responde de forma concisa y útil."},
                {"role": "user", "content": prompt},
            ],
            max_tokens=max_tokens,
        )
    except Exception as e:
        return _response(502, {"error": f"LLM call failed: {str(e)}"})

    total_ms = round((time.time() - start_time) * 1000)

    return _response(200, {
        "answer": response.choices[0].message.content,
        "model": response.model,
        "tokens_used": response.usage.total_tokens,
        "duration_ms": total_ms,
        "cold_start": was_cold,
    })


def _response(status_code, body):
    return {
        "statusCode": status_code,
        "headers": {
            "Content-Type": "application/json",
            "Access-Control-Allow-Origin": "*",
        },
        "body": json.dumps(body),
    }

Requirements

# lambda/requirements.txt
openai>=1.0.0

Empaquetar como zip

# Crear directorio de packaging
mkdir -p lambda/package

# Instalar dependencias en el directorio de package
pip install -r lambda/requirements.txt -t lambda/package/

# Copiar handler al package
cp lambda/handler.py lambda/package/

# Crear zip
cd lambda/package
zip -r ../handler.zip .
cd ../..

# Verificar tamaño
ls -lh lambda/handler.zip
# ~5-8MB esperado (openai SDK + dependencias)

Desplegar Lambda en LocalStack

Crear la función

# Asegúrate de que LocalStack está corriendo
curl http://localhost:4566/_localstack/health

# Crear la función Lambda
awslocal lambda create-function \
  --function-name ai-endpoint \
  --runtime python3.11 \
  --handler handler.handler \
  --zip-file fileb://lambda/handler.zip \
  --role arn:aws:iam::000000000000:role/lambda-role \
  --timeout 60 \
  --memory-size 768 \
  --environment "Variables={OPENAI_API_KEY=$OPENAI_API_KEY,MODEL_NAME=gpt-4o-mini,DEFAULT_MAX_TOKENS=500}"

# Output esperado:
# {
#     "FunctionName": "ai-endpoint",
#     "Runtime": "python3.11",
#     "Handler": "handler.handler",
#     "MemorySize": 768,
#     "Timeout": 60,
#     ...
# }

Notas importantes:

  • El --role es un ARN dummy — LocalStack Community no enforce IAM
  • $OPENAI_API_KEY se expande de tu shell — asegúrate de tenerla configurada
  • El zip incluye tu handler + dependencias (openai SDK)

Verificar que existe

# Listar funciones
awslocal lambda list-functions

# Ver detalles de la función
awslocal lambda get-function --function-name ai-endpoint

# Ver solo configuración
awslocal lambda get-function-configuration --function-name ai-endpoint

Actualizar la función (cuando cambies el código)

# Reempaquetar
cd lambda/package
zip -r ../handler.zip .
cd ../..

# Actualizar código
awslocal lambda update-function-code \
  --function-name ai-endpoint \
  --zip-file fileb://lambda/handler.zip

# Actualizar variables de entorno
awslocal lambda update-function-configuration \
  --function-name ai-endpoint \
  --environment "Variables={OPENAI_API_KEY=$OPENAI_API_KEY,MODEL_NAME=gpt-4o-mini,DEFAULT_MAX_TOKENS=300}"

Invocar Lambda en LocalStack

Invocación directa (síncrona)

# Invocar con un payload
awslocal lambda invoke \
  --function-name ai-endpoint \
  --payload '{"body": "{\"prompt\": \"¿Qué es LocalStack en una frase?\", \"max_tokens\": 100}"}' \
  --cli-binary-format raw-in-base64-out \
  output.json

# Ver el resultado
cat output.json | python3 -m json.tool

# Output esperado:
# {
#     "statusCode": 200,
#     "headers": {...},
#     "body": "{\"answer\": \"LocalStack es...\", \"model\": \"gpt-4o-mini\", ...}"
# }

Parsear el resultado completo

# El body viene como string JSON dentro del response
# Parsearlo:
cat output.json | python3 -c "
import json, sys
resp = json.load(sys.stdin)
body = json.loads(resp['body'])
print(f\"Answer: {body['answer']}\")
print(f\"Model: {body['model']}\")
print(f\"Tokens: {body['tokens_used']}\")
print(f\"Duration: {body['duration_ms']}ms\")
print(f\"Cold start: {body['cold_start']}\")
"

Invocación desde Python (boto3)

# invoke_lambda.py
import boto3
import json

lambda_client = boto3.client(
    "lambda",
    endpoint_url="http://localhost:4566",
    aws_access_key_id="test",
    aws_secret_access_key="test",
    region_name="us-east-1",
)

payload = {
    "body": json.dumps({
        "prompt": "Explica qué es serverless en 2 oraciones.",
        "max_tokens": 150,
    })
}

response = lambda_client.invoke(
    FunctionName="ai-endpoint",
    InvocationType="RequestResponse",
    Payload=json.dumps(payload),
)

result = json.loads(response["Payload"].read())
body = json.loads(result["body"])

print(f"Answer: {body['answer']}")
print(f"Model: {body['model']}")
print(f"Tokens: {body['tokens_used']}")
print(f"Duration: {body['duration_ms']}ms")
python invoke_lambda.py
# Answer: Serverless es un modelo de ejecución...
# Model: gpt-4o-mini
# Tokens: 89
# Duration: 2340ms

Invocación asíncrona

# Para procesamiento en background (no espera resultado)
awslocal lambda invoke \
  --function-name ai-endpoint \
  --invocation-type Event \
  --payload '{"body": "{\"prompt\": \"Analiza este texto largo...\"}"}' \
  --cli-binary-format raw-in-base64-out \
  /dev/null

# Retorna 202 inmediatamente — la función corre en background

Leer Logs

Logs desde Docker

# Ver logs del container de LocalStack
docker compose logs localstack --tail 50

# Seguir logs en tiempo real
docker compose logs localstack -f

# Filtrar por Lambda
docker compose logs localstack | grep -i "lambda"

Logs desde awslocal (CloudWatch)

# LocalStack emula CloudWatch Logs básico
awslocal logs describe-log-groups

# Ver log streams de tu función
awslocal logs describe-log-streams \
  --log-group-name /aws/lambda/ai-endpoint

# Ver eventos de log
awslocal logs get-log-events \
  --log-group-name /aws/lambda/ai-endpoint \
  --log-stream-name "$(awslocal logs describe-log-streams \
    --log-group-name /aws/lambda/ai-endpoint \
    --query 'logStreams[0].logStreamName' \
    --output text)"

Script de logs útil

#!/bin/bash
# scripts/lambda-logs.sh — Ver logs de Lambda en LocalStack

FUNCTION_NAME=${1:-ai-endpoint}

echo "=== Últimas invocaciones de $FUNCTION_NAME ==="

LOG_GROUP="/aws/lambda/$FUNCTION_NAME"

STREAMS=$(awslocal logs describe-log-streams \
  --log-group-name "$LOG_GROUP" \
  --order-by LastEventTime \
  --descending \
  --query 'logStreams[0:3].logStreamName' \
  --output text 2>/dev/null)

if [ -z "$STREAMS" ]; then
  echo "No hay logs disponibles"
  exit 0
fi

for STREAM in $STREAMS; do
  echo "--- Stream: $STREAM ---"
  awslocal logs get-log-events \
    --log-group-name "$LOG_GROUP" \
    --log-stream-name "$STREAM" \
    --query 'events[].message' \
    --output text
done

Comparar con AWS Deployment

Mismo código, diferente target

# Deploy a LocalStack
awslocal lambda create-function \
  --function-name ai-endpoint \
  --runtime python3.11 \
  --handler handler.handler \
  --zip-file fileb://lambda/handler.zip \
  --role arn:aws:iam::000000000000:role/lambda-role \
  --timeout 60 \
  --memory-size 768

# Deploy a AWS real (si tuvieras cuenta)
aws lambda create-function \
  --function-name ai-endpoint \
  --runtime python3.11 \
  --handler handler.handler \
  --zip-file fileb://lambda/handler.zip \
  --role arn:aws:iam::123456789:role/lambda-execution-role \
  --timeout 60 \
  --memory-size 768

# La ÚNICA diferencia: "awslocal" vs "aws" (y el role ARN real)

Tabla comparativa

                        LocalStack              AWS Real
─────────────────────────────────────────────────────────
Comando deploy          awslocal                aws
Endpoint                localhost:4566          lambda.region.amazonaws.com
Role                    ARN dummy               ARN real con permisos
IAM enforcement         No (Community)          Sí
Cold starts             Más rápidos             Reales (1-15s)
Logs                    docker logs / awslocal  CloudWatch
Coste                   $0                      $$ por invocación
Fidelidad               ~95% de la API          100%
Ideal para              Desarrollo, testing     Staging, producción

Deploy con Script Automatizado

Script completo de deploy

#!/bin/bash
# scripts/deploy-lambda.sh — Deploy Lambda a LocalStack

set -e

FUNCTION_NAME="ai-endpoint"
ZIP_FILE="lambda/handler.zip"
RUNTIME="python3.11"
HANDLER="handler.handler"
MEMORY=768
TIMEOUT=60

echo "=== Empaquetando Lambda ==="
cd lambda/package
zip -r ../handler.zip . -q
cd ../..
echo "Package creado: $(ls -lh $ZIP_FILE | awk '{print $5}')"

echo "=== Verificando LocalStack ==="
curl -s http://localhost:4566/_localstack/health | python3 -m json.tool

# Verificar si la función ya existe
EXISTING=$(awslocal lambda get-function --function-name $FUNCTION_NAME 2>/dev/null || true)

if [ -n "$EXISTING" ]; then
  echo "=== Actualizando función existente ==="
  awslocal lambda update-function-code \
    --function-name $FUNCTION_NAME \
    --zip-file fileb://$ZIP_FILE
else
  echo "=== Creando nueva función ==="
  awslocal lambda create-function \
    --function-name $FUNCTION_NAME \
    --runtime $RUNTIME \
    --handler $HANDLER \
    --zip-file fileb://$ZIP_FILE \
    --role arn:aws:iam::000000000000:role/lambda-role \
    --timeout $TIMEOUT \
    --memory-size $MEMORY \
    --environment "Variables={OPENAI_API_KEY=${OPENAI_API_KEY},MODEL_NAME=gpt-4o-mini}"
fi

echo "=== Verificando deploy ==="
awslocal lambda get-function-configuration \
  --function-name $FUNCTION_NAME \
  --query '{Name: FunctionName, Runtime: Runtime, Memory: MemorySize, Timeout: Timeout}' \
  --output table

echo "=== Test invoke ==="
awslocal lambda invoke \
  --function-name $FUNCTION_NAME \
  --payload '{"body": "{\"prompt\": \"ping\", \"max_tokens\": 10}"}' \
  --cli-binary-format raw-in-base64-out \
  /tmp/lambda-test.json

STATUS=$(cat /tmp/lambda-test.json | python3 -c "import json,sys; print(json.load(sys.stdin)['statusCode'])")
echo "Status: $STATUS"

if [ "$STATUS" = "200" ]; then
  echo "Deploy exitoso"
else
  echo "Deploy con problemas — revisar /tmp/lambda-test.json"
fi

Ejercicios

Ejercicio 1: Deploy y invoke de un Lambda simple

Crea una función Lambda que reciba un texto y retorne la cantidad de palabras y caracteres. Despliégala en LocalStack e invócala.

Ver solución
# lambda/word_counter.py
import json

def handler(event, context):
    try:
        body = json.loads(event.get("body", "{}"))
    except (json.JSONDecodeError, TypeError):
        return {"statusCode": 400, "body": json.dumps({"error": "Invalid JSON"})}

    text = body.get("text", "")
    if not text:
        return {"statusCode": 400, "body": json.dumps({"error": "text is required"})}

    return {
        "statusCode": 200,
        "body": json.dumps({
            "words": len(text.split()),
            "characters": len(text),
            "text_preview": text[:50],
        }),
    }
# Empaquetar (sin dependencias — solo stdlib)
cd lambda
zip word_counter.zip word_counter.py
cd ..

# Deploy
awslocal lambda create-function \
  --function-name word-counter \
  --runtime python3.11 \
  --handler word_counter.handler \
  --zip-file fileb://lambda/word_counter.zip \
  --role arn:aws:iam::000000000000:role/lambda-role \
  --timeout 10 \
  --memory-size 128

# Invoke
awslocal lambda invoke \
  --function-name word-counter \
  --payload '{"body": "{\"text\": \"LocalStack permite desarrollar contra AWS sin coste económico\"}"}' \
  --cli-binary-format raw-in-base64-out \
  output.json

cat output.json | python3 -m json.tool
# {"statusCode": 200, "body": "{\"words\": 8, \"characters\": 58, ...}"}

Ejercicio 2: Actualizar función y verificar cambio

Modifica la función word-counter para que también retorne la palabra más larga. Actualiza el código en LocalStack e invoca para verificar.

Ver solución
# lambda/word_counter.py (actualizado)
import json

def handler(event, context):
    try:
        body = json.loads(event.get("body", "{}"))
    except (json.JSONDecodeError, TypeError):
        return {"statusCode": 400, "body": json.dumps({"error": "Invalid JSON"})}

    text = body.get("text", "")
    if not text:
        return {"statusCode": 400, "body": json.dumps({"error": "text is required"})}

    words = text.split()
    longest = max(words, key=len) if words else ""

    return {
        "statusCode": 200,
        "body": json.dumps({
            "words": len(words),
            "characters": len(text),
            "longest_word": longest,
            "longest_word_length": len(longest),
        }),
    }
# Re-empaquetar
cd lambda
zip word_counter.zip word_counter.py
cd ..

# Actualizar (no create — update)
awslocal lambda update-function-code \
  --function-name word-counter \
  --zip-file fileb://lambda/word_counter.zip

# Invoke para verificar el cambio
awslocal lambda invoke \
  --function-name word-counter \
  --payload '{"body": "{\"text\": \"LocalStack permite desarrollar contra AWS sin coste económico\"}"}' \
  --cli-binary-format raw-in-base64-out \
  output.json

cat output.json | python3 -c "
import json, sys
resp = json.load(sys.stdin)
body = json.loads(resp['body'])
print(f\"Longest word: {body['longest_word']} ({body['longest_word_length']} chars)\")
"
# Longest word: desarrollar (11 chars)

Ejercicio 3: Invocar Lambda con boto3 desde Python

Escribe un script Python que invoque la función ai-endpoint en LocalStack, envíe 3 prompts diferentes, y recolecte las respuestas con tiempos de duración.

Ver solución
# test_lambda_batch.py
import boto3
import json
import time

lambda_client = boto3.client(
    "lambda",
    endpoint_url="http://localhost:4566",
    aws_access_key_id="test",
    aws_secret_access_key="test",
    region_name="us-east-1",
)

prompts = [
    "¿Qué es S3 en una frase?",
    "¿Qué es Lambda en una frase?",
    "¿Qué es LocalStack en una frase?",
]

results = []
for prompt in prompts:
    payload = {"body": json.dumps({"prompt": prompt, "max_tokens": 80})}

    start = time.time()
    response = lambda_client.invoke(
        FunctionName="ai-endpoint",
        InvocationType="RequestResponse",
        Payload=json.dumps(payload),
    )
    invoke_time = round((time.time() - start) * 1000)

    result = json.loads(response["Payload"].read())
    body = json.loads(result["body"])

    results.append({
        "prompt": prompt,
        "answer": body["answer"][:80],
        "tokens": body["tokens_used"],
        "lambda_ms": body["duration_ms"],
        "total_ms": invoke_time,
    })

print(f"{'Prompt':<35} {'Tokens':>6} {'Lambda':>8} {'Total':>8}")
print("-" * 65)
for r in results:
    print(f"{r['prompt']:<35} {r['tokens']:>6} {r['lambda_ms']:>6}ms {r['total_ms']:>6}ms")

print(f"\nTotal tokens: {sum(r['tokens'] for r in results)}")
print(f"Promedio Lambda: {sum(r['lambda_ms'] for r in results) // len(results)}ms")

Ejercicio 4: Script de deploy completo con verificación

Crea un script bash que: empaquete el handler, despliegue a LocalStack (create o update según exista), invoque con un prompt de test, y reporte success/failure.

Ver solución
#!/bin/bash
# deploy-and-test.sh
set -e

FUNCTION="ai-endpoint"
HANDLER_DIR="lambda"
ZIP="$HANDLER_DIR/handler.zip"

echo "1. Empaquetando..."
cd "$HANDLER_DIR/package"
zip -r ../handler.zip . -q
cd ../..
echo "   Size: $(ls -lh $ZIP | awk '{print $5}')"

echo "2. Verificando LocalStack..."
HEALTH=$(curl -s http://localhost:4566/_localstack/health)
LAMBDA_STATUS=$(echo $HEALTH | python3 -c "import json,sys; print(json.load(sys.stdin)['services'].get('lambda','unavailable'))")
if [ "$LAMBDA_STATUS" != "available" ]; then
  echo "   ERROR: Lambda no disponible en LocalStack"
  exit 1
fi
echo "   Lambda: available"

echo "3. Desplegando..."
EXISTS=$(awslocal lambda get-function --function-name $FUNCTION 2>/dev/null && echo "yes" || echo "no")

if [ "$EXISTS" = "yes" ]; then
  awslocal lambda update-function-code \
    --function-name $FUNCTION \
    --zip-file fileb://$ZIP > /dev/null
  echo "   Función actualizada"
else
  awslocal lambda create-function \
    --function-name $FUNCTION \
    --runtime python3.11 \
    --handler handler.handler \
    --zip-file fileb://$ZIP \
    --role arn:aws:iam::000000000000:role/lambda-role \
    --timeout 60 \
    --memory-size 768 \
    --environment "Variables={OPENAI_API_KEY=${OPENAI_API_KEY},MODEL_NAME=gpt-4o-mini}" > /dev/null
  echo "   Función creada"
fi

echo "4. Testing..."
awslocal lambda invoke \
  --function-name $FUNCTION \
  --payload '{"body": "{\"prompt\": \"Responde solo OK\", \"max_tokens\": 5}"}' \
  --cli-binary-format raw-in-base64-out \
  /tmp/test-result.json > /dev/null

STATUS=$(cat /tmp/test-result.json | python3 -c "import json,sys; print(json.load(sys.stdin).get('statusCode','error'))")

if [ "$STATUS" = "200" ]; then
  echo "   PASS — Status 200"
  BODY=$(cat /tmp/test-result.json | python3 -c "import json,sys; r=json.load(sys.stdin); print(json.loads(r['body'])['answer'][:50])")
  echo "   Response: $BODY"
else
  echo "   FAIL — Status $STATUS"
  cat /tmp/test-result.json | python3 -m json.tool
  exit 1
fi

echo "Deploy completo"

Troubleshooting

"Lambda invoke retorna error 'Function not found'"

# Verifica que la función existe
awslocal lambda list-functions --query 'Functions[].FunctionName'

# Si no existe, despliégala:
awslocal lambda create-function ...

# Si el nombre no coincide, verifica casing
# Lambda names son case-sensitive

"Lambda invoke retorna timeout"

# El handler está tardando más que el timeout configurado
# Verifica el timeout:
awslocal lambda get-function-configuration \
  --function-name ai-endpoint \
  --query 'Timeout'

# Aumentar si es necesario:
awslocal lambda update-function-configuration \
  --function-name ai-endpoint \
  --timeout 120

"ModuleNotFoundError: No module named 'openai'"

# Las dependencias no están en el zip
# Verifica que empaquetaste correctamente:
unzip -l lambda/handler.zip | grep openai
# Debe mostrar archivos de openai/

# Si no están, re-empaqueta:
pip install -r lambda/requirements.txt -t lambda/package/
cp lambda/handler.py lambda/package/
cd lambda/package && zip -r ../handler.zip . && cd ../..

"OPENAI_API_KEY no está configurada en Lambda"

# Verifica las variables de entorno:
awslocal lambda get-function-configuration \
  --function-name ai-endpoint \
  --query 'Environment.Variables'

# Actualizar:
awslocal lambda update-function-configuration \
  --function-name ai-endpoint \
  --environment "Variables={OPENAI_API_KEY=sk-proj-tu-key-real}"

"Lambda con LAMBDA_EXECUTOR=docker no funciona"

# Verifica que Docker socket está montado en LocalStack
docker compose exec localstack ls -la /var/run/docker.sock

# Si no está, agrega al Compose:
volumes:
  - /var/run/docker.sock:/var/run/docker.sock

# Alternativa: usa LAMBDA_EXECUTOR=local

Resumen

  • Lambda en LocalStack usa el mismo código y las mismas APIs que Lambda en AWS. Solo cambia el endpoint.
  • El handler del M3 funciona en LocalStack sin cambios de código — solo cambia cómo lo despliegas.
  • awslocal lambda create-function es idéntico a aws lambda create-function — mismos parámetros, mismo formato.
  • Invocación funciona con awslocal CLI o con boto3 (usando endpoint_url).
  • Logs se leen desde Docker (docker compose logs) o desde CloudWatch emulado (awslocal logs).
  • El ciclo de desarrollo es rápido: editar → empaquetar → update → invoke → verificar.
  • La confianza clave: si funciona en LocalStack, funciona en AWS (con mínimos ajustes de IAM y endpoint).

Recursos Adicionales

  1. LocalStack Lambda Documentation — Guía oficial de Lambda en LocalStack
  2. AWS Lambda CLI Reference — Referencia completa del CLI
  3. boto3 Lambda Client — API de Lambda en boto3
  4. Lambda Deployment Package — Cómo empaquetar Lambda en Python
  5. LocalStack Lambda Executor — Modos de ejecución de Lambda
  6. AWS Lambda Invoke API — API de invocación