Módulo 4: LocalStack — AWS Local Development

2. LocalStack Setup y Configuración

Descripción

En esta cápsula vas a instalar y configurar LocalStack como un servicio dentro de tu Docker Compose — el mismo Compose que construiste en el Módulo 2 con FastAPI y Redis. Al terminar, tendrás LocalStack corriendo junto a tu app AI, listo para emular S3, Lambda, y otros servicios AWS. No es una instalación aislada: LocalStack se integra en tu infraestructura de desarrollo existente.

Contexto: La cápsula anterior explicó qué es LocalStack y por qué importa. Aquí lo pones en marcha. El setup es deliberadamente simple: 5 líneas en tu Compose file y un comando para verificar. El valor no está en instalarlo — está en lo que haces con él en las cápsulas siguientes.


LocalStack en Docker Compose

La configuración mínima

LocalStack corre como un container Docker. La forma más limpia de integrarlo es como un servicio más en tu Docker Compose:

# docker-compose.yml — Agregar LocalStack al Compose del M2

services:
  # --- Servicios existentes del M2 ---
  api:
    build:
      context: ./api
    ports:
      - "8000:8000"
    env_file:
      - .env
    environment:
      - REDIS_URL=redis://cache:6379
      - AWS_ENDPOINT_URL=http://localstack:4566
    depends_on:
      cache:
        condition: service_healthy
      localstack:
        condition: service_healthy

  cache:
    image: redis:7-alpine
    command: redis-server --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3

  # --- NUEVO: LocalStack ---
  localstack:
    image: localstack/localstack:latest
    ports:
      - "4566:4566"
    environment:
      - SERVICES=s3,lambda
      - DEBUG=0
      - LAMBDA_EXECUTOR=docker
      - DOCKER_HOST=unix:///var/run/docker.sock
    volumes:
      - localstack_data:/var/lib/localstack
      - /var/run/docker.sock:/var/run/docker.sock
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 15s

volumes:
  redis_data:
  localstack_data:

Cada línea explicada

localstack:
  image: localstack/localstack:latest
  # Imagen oficial de LocalStack desde Docker Hub

  ports:
    - "4566:4566"
    # Puerto único para TODOS los servicios AWS
    # S3, Lambda, API Gateway — todo por el puerto 4566
    # LocalStack rutea internamente según el servicio solicitado

  environment:
    - SERVICES=s3,lambda
    # Solo arranca los servicios que necesitas
    # Menos servicios = arranque más rápido, menos RAM
    # Para esta guía: S3 y Lambda son suficientes

    - DEBUG=0
    # 0 = logs normales, 1 = logs verbose
    # Usa 1 cuando debuggees problemas

    - LAMBDA_EXECUTOR=docker
    # Lambda ejecuta funciones en containers Docker separados
    # Alternativa: "local" (ejecuta en el mismo proceso)
    # "docker" es más fiel a AWS real

    - DOCKER_HOST=unix:///var/run/docker.sock
    # Permite a LocalStack crear containers para Lambda
    # Necesario cuando LAMBDA_EXECUTOR=docker

  volumes:
    - localstack_data:/var/lib/localstack
    # Persiste datos entre reinicios del container
    # Sin esto, pierdes buckets y funciones al reiniciar

    - /var/run/docker.sock:/var/run/docker.sock
    # Da acceso a Docker socket para crear containers Lambda

  healthcheck:
    test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
    # Verifica que LocalStack está ready
    # Los servicios no están disponibles hasta que el health pasa
    interval: 10s
    timeout: 5s
    retries: 5
    start_period: 15s
    # start_period: LocalStack tarda ~10-15s en arrancar

Por qué un solo puerto

A diferencia de AWS real (donde S3 está en s3.amazonaws.com, Lambda en lambda.us-east-1.amazonaws.com), LocalStack expone todo por un solo puerto: 4566. El servicio se determina por los headers de la request, no por la URL.

AWS real:
├── s3.amazonaws.com              → S3
├── lambda.us-east-1.amazonaws.com → Lambda
└── apigateway.us-east-1.amazonaws.com → API Gateway

