Skip to content

📕 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 servidorLambda
Está encendidoSiempreSolo mientras se ejecuta
Se pagaPor hora, uses o noPor invocación y milisegundo
EscalarLevantar más máquinasAutomático, sin hacer nada
ArranqueSegundos o minutosMilisegundos (si está caliente)
Estado en memoriaPuedes guardarloNo: 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

⚠️ queryStringParameters llega como None, no como {}, cuando no hay parámetros. Por eso el or {}: sin él, el primer .get() revienta con AttributeError.

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 body es un diccionario en vez de una cadena, API Gateway devuelve un error 502 confuso. Siempre json.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 v2

Un 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:

  1. La respuesta de invoke: si trae FunctionError, el fallo fue del código.
  2. CloudWatch Logs: los print() y las trazas completas.
  3. 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.json

Sin --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),
    }
zsh · handler.py
$ 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 devuelve get-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.zip
Ver 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.json
Ver 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    128

Ejercicio 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 text
Ver solución
bash
aws lambda list-functions --query 'Functions[*].FunctionName' --output text

Ejercicio 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.json
Ver 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.json

Ejercicio 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.json
Ver solución
bash
aws lambda invoke --function-name ej02-fn --invocation-type Event respuesta.json \
  --query 'StatusCode' --output text
# 202

Ejercicio 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 demo
Ver 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 30
Ver solución
bash
aws lambda update-function-configuration --function-name ej09-fn \
  --environment "Variables={SALUDO=adios}" \
  --query 'Environment.Variables.SALUDO' --output text
# adios

Ejercicio 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 15
Ver solución
bash
aws lambda update-function-configuration --function-name ej09-fn --timeout 30 \
  --query 'Timeout' --output text
# 30

Ejercicio 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
# 1

Ejercicio 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 1
Ver solución
bash
aws lambda create-alias --function-name ej09-fn --name prod \
  --function-version '$LATEST' --query 'Name' --output text
# prod

Ejercicio 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 text

Ejercicio 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.zip
Ver 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 text

Ejercicio 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 NONE
Ver 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:4566 del final: el emulador la resuelve como subdominio de localhost. 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 text
Ver 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 usuarios
Ver 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 NONE
Ver solución
bash
aws apigateway put-method --rest-api-id "$API" --resource-id "$RES" \
  --http-method GET --authorization-type NONE

Ejercicio 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/invocations
Ver 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 dev
Ver 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 creo

Ejercicio 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 $DEP
Ver solución
bash
aws apigateway create-stage --rest-api-id "$API" --stage-name prod \
  --deployment-id "$DEP" --query 'stageName' --output text
# prod

Ejercicio 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.com
Ver 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 80

Ejercicio 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_REQUEST
Ver 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
# ACTIVE

Ejercicio 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 mas

Ejercicio 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
# Teleconsulta

Ejercicio 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.tool
Ver 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 text
Ver 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 --descending y 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 / 0
Ver 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 apigw
Ver 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 $API
Ver 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, headers y body, y body debe ser texto (json.dumps), no un diccionario.

3. ¿Por qué queryStringParameters necesita un or {}?

Porque llega como None cuando no hay parámetros, y un .get() sobre None revienta.

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-name no 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

➡️ Siguiente

Clase 5 — Data lake con Athena: consultar con SQL archivos que están en S3, sin ninguna base de datos encendida.

Apuntes de AWS con Floci