Tema
📕 Clase 4 — API REST serverless: API Gateway + Lambda + DynamoDB
Curso AWS (emulador Floci) · 2026-08-08 · Carpeta:
02-Ejercicios/clase-04-api⬅️ Volver al índice de clases
🎯 Qué aprendí
- Qué es una función sin servidor y en qué se diferencia de un backend de siempre.
- El evento: cómo llega una petición HTTP convertida en un diccionario.
- Encadenar tres servicios hasta obtener una URL que responde con datos reales.
- Versiones y alias: por qué una función tiene "instantáneas" y para qué sirven.
- Depurar lo que no ves: CloudWatch Logs y el error que no aparece en ningún sitio.
- Tres trampas que cuestan una tarde: el stage que no se crea, el payload en base64 y el permiso invisible.
📖 PARTE TEÓRICA
⚡ 1. Sin servidor no significa sin servidores
Significa que tú no los administras. Comparado con el backend de CENATE:
| Spring Boot en un servidor | Lambda | |
|---|---|---|
| Está encendido | Siempre | Solo mientras se ejecuta |
| Se paga | Por hora, uses o no | Por invocación y milisegundo |
| Escalar | Levantar más máquinas | Automático, sin hacer nada |
| Arranque | Segundos o minutos | Milisegundos (si está caliente) |
| Estado en memoria | Puedes guardarlo | No: cada invocación puede caer en otra instancia |
⚠️ La consecuencia más importante: una función no puede guardar estado entre invocaciones. Si necesitas recordar algo, va a DynamoDB, a la caché o a S3. Guardarlo en una variable global funciona a veces y falla otras — que es peor que fallar siempre.
📨 2. La petición llega como un diccionario
Lambda no recibe un objeto HttpServletRequest: recibe un evento, un diccionario JSON. API Gateway traduce la petición HTTP a esa forma:
python
def lambda_handler(event, context):
metodo = event["httpMethod"] # "GET"
ruta = event["path"] # "/catalogo"
query = event.get("queryStringParameters") or {} # {"q": "1"}
params = event.get("pathParameters") or {} # {"id": "7"}
cuerpo = event.get("body") # texto crudo, no dict⚠️
queryStringParametersllega comoNone, no como{}, cuando no hay parámetros. Por eso elor {}: sin él, el primer.get()revienta conAttributeError.
Y la respuesta debe tener una forma concreta:
python
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"total": 4}) # ← texto, NO un diccionario
}⚠️ Si
bodyes un diccionario en vez de una cadena, API Gateway devuelve un error 502 confuso. Siemprejson.dumps.
🔗 3. La cadena completa
GET /catalogo
│
▼
┌──────────────┐ traduce HTTP → evento JSON
│ API GATEWAY │ y la respuesta → HTTP
└──────┬───────┘
│ AWS_PROXY (integracion transparente)
▼
┌──────────────┐ arranca un contenedor,
│ LAMBDA │ ejecuta el handler, se apaga
└──────┬───────┘
│ boto3
▼
┌──────────────┐
│ DYNAMODB │
└──────────────┘Integración AWS_PROXY significa que API Gateway pasa la petición entera sin transformarla y espera la respuesta con statusCode/headers/body. Es la que se usa el 99% de las veces; la alternativa exige plantillas de mapeo y es un dolor.
🏷️ 4. Versiones y alias
Cada vez que publicas, se congela una versión numerada e inmutable. $LATEST es la que estás editando.
$LATEST ← siempre editable, la que cambias
│
├── publish-version ──▶ v1 (congelada)
├── publish-version ──▶ v2 (congelada)
│
alias "prod" ──▶ apunta a v1
alias "test" ──▶ apunta a v2Un alias es un nombre que apunta a una versión. Cambiar a qué versión apunta prod es un despliegue instantáneo — y volver atrás, también.
🐛 5. Lo que no se ve
Una función falla y no hay pantalla donde mirar. Los tres sitios, en orden:
- La respuesta de
invoke: si traeFunctionError, el fallo fue del código. - CloudWatch Logs: los
print()y las trazas completas. - Los registros del emulador (
floci logs): si ni siquiera llegó a arrancar.
💡 El síntoma más engañoso es el timeout sin mensaje: la función se queda esperando algo que nunca llega —casi siempre una conexión de red mal apuntada— y muere en silencio.
⚠️ 6. Tres trampas verificadas
1 · create-deployment --stage-name no crea el stage. En AWS sí; en el emulador hay que crearlo aparte o la invocación devuelve {"message":"Stage not found"}.
2 · El payload en línea necesita un indicador. El AWS CLI v2 espera base64 por defecto:
bash
aws lambda invoke --function-name f --payload '{"a":1}' \
--cli-binary-format raw-in-base64-out salida.jsonSin --cli-binary-format raw-in-base64-out, falla al decodificar.
3 · S3 y API Gateway necesitan permiso explícito para invocar. Sin add-permission, no llaman a la función y no hay error en ninguna parte.
💻 PARTE PRÁCTICA
bash
floci start
eval $(floci env)La función con la que trabajaremos:
python
# 02-Ejercicios/clase-04-api/handler.py
import json, os, boto3
ddb = boto3.client("dynamodb",
endpoint_url=os.environ["AWS_ENDPOINT_URL"],
region_name="us-east-1",
aws_access_key_id="test", aws_secret_access_key="test")
def lambda_handler(event, context):
r = ddb.scan(TableName="Catalogo", Limit=25)
items = [{k: list(v.values())[0] for k, v in i.items()} for i in r.get("Items", [])]
items.sort(key=lambda x: x.get("id", ""))
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"total": len(items), "items": items}, ensure_ascii=False),
}$ curl -s http://localhost:4566/restapis/91cded3e12/prod/_user_request_/catalogo
{"total": 4, "items": [{"id": "001", "nombre": "Teleconsulta", "area": "Cardiologia"}, ...]}💡 La URL de invocación en el emulador es
http://localhost:4566/restapis/{apiId}/{stage}/_user_request_/{ruta}. La que devuelveget-rest-api(https://{id}.execute-api.us-east-1.amazonaws.com) es la forma de AWS y no resuelve en local.
🏋️ EJERCICIOS CON SOLUCIÓN
🟢 Nivel 1 — Tu primera función (1-8)
Ejercicio 1 — Empaquetar el código
Crea un .zip con un handler.py que devuelva {"mensaje": "hola"}.
💡 ¿Sabías que…? — Lambda recibe un zip, no un archivo
Aunque el código sea una función de cinco líneas, se sube comprimido. El nombre del archivo dentro del zip debe coincidir con el --handler.
python
# ejemplo de referencia — otro handler
def lambda_handler(event, context):
return {"ok": True}Ver solución
python
# handler.py
import json
def lambda_handler(event, context):
return {"statusCode": 200, "body": json.dumps({"mensaje": "hola"})}bash
zip -q fn.zip handler.py
# sin zip instalado:
python3 -c "import zipfile;z=zipfile.ZipFile('fn.zip','w');z.write('handler.py');z.close()"Ejercicio 2 — Crear la función
Súbela con el nombre ej02-fn, runtime python3.12.
💡 ¿Sabías que…? — el formato de `--handler`
Es archivo.funcion: handler.lambda_handler significa «en handler.py, la función lambda_handler». Un desajuste aquí da Unable to import module.
bash
# ejemplo de referencia
aws lambda create-function --function-name demo --runtime python3.12 \
--handler app.principal --role arn:aws:iam::000000000000:role/lab-lambda-role \
--zip-file fileb://demo.zipVer solución
bash
aws lambda create-function --function-name ej02-fn \
--runtime python3.12 --handler handler.lambda_handler \
--role arn:aws:iam::000000000000:role/lab-lambda-role \
--zip-file fileb://fn.zip --timeout 20
# "State": "Active"Ejercicio 3 — Invocarla
Ejecútala y muestra su respuesta.
💡 ¿Sabías que…? — la respuesta va a un archivo
invoke escribe el resultado en el archivo que le indiques y devuelve por pantalla solo el código de estado. Es de los comandos menos intuitivos del CLI.
bash
# ejemplo de referencia
aws lambda invoke --function-name demo salida.json
cat salida.jsonVer solución
bash
aws lambda invoke --function-name ej02-fn respuesta.json
cat respuesta.json
# {"statusCode": 200, "body": "{\"mensaje\": \"hola\"}"}Ejercicio 4 — Consultar su configuración
Muestra runtime, tiempo máximo y memoria de ej02-fn.
💡 ¿Sabías que…? — la memoria decide también la CPU
En Lambda, la CPU es proporcional a la memoria asignada. Subir de 128 MB a 512 MB no solo da más memoria: da más CPU, y a veces sale más barato porque termina antes.
bash
# ejemplo de referencia
aws lambda get-function-configuration --function-name demo --query 'MemorySize'Ver solución
bash
aws lambda get-function-configuration --function-name ej02-fn \
--query '[Runtime,Timeout,MemorySize]' --output text
# python3.12 20 128Ejercicio 5 — Listar las funciones
Muestra los nombres de todas las funciones que existen.
💡 ¿Sabías que…? — inventariar antes de crear
Crear una función con un nombre que ya existe da error. Listar primero evita ese tropiezo en scripts.
bash
# ejemplo de referencia
aws lambda list-functions --query 'Functions[*].[FunctionName,Runtime]' --output textVer solución
bash
aws lambda list-functions --query 'Functions[*].FunctionName' --output textEjercicio 6 — Pasarle datos al invocarla
Invócala con el evento {"nombre":"Styp"} y haz que la función lo devuelva.
💡 ¿Sabías que…? — el indicador que casi nadie recuerda
El AWS CLI v2 interpreta --payload como base64 por defecto. Para pasar JSON en claro hace falta --cli-binary-format raw-in-base64-out. En la v1 no era necesario, y por eso medio internet lo omite.
bash
# ejemplo de referencia
aws lambda invoke --function-name demo --payload '{"x":1}' \
--cli-binary-format raw-in-base64-out salida.jsonVer solución
python
def lambda_handler(event, context):
return {"statusCode": 200, "body": json.dumps({"hola": event.get("nombre","?")})}bash
aws lambda invoke --function-name ej02-fn --payload '{"nombre":"Styp"}' \
--cli-binary-format raw-in-base64-out respuesta.json
cat respuesta.jsonEjercicio 7 — Invocarla sin esperar respuesta
Lánzala de forma asíncrona y observa el código que devuelve.
💡 ¿Sabías que…? — `202` significa "aceptado", no "hecho"
Con --invocation-type Event, AWS encola la invocación y responde 202 de inmediato. No sabrás si funcionó salvo que mires los registros. Es lo apropiado para tareas de fondo.
bash
# ejemplo de referencia
aws lambda invoke --function-name demo --invocation-type Event salida.jsonVer solución
bash
aws lambda invoke --function-name ej02-fn --invocation-type Event respuesta.json \
--query 'StatusCode' --output text
# 202Ejercicio 8 — Borrar la función
Elimínala y comprueba que ya no aparece.
💡 ¿Sabías que…? — borrar la función no borra sus registros
El grupo de CloudWatch Logs sobrevive. Es útil (puedes investigar después) y también una fuente de coste olvidado en AWS real.
bash
# ejemplo de referencia
aws lambda delete-function --function-name demoVer solución
bash
aws lambda delete-function --function-name ej02-fn
aws lambda list-functions --query 'Functions[*].FunctionName' --output text | grep ej02 \
|| echo "ya no existe"🔵 Nivel 2 — Configuración, versiones y alias (9-16)
Ejercicio 9 — Pasarle configuración por variables
Crea una función con la variable de entorno SALUDO=hola y haz que la devuelva.
💡 ¿Sabías que…? — configuración fuera del código
Las variables de entorno permiten cambiar el comportamiento sin volver a desplegar. Es donde van endpoints, nombres de tabla y modos de depuración — nunca contraseñas.
python
# ejemplo de referencia
import os
destino = os.environ.get("DESTINO", "por-defecto")Ver solución
bash
aws lambda create-function --function-name ej09-fn \
--runtime python3.12 --handler handler.lambda_handler \
--role arn:aws:iam::000000000000:role/lab-lambda-role \
--zip-file fileb://fn.zip --timeout 20 \
--environment "Variables={SALUDO=hola}"python
"entorno": os.environ.get("SALUDO", "(sin var)")Ejercicio 10 — Cambiar una variable sin tocar el código
Cambia SALUDO a adios sin volver a subir el zip.
💡 ¿Sabías que…? — configuración y código se despliegan por separado
update-function-configuration toca ajustes; update-function-code, el código. Separarlos permite cambiar un endpoint sin arriesgar un despliegue completo.
bash
# ejemplo de referencia
aws lambda update-function-configuration --function-name demo --timeout 30Ver solución
bash
aws lambda update-function-configuration --function-name ej09-fn \
--environment "Variables={SALUDO=adios}" \
--query 'Environment.Variables.SALUDO' --output text
# adiosEjercicio 11 — Ampliar el tiempo máximo
Sube el tiempo límite de la función a 30 segundos.
💡 ¿Sabías que…? — el timeout es tu red de seguridad
Una función colgada consume tiempo facturado hasta agotar el límite. Ponerlo generoso "por si acaso" convierte un error en una factura.
bash
# ejemplo de referencia
aws lambda update-function-configuration --function-name demo --timeout 15Ver solución
bash
aws lambda update-function-configuration --function-name ej09-fn --timeout 30 \
--query 'Timeout' --output text
# 30Ejercicio 12 — Publicar una versión
Congela el estado actual como versión numerada.
💡 ¿Sabías que…? — una versión no se puede modificar
Una vez publicada, su código y configuración quedan fijos para siempre. Es lo que permite volver atrás con garantías: la v1 de hace un mes sigue siendo exactamente la misma.
bash
# ejemplo de referencia
aws lambda publish-version --function-name demo --query 'Version'Ver solución
bash
aws lambda publish-version --function-name ej09-fn --query 'Version' --output text
# 1Ejercicio 13 — Crear un alias
Crea un alias prod que apunte a la versión que quieras servir.
💡 ¿Sabías que…? — el alias es el punto de despliegue
Los consumidores llaman al alias, no a la versión. Desplegar es cambiar a qué versión apunta el alias — instantáneo, y volver atrás también.
bash
# ejemplo de referencia
aws lambda create-alias --function-name demo --name estable --function-version 1Ver solución
bash
aws lambda create-alias --function-name ej09-fn --name prod \
--function-version '$LATEST' --query 'Name' --output text
# prodEjercicio 14 — Listar versiones y alias
Muestra qué versiones y qué alias tiene la función.
💡 ¿Sabías que…? — `$LATEST` siempre está
Aparece junto a las numeradas. Es la única que cambia cuando actualizas el código.
bash
# ejemplo de referencia
aws lambda list-versions-by-function --function-name demo --query 'Versions[*].Version'Ver solución
bash
aws lambda list-versions-by-function --function-name ej09-fn \
--query 'Versions[*].Version' --output text
aws lambda list-aliases --function-name ej09-fn --query 'Aliases[*].[Name,FunctionVersion]' --output textEjercicio 15 — Actualizar el código
Cambia el mensaje que devuelve la función y vuelve a subirlo.
💡 ¿Sabías que…? — actualizar el código no toca las versiones publicadas
update-function-code modifica $LATEST. Las versiones ya publicadas siguen intactas: por eso un alias apuntando a v1 no se ve afectado.
bash
# ejemplo de referencia
aws lambda update-function-code --function-name demo --zip-file fileb://nuevo.zipVer solución
bash
# editar handler.py y volver a comprimir
aws lambda update-function-code --function-name ej09-fn --zip-file fileb://fn.zip \
--query 'LastUpdateStatus' --output textEjercicio 16 — Darle una URL propia
Crea una URL de función y observa su forma.
💡 ¿Sabías que…? — una URL sin API Gateway
Las Function URLs dan un endpoint HTTPS directo, sin API Gateway de por medio. Para un webhook simple sobran; para una API con rutas, no.
bash
# ejemplo de referencia
aws lambda create-function-url-config --function-name demo --auth-type NONEVer solución
bash
aws lambda create-function-url-config --function-name ej09-fn --auth-type NONE \
--query 'FunctionUrl' --output text
# http://ab4e109dc61e....lambda-url.us-east-1.localhost:4566/💡 Fíjate en el
localhost:4566del final: el emulador la resuelve como subdominio delocalhost. En AWS real terminaría en.on.aws.
🟡 Nivel 3 — Poner una API delante (17-26)
Ejercicio 17 — Crear la API
Crea una API REST llamada ej17-api y guarda su identificador.
💡 ¿Sabías que…? — el identificador es lo que necesitarás siempre
Todos los comandos siguientes lo piden. Conviene guardarlo en una variable desde el principio.
bash
# ejemplo de referencia
API=$(aws apigateway create-rest-api --name demo --query 'id' --output text)Ver solución
bash
API=$(aws apigateway create-rest-api --name ej17-api --query 'id' --output text)
echo "$API"Ejercicio 18 — Encontrar el recurso raíz
Obtén el identificador del recurso /.
💡 ¿Sabías que…? — la raíz ya existe
Al crear la API, AWS crea automáticamente el recurso /. Todas las rutas que añadas cuelgan de él.
bash
# ejemplo de referencia
aws apigateway get-resources --rest-api-id $API --query 'items[?path==`/`].id' --output textVer solución
bash
ROOT=$(aws apigateway get-resources --rest-api-id "$API" \
--query 'items[0].id' --output text)
echo "$ROOT"Ejercicio 19 — Crear una ruta
Añade el recurso /catalogo bajo la raíz.
💡 ¿Sabías que…? — las rutas son un árbol
Cada recurso tiene un padre. /api/v1/usuarios son tres recursos anidados, no una cadena.
bash
# ejemplo de referencia
aws apigateway create-resource --rest-api-id $API --parent-id $ROOT --path-part usuariosVer solución
bash
RES=$(aws apigateway create-resource --rest-api-id "$API" --parent-id "$ROOT" \
--path-part catalogo --query 'id' --output text)Ejercicio 20 — Declarar el método
Define que /catalogo acepta GET sin autenticación.
💡 ¿Sabías que…? — método e integración son dos pasos
Primero declaras que el método existe; después, a quién llama. Sin la integración, la ruta existe pero no hace nada.
bash
# ejemplo de referencia
aws apigateway put-method --rest-api-id $API --resource-id $RES \
--http-method POST --authorization-type NONEVer solución
bash
aws apigateway put-method --rest-api-id "$API" --resource-id "$RES" \
--http-method GET --authorization-type NONEEjercicio 21 — Conectarlo a la función
Integra ese GET con tu Lambda usando AWS_PROXY.
💡 ¿Sabías que…? — el `integration-http-method` siempre es POST
Aunque tu ruta sea GET, API Gateway invoca a Lambda por POST: es como funciona su API de invocación. Confunde la primera vez.
bash
# ejemplo de referencia — la forma del ARN
# arn:aws:apigateway:REGION:lambda:path/2015-03-31/functions/ARN_LAMBDA/invocationsVer solución
bash
aws apigateway put-integration --rest-api-id "$API" --resource-id "$RES" \
--http-method GET --type AWS_PROXY --integration-http-method POST \
--uri "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:000000000000:function:ej09-fn/invocations"Ejercicio 22 — Desplegar
Crea un despliegue para el stage prod.
💡 ¿Sabías que…? — sin desplegar, los cambios no se ven
Editar rutas no las publica. Hasta que no hay despliegue, la API sigue sirviendo la versión anterior.
bash
# ejemplo de referencia
aws apigateway create-deployment --rest-api-id $API --stage-name devVer solución
bash
DEP=$(aws apigateway create-deployment --rest-api-id "$API" --stage-name prod \
--query 'id' --output text)Ejercicio 23 — Comprobar si el stage existe
Lista los stages de la API. ¿Está prod?
💡 ¿Sabías que…? — qué es un stage
Es una publicación con nombre (dev, test, prod), cada una con su URL. Permite tener versiones distintas conviviendo.
bash
# ejemplo de referencia
aws apigateway get-stages --rest-api-id $API --query 'item[*].stageName'Ver solución
bash
aws apigateway get-stages --rest-api-id "$API" --query 'item[*].stageName' --output text
# (vacio) ← el emulador NO lo creoEjercicio 24 — Crear el stage a mano
Crea el stage prod explícitamente, asociándolo al despliegue.
💡 ¿Sabías que…? — la diferencia con AWS real
En AWS, create-deployment --stage-name prod crea el stage si no existe. En el emulador, no: hay que crearlo con create-stage o la invocación devolverá Stage not found.
bash
# ejemplo de referencia
aws apigateway create-stage --rest-api-id $API --stage-name dev --deployment-id $DEPVer solución
bash
aws apigateway create-stage --rest-api-id "$API" --stage-name prod \
--deployment-id "$DEP" --query 'stageName' --output text
# prodEjercicio 25 — Darle permiso a API Gateway
Autoriza a API Gateway a invocar tu función.
💡 ¿Sabías que…? — el permiso que falla en silencio
Sin él, la invocación no ocurre y no hay error en ningún registro. Es el mismo caso que S3 invocando a Lambda: en AWS, nada llama a nada sin permiso explícito.
bash
# ejemplo de referencia
aws lambda add-permission --function-name demo --statement-id apigw \
--action lambda:InvokeFunction --principal apigateway.amazonaws.comVer solución
bash
aws lambda add-permission --function-name ej09-fn --statement-id apigw-invoke \
--action lambda:InvokeFunction --principal apigateway.amazonaws.com \
--query 'Statement' --output text | head -c 80Ejercicio 26 — Llamar al endpoint
Invoca la ruta y comprueba que devuelve datos.
💡 ¿Sabías que…? — la URL local no es la de AWS
get-rest-api devuelve la forma de AWS (https://{id}.execute-api…), que no resuelve en local. El emulador expone /restapis/{id}/{stage}/_user_request_/{ruta}.
bash
# ejemplo de referencia
curl -s "http://localhost:4566/restapis/$API/dev/_user_request_/usuarios"Ver solución
bash
curl -s "http://localhost:4566/restapis/$API/prod/_user_request_/catalogo"
# {"total": 4, "items": [...]}🟠 Nivel 4 — La cadena completa con datos (27-34)
Ejercicio 27 — Crear la tabla
Crea una tabla Catalogo con clave de partición id.
💡 ¿Sabías que…? — `PAY_PER_REQUEST` evita decidir capacidad
Con ese modo no hay que estimar lecturas ni escrituras por segundo. Para un laboratorio o una carga irregular, es lo sensato.
bash
# ejemplo de referencia
aws dynamodb create-table --table-name Otra \
--attribute-definitions AttributeName=clave,AttributeType=S \
--key-schema AttributeName=clave,KeyType=HASH --billing-mode PAY_PER_REQUESTVer solución
bash
aws dynamodb create-table --table-name Catalogo \
--attribute-definitions AttributeName=id,AttributeType=S \
--key-schema AttributeName=id,KeyType=HASH \
--billing-mode PAY_PER_REQUEST --query 'TableDescription.TableStatus' --output text
# ACTIVEEjercicio 28 — Cargar datos
Inserta cuatro elementos con id, nombre y area.
💡 ¿Sabías que…? — DynamoDB pide el tipo de cada valor
{"S": "texto"} para cadenas, {"N": "123"} para números (¡también entre comillas!), {"BOOL": true}. Es verboso, pero explícito.
bash
# ejemplo de referencia
aws dynamodb put-item --table-name Otra \
--item '{"clave":{"S":"a"},"valor":{"N":"1"}}'Ver solución
bash
aws dynamodb put-item --table-name Catalogo \
--item '{"id":{"S":"001"},"nombre":{"S":"Teleconsulta"},"area":{"S":"Cardiologia"}}'
aws dynamodb put-item --table-name Catalogo \
--item '{"id":{"S":"002"},"nombre":{"S":"Teleinterconsulta"},"area":{"S":"Neurologia"}}'
# ...y dos masEjercicio 29 — Leer un elemento por su clave
Recupera el elemento 001.
💡 ¿Sabías que…? — `get-item` frente a `scan`
get-item va directo por la clave: coste constante. scan recorre la tabla entera. En una tabla grande, la diferencia es de céntimos a euros.
bash
# ejemplo de referencia
aws dynamodb get-item --table-name Otra --key '{"clave":{"S":"a"}}'Ver solución
bash
aws dynamodb get-item --table-name Catalogo --key '{"id":{"S":"001"}}' \
--query 'Item.nombre.S' --output text
# TeleconsultaEjercicio 30 — Hacer que la función lea la tabla
Modifica el handler para que devuelva el contenido de Catalogo.
💡 ¿Sabías que…? — el endpoint dentro de la función
La función corre en su propio contenedor: localhost ahí es ella misma. Necesita el endpoint del emulador en una variable de entorno, con la IP correcta.
python
# ejemplo de referencia
ddb = boto3.client("dynamodb", endpoint_url=os.environ["AWS_ENDPOINT_URL"], ...)Ver solución
python
r = ddb.scan(TableName="Catalogo", Limit=25)
items = [{k: list(v.values())[0] for k, v in i.items()} for i in r.get("Items", [])]
return {"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"total": len(items), "items": items}, ensure_ascii=False)}Ejercicio 31 — Aplanar el formato de DynamoDB
Convierte {"id": {"S": "001"}} en {"id": "001"} antes de devolverlo.
💡 ¿Sabías que…? — nadie quiere consumir el formato crudo
Los tipos anidados son útiles para la base, no para quien consume la API. Aplanarlos es trabajo de la función.
python
# ejemplo de referencia
plano = {clave: list(valor.values())[0] for clave, valor in item.items()}Ver solución
python
items = [{k: list(v.values())[0] for k, v in i.items()} for i in r.get("Items", [])]Ejercicio 32 — Probar la cadena entera
Llama al endpoint y comprueba que los datos vienen de la tabla.
💡 ¿Sabías que…? — cómo confirmar que no está inventado
Añade un elemento a la tabla y vuelve a llamar sin tocar nada más. Si total sube, la respuesta se está construyendo en el momento.
bash
# ejemplo de referencia
curl -s "$URL" | python3 -m json.toolVer solución
bash
curl -s "http://localhost:4566/restapis/$API/prod/_user_request_/catalogo"
# {"total": 4, "items": [{"id": "001", "nombre": "Teleconsulta", "area": "Cardiologia"}, ...]}Ejercicio 33 — Comprobar que es dinámica
Añade un quinto elemento y vuelve a llamar sin redesplegar.
💡 ¿Sabías que…? — código y datos van por caminos distintos
Cambiar datos no requiere desplegar; cambiar código, sí. Es la separación que permite que un equipo cargue datos sin tocar la aplicación.
bash
# ejemplo de referencia
aws dynamodb put-item --table-name Otra --item '{"clave":{"S":"z"}}'Ver solución
bash
aws dynamodb put-item --table-name Catalogo \
--item '{"id":{"S":"005"},"nombre":{"S":"Telecapacitacion"},"area":{"S":"Docencia"}}'
curl -s "http://localhost:4566/restapis/$API/prod/_user_request_/catalogo" | head -c 60
# {"total": 5, ...Ejercicio 34 — Contar cuántos elementos hay
Averigua el número de elementos de la tabla sin traértelos todos.
💡 ¿Sabías que…? — `--select COUNT` no transfiere los datos
Devuelve solo el número. Sigue recorriendo la tabla (paga la lectura), pero ahorra el envío por red.
bash
# ejemplo de referencia
aws dynamodb scan --table-name Otra --select COUNT --query 'Count'Ver solución
bash
aws dynamodb scan --table-name Catalogo --select COUNT --query 'Count' --output text
# 5🔴 Nivel 5 — Depurar lo que no se ve (35-40)
Ejercicio 35 — Leer los registros de la función
Encuentra el grupo de registros de tu función y lee sus últimas líneas.
💡 ¿Sabías que…? — el nombre del grupo es predecible
Siempre /aws/lambda/<nombre-de-la-funcion>. Se crea solo en la primera invocación.
bash
# ejemplo de referencia
aws logs describe-log-groups --query 'logGroups[*].logGroupName' --output textVer solución
bash
G=/aws/lambda/ej09-fn
S=$(aws logs describe-log-streams --log-group-name $G \
--query 'logStreams[-1].logStreamName' --output text)
aws logs get-log-events --log-group-name $G --log-stream-name "$S" \
--query 'events[*].message' --output text⚠️ Fíjate en
logStreams[-1]: el emulador ignora--order-by LastEventTime --descendingy devuelve el stream más antiguo. Hay que coger el último de la lista.
Ejercicio 36 — Provocar un error y encontrarlo
Haz que la función falle a propósito y localiza la traza.
💡 ¿Sabías que…? — `FunctionError` en la respuesta
Cuando el código lanza una excepción, invoke devuelve un FunctionError y el archivo de salida contiene el mensaje y el tipo. Los dos valores no significan lo mismo: Handled es una excepción del código, capturada por el runtime; Unhandled es un fallo del entorno, como agotar el tiempo. La traza completa está en los registros.
python
# ejemplo de referencia
def lambda_handler(event, context):
return 1 / 0Ver solución
bash
aws lambda invoke --function-name ej36-fn respuesta.json --query 'FunctionError' --output text
# Handled
cat respuesta.json
# {"errorMessage": "division by zero", "errorType": "ZeroDivisionError", ...}Ejercicio 37 — Diagnosticar un timeout
Crea una función que espere más que su tiempo límite e interpreta el resultado.
💡 ¿Sabías que…? — el timeout no dice qué lo causó
Solo dice que se agotó el tiempo. En la práctica, la causa casi siempre es una conexión de red que nunca responde: un endpoint mal apuntado.
python
# ejemplo de referencia
import time
def lambda_handler(event, context):
time.sleep(60)Ver solución
bash
aws lambda invoke --function-name ej37-fn respuesta.json
cat respuesta.json
# {"errorMessage": "Task timed out after 20 seconds", "errorType": "Function.TimedOut"}
# FunctionError: Unhandled ← aqui SI es Unhandled: fallo del entorno, no del codigo💡 Si te aparece esto sin haber puesto ningún
sleep, sospecha del endpoint: es el síntoma de una función intentando alcanzar una dirección inalcanzable.
Ejercicio 38 — El fallo sin error
Quita el permiso de invocación y llama al endpoint. ¿Qué ves?
💡 ¿Sabías que…? — el peor tipo de fallo es el silencioso
Sin permiso, API Gateway no llega a invocar. No hay registro de la función porque la función nunca se ejecutó. El único indicio está del lado de API Gateway.
bash
# ejemplo de referencia
aws lambda remove-permission --function-name demo --statement-id apigwVer solución
bash
aws lambda remove-permission --function-name ej09-fn --statement-id apigw-invoke
curl -s -o /dev/null -w "%{http_code}\n" \
"http://localhost:4566/restapis/$API/prod/_user_request_/catalogo"La lección: antes de depurar el código, comprueba que el código llegó a ejecutarse. Si no hay registros nuevos, el problema está antes.
Ejercicio 39 — La URL que no resuelve
Intenta llamar a la URL que devuelve get-rest-api y explica por qué falla.
💡 ¿Sabías que…? — dos URLs para la misma API
El emulador devuelve la forma de AWS por fidelidad de la API, pero solo sirve la ruta /restapis/…. Confundirlas es de los primeros tropiezos.
bash
# ejemplo de referencia — la que SI funciona en local
curl "http://localhost:4566/restapis/$API/prod/_user_request_/ruta"Ver solución
bash
aws apigateway get-rest-apis --query 'items[0].id' --output text
# La URL "oficial" seria:
# https://<id>.execute-api.us-east-1.amazonaws.com/prod/catalogo
# ...pero ese dominio no existe fuera de AWS: no resuelve.Ejercicio 40 — Desmontarlo todo
Elimina la API, la función y la tabla, y comprueba que no queda nada.
💡 ¿Sabías que…? — el orden importa poco, pero la limpieza mucho
En AWS, una API olvidada no cuesta si nadie la llama, pero una tabla sí. Desmontar es parte del ejercicio, no un apéndice.
bash
# ejemplo de referencia
aws apigateway delete-rest-api --rest-api-id $APIVer solución
bash
aws apigateway delete-rest-api --rest-api-id "$API"
aws lambda delete-function --function-name ej09-fn
aws dynamodb delete-table --table-name Catalogo
aws apigateway get-rest-apis --query 'length(items)' --output text❓ Preguntas y respuestas (autoevaluación)
1. ¿Por qué una función no puede guardar estado en memoria entre invocaciones?
Porque cada invocación puede ejecutarse en una instancia distinta. Funciona a veces y falla otras, que es peor que fallar siempre.
2. ¿Qué forma debe tener la respuesta de una Lambda tras API Gateway?
statusCode,headersybody, ybodydebe ser texto (json.dumps), no un diccionario.
3. ¿Por qué queryStringParameters necesita un or {}?
Porque llega como
Nonecuando no hay parámetros, y un.get()sobreNonerevienta.
4. ¿Qué significa la integración AWS_PROXY?
Que API Gateway pasa la petición entera sin transformarla y espera la respuesta con la forma estándar.
5. ¿Diferencia entre versión y alias?
La versión es un estado congelado e inmutable; el alias es un nombre que apunta a una versión y se puede repuntar.
6. ¿Por qué --invocation-type Event devuelve 202 y no 200?
Porque acepta la invocación y responde de inmediato, sin esperar el resultado.
7. ¿Para qué sirve --cli-binary-format raw-in-base64-out?
Para pasar un payload JSON en claro: el CLI v2 lo interpreta como base64 por defecto.
8. Llamas al endpoint, responde mal y no hay registros nuevos. ¿Qué deduces?
Que la función nunca se ejecutó. El problema está antes: casi siempre el permiso de invocación.
9. ¿Por qué en el emulador hay que crear el stage a mano?
Porque
create-deployment --stage-nameno lo crea, a diferencia de AWS. Sin él,Stage not found.
10. Tu función muere por timeout sin ningún sleep. ¿Primera sospecha?
Una conexión de red que nunca responde, casi siempre un endpoint mal apuntado.
📎 Apuntes relacionados
- Clase 1 — CloudFormation · toda esta cadena cabe en una plantilla
- Clase 3 — ElastiCache · una caché delante de DynamoDB evita lecturas
- Comandos
➡️ Siguiente
Clase 5 — Data lake con Athena: consultar con SQL archivos que están en S3, sin ninguna base de datos encendida.