LocalStack:
└── localhost:4566                → Todo
    ├── boto3 envía header con servicio solicitado
    └── LocalStack rutea internamente

Esto simplifica tu setup: un puerto, un container, todos los servicios.


Configuración del CLI

awslocal: tu nuevo mejor amigo

awslocal es un wrapper del AWS CLI que automáticamente agrega --endpoint-url=http://localhost:4566:

# Instalar
pip install awscli-local

# Verificar
awslocal --version

Comparación:

# Sin awslocal (verbose, fácil olvidar el flag)
aws --endpoint-url=http://localhost:4566 s3 ls
aws --endpoint-url=http://localhost:4566 lambda list-functions
aws --endpoint-url=http://localhost:4566 s3 mb s3://my-bucket

# Con awslocal (limpio, mismo resultado)
awslocal s3 ls
awslocal lambda list-functions
awslocal s3 mb s3://my-bucket

Configurar credenciales dummy

LocalStack no valida credenciales (Community Edition), pero el AWS CLI necesita que existan. Configura credenciales dummy:

# Configura credenciales fake (LocalStack las ignora)
aws configure set aws_access_key_id test
aws configure set aws_secret_access_key test
aws configure set region us-east-1

# Verifica
aws configure list
# Debe mostrar access_key y secret_key como "test"

Estas credenciales nunca llegan a AWS real — solo satisfacen el requisito del CLI. LocalStack acepta cualquier valor.

Variables de entorno alternativas

Si prefieres no modificar tu ~/.aws/credentials, usa variables de entorno:

# En tu .env o en tu shell
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_DEFAULT_REGION=us-east-1
export AWS_ENDPOINT_URL=http://localhost:4566

Con AWS_ENDPOINT_URL configurado, incluso el aws CLI normal apunta a LocalStack sin necesitar awslocal:

# Con AWS_ENDPOINT_URL configurado:
aws s3 ls  # ← esto ya apunta a LocalStack

Levantar y Verificar

Primer arranque

# Levanta todo (desde el directorio de tu proyecto)
docker compose up -d

# Verifica que todos los servicios están up
docker compose ps

# Output esperado:
# NAME          STATUS           PORTS
# api           Up (healthy)     0.0.0.0:8000->8000/tcp
# cache         Up (healthy)     6379/tcp
# localstack    Up (healthy)     0.0.0.0:4566->4566/tcp

Verificar servicios disponibles

# Health check de LocalStack
curl http://localhost:4566/_localstack/health | python3 -m json.tool

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

Verificar con awslocal

# S3: listar buckets (vacío al inicio)
awslocal s3 ls
# (sin output — no hay buckets aún)

# Lambda: listar funciones (vacío al inicio)
awslocal lambda list-functions
# {"Functions": []}

# Crear un bucket de prueba
awslocal s3 mb s3://test-bucket
# make_bucket: test-bucket

# Verificar que existe
awslocal s3 ls
# 2026-03-08 12:00:00 test-bucket

# Limpiar
awslocal s3 rb s3://test-bucket

Verificar con Python (boto3)

# test_localstack.py
import boto3

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

# Crear bucket
s3.create_bucket(Bucket="test-from-python")

# Listar buckets
response = s3.list_buckets()
for bucket in response["Buckets"]:
    print(f"Bucket: {bucket['Name']}")

# Output esperado:
# Bucket: test-from-python

# Limpiar
s3.delete_bucket(Bucket="test-from-python")
print("LocalStack + boto3 funcionando correctamente")
python test_localstack.py
# Bucket: test-from-python
# LocalStack + boto3 funcionando correctamente

Si ves ese output, tu setup está completo.


Configuraciones Avanzadas

SERVICES: controla qué se arranca

# Solo S3 y Lambda (lo que necesitamos)
environment:
  - SERVICES=s3,lambda

# Todo (más lento al arrancar, más RAM)
environment:
  - SERVICES=  # vacío = todos

# S3, Lambda, SQS, DynamoDB
environment:
  - SERVICES=s3,lambda,sqs,dynamodb

Regla: solo lista los servicios que usas. Cada servicio adicional consume RAM y ralentiza el arranque.

LAMBDA_EXECUTOR: cómo ejecuta funciones

# docker (recomendado) — cada función en su container
environment:
  - LAMBDA_EXECUTOR=docker

# local — ejecuta en el proceso de LocalStack (más rápido, menos fiel)
environment:
  - LAMBDA_EXECUTOR=local

docker es más fiel a AWS real pero requiere acceso al Docker socket. local es más rápido para iteración pero puede tener diferencias de comportamiento.

Persistence con volúmenes

# Sin volumen: datos se pierden al reiniciar
localstack:
  image: localstack/localstack:latest

# Con volumen: datos persisten
localstack:
  image: localstack/localstack:latest
  volumes:
    - localstack_data:/var/lib/localstack

En Community Edition, la persistence es limitada. Para garantizar que tu entorno está listo, usa un script de inicialización:

#!/bin/bash
# scripts/setup-localstack.sh

echo "Esperando a que LocalStack esté ready..."
until curl -s http://localhost:4566/_localstack/health | grep -q '"s3": "available"'; do
  sleep 2
done
echo "LocalStack ready"

echo "Creando buckets..."
awslocal s3 mb s3://ai-input
awslocal s3 mb s3://ai-output

echo "Verificando..."
awslocal s3 ls

echo "Setup completo"

Init hooks: setup automático al arrancar

LocalStack soporta scripts de inicialización que se ejecutan al arrancar:

localstack:
  image: localstack/localstack:latest
  volumes:
    - ./init-scripts:/etc/localstack/init/ready.d
# init-scripts/setup.sh
#!/bin/bash
awslocal s3 mb s3://ai-input
awslocal s3 mb s3://ai-output
echo "Buckets creados automáticamente"

Cualquier script en /etc/localstack/init/ready.d/ se ejecuta cuando LocalStack está listo. Esto elimina la necesidad de correr el setup manualmente.


LocalStack Dentro de la Red Docker

Comunicación entre servicios

Dentro de Docker Compose, tu app FastAPI puede acceder a LocalStack usando el nombre del servicio como hostname:

# Desde tu FastAPI (dentro del Compose)
s3 = boto3.client(
    "s3",
    endpoint_url="http://localstack:4566",  # hostname del servicio
    aws_access_key_id="test",
    aws_secret_access_key="test",
)

# Desde tu máquina (fuera del Compose)
s3 = boto3.client(
    "s3",
    endpoint_url="http://localhost:4566",  # puerto expuesto
    aws_access_key_id="test",
    aws_secret_access_key="test",
)
Dentro de Docker Compose:
  api → http://localstack:4566  (red interna)
  api → http://cache:6379       (red interna)

Desde tu máquina:
  tú → http://localhost:4566    (puerto expuesto)
  tú → http://localhost:8000    (puerto expuesto)

El .env actualizado

# .env — Agrega las variables de LocalStack
OPENAI_API_KEY=sk-proj-your-key-here
ENVIRONMENT=development
LOG_LEVEL=debug
CACHE_TTL=3600
MODEL_NAME=gpt-4o-mini
MAX_TOKENS=500

# LocalStack
AWS_ENDPOINT_URL=http://localstack:4566
AWS_ACCESS_KEY_ID=test
AWS_SECRET_ACCESS_KEY=test
AWS_DEFAULT_REGION=us-east-1

Ejercicios

Ejercicio 1: Compose con LocalStack desde cero

Crea un docker-compose.yml que tenga solo LocalStack con S3 y Lambda habilitados. Levántalo, verifica el health check, crea un bucket, y lista los buckets con awslocal.

Ver solución
# docker-compose.yml
services:
  localstack:
    image: localstack/localstack:latest
    ports:
      - "4566:4566"
    environment:
      - SERVICES=s3,lambda
      - DEBUG=0
    volumes:
      - localstack_data:/var/lib/localstack
      - /var/run/docker.sock:/var/run/docker.sock
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 15s

volumes:
  localstack_data:
# Levantar
docker compose up -d

# Esperar health check
docker compose ps
# Esperar hasta que localstack muestre "Up (healthy)"

# Verificar health
curl http://localhost:4566/_localstack/health

# Crear bucket
awslocal s3 mb s3://mi-primer-bucket

# Listar buckets
awslocal s3 ls
# Output: 2026-03-08 ... mi-primer-bucket

Ejercicio 2: Script de inicialización automática

Crea un init script que se ejecute automáticamente cuando LocalStack arranca. El script debe crear dos buckets (ai-models y ai-results) y confirmar con un listado.

Ver solución
# init-scripts/setup.sh
#!/bin/bash
set -e

echo "=== Inicializando LocalStack ==="

awslocal s3 mb s3://ai-models
echo "Bucket ai-models creado"

awslocal s3 mb s3://ai-results
echo "Bucket ai-results creado"

echo "=== Buckets disponibles ==="
awslocal s3 ls

echo "=== Inicialización completa ==="
# docker-compose.yml — agregar volumen de init
services:
  localstack:
    image: localstack/localstack:latest
    ports:
      - "4566:4566"
    environment:
      - SERVICES=s3,lambda
    volumes:
      - localstack_data:/var/lib/localstack
      - /var/run/docker.sock:/var/run/docker.sock
      - ./init-scripts:/etc/localstack/init/ready.d
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 15s

volumes:
  localstack_data:
# Hacer el script ejecutable
chmod +x init-scripts/setup.sh

# Levantar
docker compose up -d

# Verificar logs para ver la inicialización
docker compose logs localstack | grep "Bucket"

# Verificar buckets
awslocal s3 ls
# ai-models
# ai-results

Ejercicio 3: Verificación con boto3

Escribe un script Python que conecte a LocalStack, cree un bucket, suba un archivo JSON con metadata de un modelo AI, lo descargue, y verifique que el contenido es idéntico.

Ver solución
# verify_localstack.py
import json
import boto3

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

bucket = "verification-test"
key = "models/gpt4o-mini-config.json"

model_config = {
    "model": "gpt-4o-mini",
    "max_tokens": 500,
    "temperature": 0.7,
    "system_prompt": "Eres un asistente útil y conciso.",
    "version": "2026-03",
}

print("1. Creando bucket...")
s3.create_bucket(Bucket=bucket)

print("2. Subiendo archivo...")
s3.put_object(
    Bucket=bucket,
    Key=key,
    Body=json.dumps(model_config),
    ContentType="application/json",
)

print("3. Descargando archivo...")
response = s3.get_object(Bucket=bucket, Key=key)
downloaded = json.loads(response["Body"].read().decode("utf-8"))

print("4. Verificando contenido...")
assert downloaded == model_config, "El contenido no coincide"

print("5. Listando objetos...")
objects = s3.list_objects_v2(Bucket=bucket)
for obj in objects.get("Contents", []):
    print(f"   {obj['Key']} ({obj['Size']} bytes)")

print("\nVerificación completa — LocalStack + boto3 funcionando")
python verify_localstack.py
# 1. Creando bucket...
# 2. Subiendo archivo...
# 3. Descargando archivo...
# 4. Verificando contenido...
# 5. Listando objetos...
#    models/gpt4o-mini-config.json (123 bytes)
#
# Verificación completa — LocalStack + boto3 funcionando

Ejercicio 4: Compose multi-servicio completo

Extiende el Docker Compose del Módulo 2 (FastAPI + Redis) agregando LocalStack. Configura las variables de entorno para que FastAPI pueda conectarse a LocalStack usando el hostname del servicio. Verifica que los tres servicios están healthy.

Ver solución
# docker-compose.yml
services:
  api:
    build:
      context: ./api
    ports:
      - "8000:8000"
    env_file:
      - .env
    environment:
      - REDIS_URL=redis://cache:6379
      - AWS_ENDPOINT_URL=http://localstack:4566
      - AWS_ACCESS_KEY_ID=test
      - AWS_SECRET_ACCESS_KEY=test
      - AWS_DEFAULT_REGION=us-east-1
    depends_on:
      cache:
        condition: service_healthy
      localstack:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 15s

  cache:
    image: redis:7-alpine
    command: redis-server --appendonly yes --maxmemory 128mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 3

  localstack:
    image: localstack/localstack:latest
    ports:
      - "4566:4566"
    environment:
      - SERVICES=s3,lambda
      - DEBUG=0
      - LAMBDA_EXECUTOR=docker
      - DOCKER_HOST=unix:///var/run/docker.sock
    volumes:
      - localstack_data:/var/lib/localstack
      - /var/run/docker.sock:/var/run/docker.sock
      - ./init-scripts:/etc/localstack/init/ready.d
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:4566/_localstack/health"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 15s

volumes:
  redis_data:
  localstack_data:
docker compose up -d
docker compose ps

# Verificar los tres servicios:
# api        Up (healthy)
# cache      Up (healthy)
# localstack Up (healthy)

curl http://localhost:8000/health
# {"status": "healthy", ...}

curl http://localhost:4566/_localstack/health
# {"services": {"s3": "available", "lambda": "available"}}

Troubleshooting

"LocalStack no arranca — container exits inmediatamente"

# Revisar logs
docker compose logs localstack

# Causa común: puerto 4566 ya en uso
lsof -i :4566
# Si hay otro proceso, mátalo o cambia el puerto en Compose

# Causa común: Docker socket no accesible
ls -la /var/run/docker.sock
# Debe existir y tener permisos de lectura

"curl a /_localstack/health retorna connection refused"

# LocalStack tarda ~10-15s en arrancar
# Espera y reintenta
sleep 15
curl http://localhost:4566/_localstack/health

# Si sigue fallando, verifica que el container está corriendo
docker compose ps localstack
docker compose logs localstack --tail 20

"awslocal s3 ls retorna error de credenciales"

# Configura credenciales dummy
aws configure set aws_access_key_id test
aws configure set aws_secret_access_key test
aws configure set region us-east-1

# O usa variables de entorno
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test

"LAMBDA_EXECUTOR=docker no funciona en Mac con Apple Silicon"

# En Mac con M1/M2/M3, asegúrate de tener Docker Desktop actualizado
docker --version

# Si Lambda falla con Docker executor, prueba local
environment:
  - LAMBDA_EXECUTOR=local
# Menos fiel a AWS pero funciona en todas las plataformas

"El servicio X no aparece en health check"

# Verifica que lo incluiste en SERVICES
# Si SERVICES=s3,lambda y pides SQS, no estará disponible
curl http://localhost:4566/_localstack/health
# Agrega el servicio a la variable SERVICES y reinicia

Resumen

  • LocalStack se integra en Docker Compose como un servicio más, al lado de tu app y Redis. No es una herramienta separada.
  • Un solo puerto (4566) expone todos los servicios AWS emulados. Simples, limpio.
  • awslocal es tu CLI para interactuar con LocalStack — same commands as aws, minus the --endpoint-url.
  • Credenciales dummy (test/test) satisfacen el requisito del CLI. LocalStack Community no valida credenciales.
  • Init scripts en /etc/localstack/init/ready.d/ automatizan el setup (crear buckets, funciones) al arrancar.
  • Dentro del Compose, los servicios se comunican por hostname (http://localstack:4566). Desde tu máquina, usas http://localhost:4566.
  • SERVICES controla qué se arranca. Solo S3 y Lambda para esta guía — menos es más.

Recursos Adicionales

  1. LocalStack Docker Compose Setup — Guía oficial de instalación con Compose
  2. LocalStack Configuration — Todas las variables de configuración
  3. LocalStack Init Hooks — Scripts de inicialización automática
  4. awscli-local GitHub — Documentación de awslocal
  5. LocalStack Lambda Executor — Configuración de Lambda en LocalStack
  6. Docker Compose Networking — Cómo los servicios se comunican en Compose
  7. LocalStack Health Endpoint — Endpoints internos de LocalStack