Cerramos el módulo con la pieza que falta. Hasta ahora, todo lo que ha montado MercadoFresco parte de la misma idea: hay un servidor encendido esperando trabajo. La instancia EC2 de la tienda está encendida aunque sea martes a las 4 de la madrugada. La instancia RDS también. Y se pagan las 730 horas del mes.
Pero hay tareas que no encajan en ese modelo. Generar la miniatura de una foto cuando Luis la sube ocurre veinte veces al día y dura dos segundos. Consultar el estado de un pedido es una llamada puntual. Procesar un fichero de rutas de reparto pasa una vez cada mañana. Tener un servidor encendido 730 horas para 40 minutos de trabajo mensual es absurdo.
AWS Lambda ejecuta código solo cuando ocurre algo y solo cobra mientras se ejecuta, con precisión de milisegundos. En esta lección conectamos por fin el evento de S3 que dejamos configurado en 02-03 y escribimos la función que genera las miniaturas del catálogo de MercadoFresco.
Contenido
- Qué es la computación sin servidor y en qué se diferencia de EC2
- Modelo de ejecución: manejador, evento, contexto y respuesta
- Arranque en frío y arranque en caliente
- Concurrencia: reservada, provisionada y el límite de la cuenta
- Configuración: memoria, CPU, tiempo de espera, variables y arquitectura
- La función de las miniaturas: el código
- Empaquetado con dependencias y capas
- Desplegar por consola y por CLI
- Permisos: el rol de ejecución y el mínimo privilegio
- Registros en CloudWatch Logs y depuración
- Orígenes de eventos habituales
- Segunda función: consultar el estado de un pedido por HTTP
- Límites reales que hay que conocer
- Precios, con el cálculo concreto de MercadoFresco
- Errores, reintentos y colas de mensajes fallidos
- Recapitulación del módulo: qué está ya en AWS y qué falta
Qué es la computación sin servidor y en qué se diferencia de EC2
"Sin servidor" (serverless) no significa que no haya servidores: significa que no son tuyos, no los ves y no los pagas cuando no trabajan. AWS aprovisiona la capacidad, la escala y la retira, todo invisible.
| EC2 | Contenedores (ECS/Fargate) | AWS Lambda | |
|---|---|---|---|
| Unidad de despliegue | Máquina virtual completa | Imagen de contenedor | Función |
| Quién gestiona el SO | Tú | AWS (con Fargate) | AWS |
| Tiempo de arranque | 30-60 s | 10-30 s | Milisegundos a 1-2 s |
| Se factura | Por segundo encendida | Por segundo de tarea | Por ms de ejecución |
| Coste en reposo | Todo el precio | Todo el precio | Cero |
| Duración máxima | Ilimitada | Ilimitada | 15 minutos |
| Escalado | Auto Scaling (minutos) | Servicio ECS (segundos) | Automático e instantáneo |
| Estado en disco | Persistente (EBS) | Efímero | Efímero (512 MB en /tmp) |
| Control del entorno | Total | Alto | Limitado |
| Caso típico | Aplicaciones tradicionales, bases de datos | Microservicios, cargas sostenidas | Eventos, tareas cortas, tráfico irregular |
Los criterios de elección, en orden de utilidad práctica:
- ¿La tarea dura menos de 15 minutos? Si no, Lambda queda descartada.
- ¿El tráfico es irregular o por eventos? Lambda gana con diferencia. Si es sostenido y constante 24×7, un contenedor o una EC2 salen más baratos.
- ¿Necesitas control del sistema operativo o estado en disco? Entonces EC2 o contenedores.
- ¿Cuánto tiempo de tu equipo quieres dedicar a infraestructura? Lambda es el mínimo posible.
Para MercadoFresco, la generación de miniaturas cumple los cuatro criterios de forma perfecta: segundos de duración, disparada por eventos esporádicos, sin estado, y sin nada que administrar.
Modelo de ejecución: manejador, evento, contexto y respuesta
Una función Lambda es una función normal de tu lenguaje con una firma concreta:
def lambda_handler(event, context):
# event -> los datos de lo que ha ocurrido (dict)
# context -> información sobre esta ejecución concreta
return {"resultado": "ok"}| Elemento | Qué es |
|---|---|
Manejador (handler) |
El punto de entrada. Se declara como fichero.funcion, p. ej. app.lambda_handler |
Evento (event) |
Diccionario con los datos del disparador. Su forma depende del origen: S3, API Gateway y SQS tienen estructuras muy distintas |
Contexto (context) |
Metadatos de la ejecución: aws_request_id, function_name, memory_limit_in_mb, get_remaining_time_in_millis() |
| Respuesta | Lo que devuelve la función. Con una invocación síncrona llega a quien llamó; con una asíncrona se descarta |
El ciclo de vida completo, que hay que entender bien porque de él dependen el rendimiento y el coste:
sequenceDiagram
participant E as Evento (S3)
participant L as Servicio Lambda
participant M as Entorno de ejecucion
participant F as Codigo de la funcion
E->>L: Ocurre algo (se sube una foto)
L->>L: Busca un entorno disponible
alt Arranque en FRIO (no hay entorno)
L->>M: Crea microVM Firecracker
M->>M: Descarga el codigo y las capas
M->>F: Ejecuta el codigo GLOBAL (imports, clientes boto3)
Note over M,F: 100 ms - 2 s. SE FACTURA (desde 2024)
end
L->>F: Invoca lambda_handler(event, context)
F-->>L: Devuelve la respuesta
Note over M: El entorno se CONGELA, no se destruye
E->>L: Llega otro evento en pocos minutos
L->>F: Reutiliza el entorno: arranque en CALIENTE
Note over M: Tras ~5-15 min sin uso, se destruye
De aquí sale la optimización más rentable de Lambda, y es una línea de código:
import boto3
# CORRECTO: fuera del manejador. Se ejecuta UNA VEZ por entorno y se reutiliza
# en todas las invocaciones siguientes. Crear un cliente boto3 cuesta 100-300 ms.
s3 = boto3.client("s3")
def lambda_handler(event, context):
# INCORRECTO seria crear el cliente aqui: se pagarian esos 200 ms
# en CADA invocacion.
...Y de aquí sale también la trampa correspondiente: el entorno se reutiliza, así que las variables globales persisten entre invocaciones. Es útil para cachés, pero desastroso si acumulas estado por descuido:
resultados = [] # PELIGRO: sobrevive entre invocaciones
def lambda_handler(event, context):
resultados.append(event) # crece sin control hasta agotar la memoriaArranque en frío y arranque en caliente
El arranque en frío es el tiempo que tarda AWS en preparar un entorno nuevo: crear la microVM, descargar el código y las capas, inicializar el intérprete y ejecutar el código global.
| Factor | Efecto sobre el arranque en frío |
|---|---|
| Lenguaje | Python y Node.js: 100-400 ms. Java y .NET: 1-4 s. Go y Rust: < 100 ms |
| Tamaño del paquete | Cuanto más grande, más tarda la descarga |
| Código de inicialización | Cargar un modelo grande o conectar a una base de datos alarga el arranque |
| VPC | Antes añadía 8-10 s; hoy la penalización es de decenas de ms |
| Memoria asignada | Más memoria = más CPU = inicialización más rápida |
Cuándo importa de verdad: si la Lambda responde a una petición síncrona de un usuario (una API), 1,5 segundos extra son inaceptables. Si procesa un evento asíncrono (la miniatura de una foto), a nadie le importa.
Estrategias de mitigación, de menor a mayor coste:
- Mover trabajo al código global y mantener el paquete pequeño. Gratis.
- Elegir un lenguaje ligero. Python o Node para funciones de latencia crítica.
- Subir la memoria. Más CPU acelera la inicialización, y a menudo el coste total no sube porque la ejecución dura menos.
- Concurrencia provisionada. AWS mantiene N entornos siempre listos. Elimina el arranque en frío por completo, pero se paga aunque no se use.
Concurrencia: reservada, provisionada y el límite de la cuenta
Lambda no encola peticiones: crea entornos nuevos. Si llegan 100 eventos simultáneos, se ejecutan 100 instancias de la función a la vez. Esa es su gran ventaja y también su riesgo.
| Concepto | Qué es | Se paga |
|---|---|---|
| Concurrencia bajo demanda | Entornos creados según llegan los eventos | Solo la ejecución |
| Límite de la cuenta | Techo por región, 1.000 por defecto (ampliable) | — |
| Concurrencia reservada | Porción del límite apartada para una función | Nada extra, pero resta del total disponible |
| Concurrencia provisionada | Entornos precalentados siempre listos | Sí, aunque estén ociosos |
La concurrencia reservada tiene un doble efecto que conviene entender:
- Garantiza que esa función siempre podrá usar esa cantidad, aunque otras estén saturando la cuenta.
- Limita esa función a ese máximo. Es un freno.
Ese freno es exactamente lo que MercadoFresco necesita para proteger la base de datos:
# La funcion que consulta pedidos NUNCA abrira mas de 20 conexiones a RDS,
# aunque lleguen 5.000 peticiones simultaneas. Protege la base de datos
# de ser el eslabon que se rompe.
aws lambda put-function-concurrency \
--function-name mercadofresco-estado-pedido \
--reserved-concurrent-executions 20 \
--profile mercadofresco-dev --region eu-west-1Sin ese límite, un pico de tráfico podría abrir cientos de conexiones a mercadofresco-pedidos y
tumbar la base de datos. Es un caso real y frecuente: Lambda escala, RDS no.
Configuración: memoria, CPU, tiempo de espera, variables y arquitectura
Memoria y CPU van juntas
Este es el parámetro que más gente configura mal. En Lambda solo se asigna memoria, de 128 MB a 10.240 MB, y la CPU se asigna proporcionalmente:
| Memoria | vCPU aproximada | Comentario |
|---|---|---|
| 128 MB | 0,08 | Mínimo. Muy lento para cualquier cálculo |
| 512 MB | 0,29 | |
| 1.769 MB | 1,00 | Un núcleo completo. Punto de referencia clave |
| 3.008 MB | 1,70 | |
| 10.240 MB | 6,00 | Máximo, con multihilo real |
La consecuencia es contraintuitiva y merece un cálculo. Una función que procesa una imagen:
| Memoria | Duración | Coste por 1.000 invocaciones |
|---|---|---|
| 512 MB | 8.000 ms | 512/1024 × 8 × 1.000 × 0,0000166667 = 0,0667 USD |
| 1.024 MB | 4.000 ms | 1 × 4 × 1.000 × 0,0000166667 = 0,0667 USD |
| 1.769 MB | 2.200 ms | 1,73 × 2,2 × 1.000 × 0,0000166667 = 0,0634 USD |
| 3.008 MB | 2.000 ms | 2,94 × 2 × 1.000 × 0,0000166667 = 0,0980 USD |
Más memoria puede salir igual de barato o más barato, y ocho veces más rápido. Poner 128 MB "para ahorrar" suele ser un error: la función tarda tanto que el coste no baja y la latencia se dispara. La herramienta AWS Lambda Power Tuning automatiza este análisis.
Tiempo de espera
De 1 segundo a 15 minutos (900 s), con 3 segundos por defecto. Regla: ponlo un poco por encima de lo que la función tarda realmente, nunca al máximo. Un tiempo de espera de 900 s en una función que debería tardar 2 s significa que un cuelgue se pagará durante 15 minutos.
Variables de entorno
aws lambda update-function-configuration \
--function-name mercadofresco-generar-miniaturas \
--environment "Variables={
BUCKET_DESTINO=mercadofresco-catalogo-fotos,
PREFIJO_MINIATURAS=miniaturas/,
ANCHO_MINIATURA=200,
NIVEL_LOG=INFO}" \
--profile mercadofresco-dev --region eu-west-1Se cifran en reposo automáticamente, pero son visibles en la consola para cualquiera con permiso de lectura. Nunca metas contraseñas ahí: usa Secrets Manager o Parameter Store (lección 04-03), como hicimos con RDS en 02-04.
Arquitectura: x86_64 frente a arm64 (Graviton)
| x86_64 | arm64 (Graviton2) | |
|---|---|---|
| Precio | Referencia | ~20 % más barato |
| Rendimiento | Referencia | Igual o mejor en la mayoría de cargas |
| Compatibilidad | Universal | Las dependencias binarias deben compilarse para ARM |
Elige arm64 salvo que una dependencia te lo impida. Es un 20 % de ahorro por cambiar un parámetro. La única precaución es que las bibliotecas con código nativo —como Pillow, que usaremos ahora— deben empaquetarse para la arquitectura correcta.
La función de las miniaturas: el código
Llegamos al objetivo. En la lección 02-03 configuramos la notificación del bucket
mercadofresco-catalogo-fotos para que cada .jpg subido bajo productos/ disparara la función
mercadofresco-generar-miniaturas. Vamos a escribirla.
"""
mercadofresco-generar-miniaturas
Genera una miniatura de 200 px de ancho cada vez que se sube una foto de
producto al bucket mercadofresco-catalogo-fotos bajo el prefijo productos/.
Disparador: evento s3:ObjectCreated:* filtrado por prefijo 'productos/'
y sufijo '.jpg' (configurado en la lección 02-03).
Salida: objeto en el prefijo 'miniaturas/' del mismo bucket.
IMPORTANTE: el prefijo de salida es DISTINTO del de entrada. Si escribiéramos
la miniatura en 'productos/' se generaría un nuevo evento que volvería a
invocar esta función: un bucle infinito y muy caro.
"""
import io
import logging
import os
import urllib.parse
import boto3
from botocore.exceptions import ClientError
from PIL import Image
# --- Código GLOBAL: se ejecuta una sola vez por entorno de ejecución ---
# Crear el cliente aquí ahorra 100-300 ms en cada invocación posterior.
s3 = boto3.client("s3")
logger = logging.getLogger()
logger.setLevel(os.environ.get("NIVEL_LOG", "INFO"))
PREFIJO_MINIATURAS = os.environ.get("PREFIJO_MINIATURAS", "miniaturas/")
ANCHO = int(os.environ.get("ANCHO_MINIATURA", "200"))
CALIDAD_JPEG = 82
def _clave_miniatura(clave_origen: str) -> str:
"""Convierte 'productos/frutas/naranjas.jpg' en 'miniaturas/frutas/naranjas.jpg'.
Se conserva la estructura de subprefijos para poder localizar la miniatura
a partir del original sin necesidad de consultar ninguna base de datos.
"""
sin_prefijo = clave_origen.split("/", 1)[1] if "/" in clave_origen else clave_origen
return f"{PREFIJO_MINIATURAS}{sin_prefijo}"
def _generar(datos: bytes) -> bytes:
"""Redimensiona manteniendo la proporción y devuelve los bytes del JPEG."""
with Image.open(io.BytesIO(datos)) as img:
# Las fotos de móvil llevan orientación en los metadatos EXIF; sin esto
# algunas miniaturas saldrían giradas 90 grados.
img = img.convert("RGB")
alto = int(img.height * (ANCHO / img.width))
# LANCZOS da la mejor calidad al reducir. thumbnail() nunca amplía,
# así que una foto ya pequeña se deja como está.
img.thumbnail((ANCHO, alto), Image.Resampling.LANCZOS)
salida = io.BytesIO()
img.save(salida, format="JPEG", quality=CALIDAD_JPEG, optimize=True)
salida.seek(0)
return salida.read()
def lambda_handler(event, context):
"""Punto de entrada. Un evento de S3 puede traer VARIOS registros."""
procesadas, fallidas = 0, 0
for registro in event.get("Records", []):
bucket = registro["s3"]["bucket"]["name"]
# Las claves llegan codificadas como URL: 'a+b.jpg' o '%C3%B1'.
# Sin unquote_plus, una foto con espacios o eñes daría NoSuchKey.
clave = urllib.parse.unquote_plus(registro["s3"]["object"]["key"])
# Defensa en profundidad: aunque el filtro del bucket ya lo impide,
# comprobamos que no estamos procesando una miniatura.
if clave.startswith(PREFIJO_MINIATURAS):
logger.warning("Se ignora %s: ya es una miniatura", clave)
continue
try:
logger.info("Procesando s3://%s/%s", bucket, clave)
original = s3.get_object(Bucket=bucket, Key=clave)["Body"].read()
miniatura = _generar(original)
destino = _clave_miniatura(clave)
s3.put_object(
Bucket=bucket,
Key=destino,
Body=miniatura,
ContentType="image/jpeg",
CacheControl="public, max-age=604800", # una semana de caché
Metadata={"origen": clave, "generado-por": context.function_name},
Tagging="Proyecto=mercadofresco&Componente=catalogo&Propietario=luis",
)
logger.info(
"Miniatura creada: s3://%s/%s (%d KB -> %d KB)",
bucket, destino, len(original) // 1024, len(miniatura) // 1024,
)
procesadas += 1
except ClientError as e:
codigo = e.response["Error"]["Code"]
if codigo == "NoSuchKey":
# El objeto se borró entre el evento y esta ejecución.
# No es un error recuperable: reintentar no serviría de nada.
logger.warning("El objeto %s ya no existe, se omite", clave)
continue
logger.error("Error de S3 con %s: %s", clave, e)
fallidas += 1
raise # relanzar activa el reintento automático de Lambda
except Exception as e:
logger.exception("Error inesperado con %s: %s", clave, e)
fallidas += 1
raise
return {"procesadas": procesadas, "fallidas": fallidas}Los detalles del código que marcan la diferencia entre un ejemplo de tutorial y algo que aguanta en producción:
unquote_plussobre la clave. S3 codifica los nombres en el evento. Sin esta línea, cualquier foto con espacios, tildes o eñes daríaNoSuchKey. Es el fallo número uno de las Lambdas de S3.event["Records"]es una lista. Un solo evento puede traer varios registros. Procesarlo conevent["Records"][0]funciona en las pruebas y falla en producción.- Prefijo de salida distinto del de entrada, más la comprobación explícita. Doble protección contra el bucle recursivo.
raiseen los errores recuperables. Lambda reintenta automáticamente las invocaciones asíncronas; tragarse la excepción con unreturnharía que el fallo pasara desapercibido.NoSuchKeyse trata aparte. Es un error no recuperable: reintentarlo tres veces es tiempo y dinero perdidos.- Etiquetado del objeto con el esquema del proyecto, coherente con lo que venimos haciendo.
Empaquetado con dependencias y capas
Pillow no viene incluido en el entorno de Lambda: hay que aportarlo. Hay tres formas y conviene
saber cuándo usar cada una.
| Método | Cuándo | Límite |
|---|---|---|
| Código en línea | Función trivial sin dependencias externas | Se edita en la consola |
| Archivo ZIP | Lo habitual: código + dependencias | 50 MB comprimido, 250 MB descomprimido |
| Capa (layer) | Dependencias compartidas por varias funciones | 5 capas por función, 250 MB en total |
| Imagen de contenedor | Dependencias enormes (ML, ffmpeg) | 10 GB |
Para MercadoFresco, una capa con Pillow es la elección correcta: la compartirán la función de miniaturas y las que vengan después, y mantiene el paquete de código pequeño (arranques en frío más rápidos y edición posible en la consola).
# --- Construir la capa con Pillow para arm64 ---
mkdir -p capa-pillow/python
# Es IMPRESCINDIBLE compilar para la plataforma de destino: Pillow lleva código
# nativo, y una rueda de macOS o de x86 fallaría con "invalid ELF header".
pip install Pillow \
--target capa-pillow/python \
--platform manylinux2014_aarch64 \
--implementation cp \
--python-version 3.12 \
--only-binary=:all:
cd capa-pillow && zip -r ../capa-pillow.zip python && cd ..
# Publicar la capa
LAYER_ARN=$(aws lambda publish-layer-version \
--layer-name mercadofresco-imagen \
--description "Pillow 10 para procesar fotos de producto (arm64)" \
--zip-file fileb://capa-pillow.zip \
--compatible-runtimes python3.12 \
--compatible-architectures arm64 \
--query 'LayerVersionArn' --output text \
--profile mercadofresco-dev --region eu-west-1)
echo "Capa publicada: $LAYER_ARN"La estructura de directorios de la capa no es negociable: Python busca las bibliotecas en
/opt/python, y AWS descomprime la capa en /opt. Por eso todo debe colgar de un directorio
llamado exactamente python/. Cada lenguaje tiene su ruta (nodejs/node_modules para Node.js).
# --- Empaquetar el código de la función (sin dependencias: van en la capa) ---
zip funcion-miniaturas.zip lambda_function.pyDesplegar por consola y por CLI
Por consola
- Consola → Lambda → región Irlanda → Crear función.
- Crear desde cero. Nombre:
mercadofresco-generar-miniaturas. Tiempo de ejecución: Python 3.12. Arquitectura: arm64. - Cambiar el rol de ejecución → crear uno nuevo (lo ajustaremos en el apartado siguiente).
- Pega el código en el editor o sube el ZIP.
- Configuración → Configuración general: memoria 1.024 MB, tiempo de espera 30 s.
- Configuración → Variables de entorno: las cuatro que definimos antes.
- Capas → Añadir una capa →
mercadofresco-imagen. - Desencadenadores → Añadir desencadenador → S3 → bucket
mercadofresco-catalogo-fotos, eventoTodos los eventos de creación de objetos, prefijoproductos/, sufijo.jpg. - Etiquetas: el esquema obligatorio del proyecto.
Por CLI
aws lambda create-function \
--function-name mercadofresco-generar-miniaturas \
--runtime python3.12 \
--architectures arm64 \
--handler lambda_function.lambda_handler \
--role arn:aws:iam::111122223333:role/rol-lambda-miniaturas \
--zip-file fileb://funcion-miniaturas.zip \
--layers "$LAYER_ARN" \
--memory-size 1024 \
--timeout 30 \
--environment "Variables={
PREFIJO_MINIATURAS=miniaturas/,
ANCHO_MINIATURA=200,
NIVEL_LOG=INFO}" \
--tags Proyecto=mercadofresco,Entorno=produccion,Componente=catalogo,\
Propietario=luis,CentroCoste=marketing \
--profile mercadofresco-dev --region eu-west-1
# Permitir que S3 invoque la función. Sin esto, el evento se dispara
# y no pasa nada, sin ningún mensaje de error visible.
aws lambda add-permission \
--function-name mercadofresco-generar-miniaturas \
--statement-id permitir-s3-catalogo \
--action lambda:InvokeFunction \
--principal s3.amazonaws.com \
--source-arn arn:aws:s3:::mercadofresco-catalogo-fotos \
--source-account 111122223333 \
--profile mercadofresco-dev --region eu-west-1Actualizar el código después de un cambio:
zip funcion-miniaturas.zip lambda_function.py
aws lambda update-function-code \
--function-name mercadofresco-generar-miniaturas \
--zip-file fileb://funcion-miniaturas.zip \
--publish \
--profile mercadofresco-dev --region eu-west-1--publish crea una versión inmutable numerada. Combinada con alias (produccion,
pruebas), permite despliegues graduales dirigiendo, por ejemplo, el 10 % del tráfico a la versión
nueva. Es otra pieza contra el problema 4 de MercadoFresco, los despliegues arriesgados, que se
aborda de lleno en el módulo 8.
Probar sin subir nada a S3:
cat > evento-prueba.json <<'JSON'
{
"Records": [{
"eventName": "ObjectCreated:Put",
"s3": {
"bucket": {"name": "mercadofresco-catalogo-fotos"},
"object": {"key": "productos/frutas/naranjas-valencia-1kg.jpg"}
}
}]
}
JSON
aws lambda invoke \
--function-name mercadofresco-generar-miniaturas \
--payload fileb://evento-prueba.json \
--log-type Tail \
--query 'LogResult' --output text \
respuesta.json \
--profile mercadofresco-dev --region eu-west-1 | base64 -d--log-type Tail devuelve los últimos 4 KB del log codificados en base64: la forma más rápida de
depurar sin salir de la terminal.
Aviso de coste. El nivel gratuito de Lambda es permanente, no de 12 meses: 1 millón de peticiones y 400.000 GB-segundo al mes, para siempre. Esta práctica no te costará nada. Aun así, borra la función al terminar si no vas a seguir:
aws lambda delete-function --function-name mercadofresco-generar-miniaturas. Y recuerda eliminar también la notificación del bucket y la capa si no las usas.
Permisos: el rol de ejecución y el mínimo privilegio
Una función Lambda no tiene credenciales propias: asume un rol de ejecución de IAM, y los permisos de ese rol son exactamente lo que la función puede hacer. Ni más, ni menos.
La política de confianza, que define quién puede asumir el rol:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}Y la política de permisos, que define qué puede hacer. Aquí está el principio de mínimo privilegio aplicado con rigor:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "EscribirRegistros",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:eu-west-1:111122223333:log-group:/aws/lambda/mercadofresco-generar-miniaturas:*"
},
{
"Sid": "LeerSoloLasFotosOriginales",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::mercadofresco-catalogo-fotos/productos/*"
},
{
"Sid": "EscribirSoloEnMiniaturas",
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:PutObjectTagging"],
"Resource": "arn:aws:s3:::mercadofresco-catalogo-fotos/miniaturas/*"
}
]
}Cuatro decisiones deliberadas, y cada una evita un problema real:
s3:GetObjectsolo bajoproductos/. Si la función se ve comprometida, no puede leer los informes de ventas ni las copias de la base de datos.s3:PutObjectsolo bajominiaturas/. Aunque hubiera un fallo lógico, la función no puede sobrescribir las fotos originales. Es la última defensa contra el bucle recursivo: incluso si el filtro del bucket fallara, el escritor no tiene permiso sobre el prefijo de entrada.- Sin
s3:DeleteObject. La función no necesita borrar nada, así que no puede. - Los registros acotados a su propio grupo de logs, no a
*.
Comparado con la práctica habitual de adjuntar AmazonS3FullAccess "para que funcione", la
diferencia entre un incidente menor y una filtración completa está en estas tres instrucciones. La
lógica completa de IAM, los roles, las políticas y su evaluación es la lección 04-01.
Registros en CloudWatch Logs y depuración
Cada función escribe automáticamente en un grupo de logs llamado
/aws/lambda/<nombre-de-la-función>. Todo lo que imprimas con print() o logger.info() acaba
ahí.
# Seguir los registros en vivo mientras pruebas
aws logs tail /aws/lambda/mercadofresco-generar-miniaturas --follow \
--profile mercadofresco-dev --region eu-west-1
# Buscar solo los errores de la última hora
aws logs tail /aws/lambda/mercadofresco-generar-miniaturas --since 1h \
--filter-pattern "ERROR" \
--profile mercadofresco-dev --region eu-west-1Al final de cada invocación, Lambda escribe una línea REPORT que es oro puro para ajustar la
configuración:
REPORT RequestId: 8f3c1a2b-... Duration: 1843.21 ms Billed Duration: 1844 ms
Memory Size: 1024 MB Max Memory Used: 187 MB Init Duration: 412.55 msCómo se lee:
| Campo | Qué dice | Qué hacer con él |
|---|---|---|
Duration |
Tiempo real de ejecución | Si crece, algo se ha degradado |
Billed Duration |
Lo que se factura (redondeado al ms) | Base del cálculo de coste |
Memory Size |
Lo que has asignado | 1.024 MB |
Max Memory Used |
Lo que realmente ha usado | 187 MB de 1.024: sobra memoria |
Init Duration |
Tiempo del arranque en frío | Solo aparece en arranques fríos |
En este caso la tentación es bajar la memoria a 256 MB. Cuidado: recuerda que la CPU es proporcional. Con 256 MB la función tardaría unos 7 segundos en lugar de 1,8, y el coste sería prácticamente el mismo con cuatro veces más latencia. La decisión correcta es probar 512 MB y medir.
Otras dos capacidades básicas de depuración:
- Métricas automáticas en CloudWatch:
Invocations,Errors,Duration,Throttles,ConcurrentExecutions. Una alarma sobreErrors > 0es lo mínimo exigible en producción. - Registros estructurados en JSON: configurando el formato de log en JSON, los campos se pueden consultar con CloudWatch Logs Insights sin analizar texto.
La monitorización a fondo —paneles, alarmas, retención, Logs Insights— es la lección 05-01, y el trazado distribuido de una petición que atraviesa varias funciones y servicios es 05-02.
Orígenes de eventos habituales
Lambda se integra con más de 200 servicios. Estos son los que importan al empezar:
| Origen | Invocación | Estructura del evento | Caso en MercadoFresco | Se ve en |
|---|---|---|---|---|
| Amazon S3 | Asíncrona | Records[].s3 |
Generar miniaturas | 02-03 y esta lección |
| API Gateway / Function URL | Síncrona | requestContext, body |
Endpoint de estado de pedido | Esta lección |
| Amazon SQS | Sondeo por lotes | Records[].body |
Procesar pedidos del pico de los viernes | Módulo 7 (07-01) |
| Amazon SNS | Asíncrona | Records[].Sns |
Avisar de una incidencia de reparto | Módulo 7 (07-02) |
| Amazon EventBridge | Asíncrona | detail, source |
Tarea programada cada mañana | Módulo 7 (07-03) |
| DynamoDB Streams | Sondeo por lotes | Records[].dynamodb |
Reaccionar a cambios de datos | Módulo 6 (06-02) |
| CloudWatch Logs | Asíncrona | Datos comprimidos | Alertar ante patrones de error | Módulo 5 (05-01) |
Las tres formas de invocación tienen consecuencias muy distintas en el comportamiento ante errores:
| Modo | Quién espera respuesta | Reintentos automáticos | Ejemplo |
|---|---|---|---|
| Síncrona | El invocador espera | Ninguno (los gestiona el cliente) | API Gateway |
| Asíncrona | Nadie espera | 2 reintentos | S3, SNS |
| Sondeo (poll) | Lambda consulta la fuente | Hasta que caduque o vaya a la cola de fallidos | SQS, DynamoDB Streams |
Segunda función: consultar el estado de un pedido por HTTP
La primera función reaccionaba a un evento interno. Ahora expondremos una función a internet para que la aplicación móvil de MercadoFresco consulte el estado de un pedido.
Hay dos formas de poner HTTP delante de una Lambda:
| Function URL | API Gateway | |
|---|---|---|
| Configuración | Una casilla | Un servicio aparte que configurar |
| Coste | Gratis | ~1 USD por millón de peticiones |
| Dominio propio | No | Sí |
| Autenticación | Ninguna o IAM | IAM, Cognito, JWT, autorizadores propios |
| Limitación de tasa | No | Sí |
| Varias rutas | No, una sola URL | Sí, enrutado completo |
| Cuándo | Prototipos, webhooks internos | Producción |
Empezamos con Function URL por simplicidad, sabiendo que producción pedirá API Gateway.
"""
mercadofresco-estado-pedido
Devuelve el estado de un pedido a partir de su identificador.
Invocación: Function URL (HTTP GET), p. ej. /?pedido=48213
Concurrencia reservada = 20: la base de datos RDS no puede soportar
cientos de conexiones simultáneas, así que se pone el freno aquí.
"""
import json
import logging
import os
import boto3
import psycopg
logger = logging.getLogger()
logger.setLevel("INFO")
# --- Código global: se ejecuta una vez por entorno ---
# El secreto se lee UNA VEZ y se reutiliza en las invocaciones siguientes.
_secretos = boto3.client("secretsmanager")
_credenciales = json.loads(
_secretos.get_secret_value(SecretId=os.environ["MF_SECRETO_BD"])["SecretString"]
)
DSN = (
f"host={os.environ['MF_BD_HOST']} port=5432 dbname=pedidos "
f"user={_credenciales['username']} password={_credenciales['password']} "
f"sslmode=require connect_timeout=3"
)
# Una conexión por entorno de ejecución, reutilizada entre invocaciones.
# Es la razón por la que la concurrencia reservada limita las conexiones a RDS.
_conexion = None
def _obtener_conexion():
global _conexion
if _conexion is None or _conexion.closed:
_conexion = psycopg.connect(DSN)
return _conexion
def _respuesta(codigo: int, cuerpo: dict) -> dict:
"""Formato de respuesta que espera una Function URL o API Gateway."""
return {
"statusCode": codigo,
"headers": {
"Content-Type": "application/json; charset=utf-8",
"Cache-Control": "no-store",
},
"body": json.dumps(cuerpo, ensure_ascii=False),
}
def lambda_handler(event, context):
# Los parámetros de consulta llegan en queryStringParameters (puede ser None).
parametros = event.get("queryStringParameters") or {}
pedido_id = parametros.get("pedido")
if not pedido_id or not pedido_id.isdigit():
return _respuesta(400, {"error": "Falta el parámetro 'pedido' o no es numérico"})
try:
con = _obtener_conexion()
with con.cursor() as cur:
# Consulta parametrizada: NUNCA concatenar el identificador en el SQL.
cur.execute(
"""
SELECT id, estado, creado_en, entrega_estimada, ciudad
FROM pedidos
WHERE id = %s
""",
(int(pedido_id),),
)
fila = cur.fetchone()
if fila is None:
return _respuesta(404, {"error": "Pedido no encontrado"})
return _respuesta(200, {
"pedido": fila[0],
"estado": fila[1],
"creado_en": fila[2].isoformat(),
"entrega_estimada": fila[3].isoformat() if fila[3] else None,
"ciudad": fila[4],
})
except psycopg.OperationalError as e:
# La conexión pudo morir por una conmutación de RDS (lección 02-04).
# Se descarta para que la siguiente invocación cree una nueva.
logger.error("Error de conexión a la base de datos: %s", e)
global _conexion
_conexion = None
return _respuesta(503, {"error": "Servicio temporalmente no disponible"})
except Exception:
logger.exception("Error inesperado consultando el pedido %s", pedido_id)
# Nunca devolver el detalle de la excepción al cliente: filtra información.
return _respuesta(500, {"error": "Error interno"})# Crear la Function URL. AuthType NONE la deja pública: solo para pruebas.
# En producción se pone API Gateway con autenticación delante.
aws lambda create-function-url-config \
--function-name mercadofresco-estado-pedido \
--auth-type NONE \
--cors '{"AllowOrigins":["https://mercadofresco.example"],"AllowMethods":["GET"]}' \
--profile mercadofresco-dev --region eu-west-1
# Limitar la concurrencia para proteger a RDS
aws lambda put-function-concurrency \
--function-name mercadofresco-estado-pedido \
--reserved-concurrent-executions 20 \
--profile mercadofresco-dev --region eu-west-1Cuatro decisiones de diseño que merecen atención:
- La conexión a la base de datos vive en el ámbito global y se reutiliza. Abrir una conexión a PostgreSQL cuesta decenas de milisegundos; hacerlo en cada invocación multiplicaría la latencia y saturaría RDS.
- Concurrencia reservada de 20 como techo de conexiones. Sin esto, un pico podría abrir cientos
de conexiones y tumbar
mercadofresco-pedidos. Es la lección de 02-04 aplicada: Lambda escala, RDS no. (Para casos exigentes existe RDS Proxy, que agrupa conexiones; queda fuera del alcance de esta lección.) - Consulta parametrizada con
%s, nunca concatenación de cadenas. Inyección SQL evitada. - Los errores internos no se detallan al cliente: se registran en CloudWatch y se devuelve un mensaje genérico.
Límites reales que hay que conocer
| Límite | Valor | Consecuencia práctica |
|---|---|---|
| Duración máxima | 15 minutos | Un proceso más largo necesita Step Functions (07-04), ECS o EC2 |
| Memoria | 128 MB a 10.240 MB | La CPU va ligada a este valor |
| Paquete ZIP comprimido | 50 MB (subida directa) | Por encima, subir desde S3 |
| Paquete descomprimido + capas | 250 MB | El límite que más molesta. Alternativa: imagen de contenedor (10 GB) |
Espacio en /tmp |
512 MB por defecto, hasta 10.240 MB | Suficiente para las fotos; configurable con --ephemeral-storage |
| Payload síncrono | 6 MB entrada y salida | No devuelvas ficheros grandes: guárdalos en S3 y devuelve una URL prefirmada (02-03) |
| Payload asíncrono | 256 KB | El evento de S3 cabe de sobra |
| Concurrencia por región | 1.000 por defecto | Ampliable con una solicitud de cuota |
| Variables de entorno | 4 KB en total | No metas ahí ficheros de configuración |
| Capas por función | 5 | Agrupa dependencias relacionadas |
El límite de los 15 minutos es el que más decisiones de arquitectura condiciona. Si el proceso nocturno que recomprime las 40.000 fotos del catálogo tarda 3 horas, no es un trabajo para una sola Lambda: o se reparte en 40.000 invocaciones de dos segundos (que sí encajan perfectamente), o se ejecuta en una tarea de contenedor. Repartir es casi siempre la respuesta correcta con Lambda.
Precios, con el cálculo concreto de MercadoFresco
Lambda cobra dos conceptos:
- Peticiones: 0,20 USD por millón.
- Duración: 0,0000166667 USD por GB-segundo (x86_64; arm64 es un 20 % menos).
Y el nivel gratuito es permanente, no de 12 meses: 1 millón de peticiones y 400.000 GB-segundo al mes, siempre.
Caso 1: la función de miniaturas. Luis sube unas 20 fotos al día (600 al mes), la función usa 1.024 MB y tarda 1,8 segundos:
Peticiones: 600 → dentro del millón gratuito = 0,00 USD
Duración: 600 × 1,8 s × 1 GB = 1.080 GB-segundo
→ dentro de los 400.000 gratuitos = 0,00 USD
--------------------------------------------------------------
Total = 0,00 USD/mesCoste cero. El equivalente en una EC2 t3.micro encendida para hacer lo mismo serían unos 7,50
USD al mes. Este es exactamente el caso de uso para el que Lambda existe.
Caso 2: el endpoint de estado de pedidos, en el escenario realista. MercadoFresco recibe unas 900.000 consultas al mes, la función usa 512 MB y tarda 120 ms, en arm64:
Peticiones: 900.000 → dentro del millón gratuito = 0,00 USD
Duración: 900.000 × 0,12 s × 0,5 GB = 54.000 GB-s
→ dentro de los 400.000 gratuitos = 0,00 USD
--------------------------------------------------------------
Total = 0,00 USD/mesCaso 3: crecimiento a tres ciudades más (problema 3). Diez veces el tráfico: 9 millones de consultas al mes:
Peticiones: (9.000.000 − 1.000.000) / 1.000.000 × 0,20 = 1,60 USD
Duración: 9.000.000 × 0,12 × 0,5 = 540.000 GB-segundo
(540.000 − 400.000) × 0,0000166667 = 2,33 USD
Descuento arm64 (−20 % sobre la duración) = −0,47 USD
------------------------------------------------------------------------
Total ≈ 3,46 USD/mesMenos de 4 dólares al mes para servir 9 millones de peticiones, sin un servidor que administrar, parchear o vigilar. Cuando la carga es irregular y por eventos, el modelo sin servidor es difícilmente batible.
El punto de equilibrio, para tenerlo presente: a partir de una utilización sostenida superior al 50-60 % de un servidor, un contenedor o una EC2 reservada salen más baratos. Lambda es cara si la usas como si fuera un servidor siempre encendido.
Errores, reintentos y colas de mensajes fallidos
Qué ocurre cuando la función falla depende del modo de invocación:
| Modo | Comportamiento ante error |
|---|---|
| Síncrona (Function URL, API Gateway) | El error se devuelve al cliente. Sin reintentos |
| Asíncrona (S3, SNS, EventBridge) | Lambda reintenta 2 veces con espera creciente; si sigue fallando, el evento se descarta |
| Sondeo (SQS, DynamoDB Streams) | Reintenta hasta que el mensaje caduca o va a la cola de mensajes fallidos |
"El evento se descarta" es la frase peligrosa. Si la función de miniaturas falla tres veces con una foto concreta, esa foto se queda sin miniatura y nadie se entera. La protección es un destino de error o una cola de mensajes fallidos (dead-letter queue, DLQ): un sitio donde acaban los eventos que no se pudieron procesar, para revisarlos y reprocesarlos.
# Configurar destinos: los fallos van a una cola SQS, los éxitos se ignoran.
aws lambda put-function-event-invoke-config \
--function-name mercadofresco-generar-miniaturas \
--maximum-retry-attempts 2 \
--maximum-event-age-in-seconds 3600 \
--destination-config '{
"OnFailure": {
"Destination": "arn:aws:sqs:eu-west-1:111122223333:mercadofresco-miniaturas-fallidas"
}
}' \
--profile mercadofresco-dev --region eu-west-1Tres principios que hay que respetar desde el primer día:
- Idempotencia. La entrega es "al menos una vez": la misma foto puede procesarse dos veces. Nuestra función lo cumple porque regenerar una miniatura sobre sí misma da el mismo resultado. Si la función incrementara un contador o cobrara un pago, habría que protegerla explícitamente.
- Distinguir errores recuperables de los que no lo son. Un tiempo de espera de red merece
reintento; un
NoSuchKeyno. Reintentar lo irrecuperable es tiempo y dinero tirados. - Vigilar la DLQ. Una cola de fallidos que nadie mira es lo mismo que no tenerla. Una alarma
sobre
ApproximateNumberOfMessagesVisible > 0es obligatoria.
Aquí solo dejamos planteado el concepto. SQS se estudia en la lección 07-01, y los patrones completos de idempotencia, reintentos y colas de mensajes fallidos son la lección 07-05.
Recapitulación del módulo: qué está ya en AWS y qué falta
Este es el punto del curso donde conviene levantar la vista. MercadoFresco empezó el módulo 2 con un servidor físico en la oficina; termina con esto:
flowchart TB
subgraph AWS["Cuenta 111122223333 - eu-west-1 (Irlanda)"]
subgraph COMPUTO["Computo"]
ASG["Auto Scaling Group<br/>asg-mercadofresco-tienda<br/>2-4 instancias t3.micro<br/>en dos AZ (02-01)"]
L1["Lambda<br/>generar-miniaturas (02-05)"]
L2["Lambda<br/>estado-pedido (02-05)"]
end
subgraph DATOS["Datos"]
S3["S3 mercadofresco-catalogo-fotos<br/>versionado + ciclo de vida (02-03)"]
RDS["RDS PostgreSQL<br/>mercadofresco-pedidos<br/>Multi-AZ + replica (02-04)"]
EBS["EBS gp3 + snapshots DLM (02-02)"]
end
ASG --> EBS
ASG --> RDS
S3 -->|"evento ObjectCreated"| L1
L1 --> S3
L2 --> RDS
end
U["Clientes de MercadoFresco"] --> ASG
U --> L2
Lo que está resuelto y lo que no:
| Problema | Estado | Dónde se resolvió |
|---|---|---|
| 1. Caídas de los viernes | Parcial: hay Auto Scaling, falta el balanceador que reparta el tráfico | 02-01; se completa en 03-03 |
| 2. Copias no fiables | Resuelto: snapshots EBS con DLM, versionado de S3, copias automáticas de RDS con PITR | 02-02, 02-03, 02-04 |
| 3. No poder crecer a más ciudades | Parcial: la infraestructura ya escala, falta distribución global y arquitectura desacoplada | 02-01, 02-05; módulos 3, 6 y 7 |
| 4. Despliegues arriesgados | Iniciado: plantillas de lanzamiento versionadas, versiones y alias de Lambda | 02-01, 02-05; se resuelve en el módulo 8 |
Y lo que aún no existe, que es mucho:
- Red propia. Todo está en la VPC por defecto, con las instancias expuestas. Falta diseñar subredes públicas y privadas, tablas de rutas, y sacar la base de datos a una subred sin salida a internet. Módulo 3.
- Balanceador de carga. Las cuatro instancias del ASG no reciben tráfico repartido, y no hay HTTPS ni dominio propio. 03-03 y 03-05.
- Distribución de contenido. El 97 % de la factura de S3 era transferencia de salida: CloudFront lo arregla. 03-04.
- Identidad y secretos con rigor. Hemos usado roles y Secrets Manager de pasada; falta la estructura completa de IAM, el cifrado con claves propias y la protección frente a ataques. Módulo 4.
- Observabilidad. Tenemos métricas sueltas y algún log. Falta un sistema de paneles, alarmas, trazas y auditoría de quién hizo qué. Módulo 5.
- Infraestructura como código. Todo lo hemos creado a mano o con comandos sueltos: no es reproducible ni revisable. Módulo 9.
Errores Comunes y Consejos
- Crear los clientes de boto3 dentro del manejador. Se pagan 100-300 ms en cada invocación. Siempre en el ámbito global.
- No decodificar la clave de S3 con
unquote_plus. Cualquier foto con espacios o eñes fallará conNoSuchKey. Es el fallo número uno de las Lambdas disparadas por S3. - Procesar solo
event["Records"][0]. Un evento puede traer varios registros. Itera siempre. - Escribir en el mismo prefijo que dispara la función. Bucle infinito. Filtra por prefijo y restringe los permisos de escritura del rol, como defensa doble.
- Asignar 128 MB "para ahorrar". La CPU es proporcional a la memoria: la función tarda tanto que
el coste no baja y la latencia se dispara. Mide con
Max Memory Usedy prueba varios valores. - Poner el tiempo de espera a 900 s por defecto. Un cuelgue se pagará durante 15 minutos.
- Guardar estado en variables globales sin control. El entorno se reutiliza y una lista global crece hasta agotar la memoria.
- Dar
AdministratorAccessal rol de ejecución. Concede solo las acciones y los recursos exactos que la función necesita. - Olvidar
lambda:add-permissionpara el origen del evento. El disparador no funciona y no aparece ningún error visible en ninguna parte. - Abrir una conexión a RDS por invocación sin límite de concurrencia. Un pico de tráfico tumba la base de datos. Conexión en el ámbito global y concurrencia reservada.
- No configurar destino de error ni DLQ en invocaciones asíncronas. Los eventos fallidos se descartan en silencio.
- Devolver ficheros grandes en la respuesta. El límite síncrono es de 6 MB. Guarda en S3 y devuelve una URL prefirmada.
- Consejo: empieza en arm64 salvo que una dependencia lo impida. Es un 20 % de ahorro por cambiar un parámetro.
Ejercicios
Ejercicio 1: elegir la tecnología de cómputo
Para cada carga de MercadoFresco, decide entre Lambda, EC2/contenedor o una combinación, y justifica con los criterios de la lección:
- A) La tienda web PHP, que atiende tráfico de forma continua con un pico los viernes.
- B) Generar el PDF de la factura cuando un pedido se marca como entregado (unos 900 al día, 1,5 s cada uno).
- C) Recomprimir las 40.000 fotos del catálogo, un proceso que hoy tarda 3 horas seguidas.
- D) Un trabajo que cada noche a las 3:00 exporta los pedidos del día a S3; tarda 90 segundos.
- E) Un servicio de recomendación que carga un modelo de 4 GB en memoria y responde en menos de 50 ms.
Ejercicio 2: calcular y optimizar el coste
La función de miniaturas está configurada con 1.024 MB y tarda 1.800 ms. Pruebas con AWS Lambda Power Tuning dan estos resultados:
| Memoria | Duración |
|---|---|
| 512 MB | 3.400 ms |
| 1.024 MB | 1.800 ms |
| 1.769 MB | 1.150 ms |
| 3.008 MB | 1.050 ms |
MercadoFresco crece y ahora se procesan 200.000 fotos al mes (ya fuera del nivel gratuito de duración). Calcula el coste de duración de cada configuración en arm64 (0,0000133334 USD por GB-segundo) y decide cuál elegirías, teniendo en cuenta también la latencia.
Ejercicio 3: escribir el rol de mínimo privilegio
MercadoFresco añade una tercera función, mercadofresco-exportar-pedidos, que cada noche:
- Lee los pedidos del día de la réplica de lectura de RDS.
- Obtiene las credenciales de la base de datos de Secrets Manager.
- Escribe un fichero CSV en
s3://mercadofresco-informes-analitica/informes/AAAA/MM/. - Envía una notificación a un tema de SNS cuando termina.
- Escribe sus registros en CloudWatch Logs.
Escribe la política de permisos del rol de ejecución aplicando el mínimo privilegio, y explica dos permisos que deliberadamente no incluyes aunque podrían parecer necesarios.
Soluciones
Solución 1.
| Caso | Elección | Justificación |
|---|---|---|
| A) Tienda web PHP | EC2 con Auto Scaling (lo que ya montamos en 02-01) | Tráfico continuo, aplicación monolítica con estado en disco, código heredado. La utilización sostenida hace que Lambda salga más cara y el rediseño no compensa |
| B) PDF de factura | Lambda | 900 × 1,5 s = 22,5 minutos de cómputo al día. Es una tarea corta, por eventos y sin estado: el caso canónico. Una EC2 estaría ociosa el 98 % del tiempo |
| C) Recomprimir 40.000 fotos | Lambda, pero repartida | 3 horas seguidas superan el límite de 15 minutos. La solución no es cambiar de tecnología: es repartir en 40.000 invocaciones de ~2 s que corren en paralelo, y el trabajo total pasa de 3 horas a unos minutos. Alternativa válida: una tarea de contenedor si el proceso no se puede paralelizar |
| D) Exportación nocturna de 90 s | Lambda + EventBridge programado | Muy por debajo de los 15 minutos, se ejecuta una vez al día. Mantener un servidor para 90 segundos diarios no tiene sentido. EventBridge se ve en 07-03 |
| E) Recomendador con modelo de 4 GB | Contenedor en ECS/Fargate | El modelo excede el límite de 250 MB del paquete (se podría usar imagen de contenedor de 10 GB), pero el problema real es el arranque en frío: cargar 4 GB tarda segundos y el requisito es de 50 ms. Necesita un proceso siempre encendido con el modelo en memoria. Módulo 10 |
Solución 2.
Coste de duración en arm64, para 200.000 invocaciones:
GB-segundo = invocaciones × (memoria_GB) × (duración_s) Coste = (GB-segundo − 400.000 gratuitos) × 0,0000133334
| Memoria | GB | Duración | GB-segundo | Facturable | Coste |
|---|---|---|---|---|---|
| 512 MB | 0,50 | 3,4 s | 340.000 | 0 (dentro del gratuito) | 0,00 USD |
| 1.024 MB | 1,00 | 1,8 s | 360.000 | 0 (dentro del gratuito) | 0,00 USD |
| 1.769 MB | 1,73 | 1,15 s | 397.900 | 0 (dentro del gratuito) | 0,00 USD |
| 3.008 MB | 2,94 | 1,05 s | 617.400 | 217.400 | 2,90 USD |
Resultado revelador: las tres primeras configuraciones cuestan exactamente lo mismo, cero, porque todas caben en el nivel gratuito de 400.000 GB-segundo. La decisión, por tanto, no es económica sino de latencia y de margen.
Elección: 1.769 MB. Razones:
- Es la más rápida de las tres gratuitas: 1,15 s frente a 3,4 s, casi tres veces mejor.
- 1.769 MB corresponde a un vCPU completo, el punto donde Pillow deja de estar limitado por CPU.
- Se queda en 397.900 GB-segundo, muy justo respecto al límite. Si el volumen crece, empezará a costar: a 300.000 fotos serían 596.850 GB-segundo, es decir 2,62 USD al mes. Sigue siendo irrelevante.
- 3.008 MB se descarta: solo mejora 100 ms sobre 1.769 MB (la función ya no está limitada por CPU) y multiplica el consumo de GB-segundo, saliéndose del nivel gratuito.
La lección general: por encima de un vCPU, seguir subiendo memoria deja de acelerar y empieza a costar. El punto óptimo suele estar cerca de 1.769 MB para trabajos de un solo hilo.
Solución 3.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Registros",
"Effect": "Allow",
"Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": "arn:aws:logs:eu-west-1:111122223333:log-group:/aws/lambda/mercadofresco-exportar-pedidos:*"
},
{
"Sid": "LeerSoloElSecretoDeLaBaseDeDatos",
"Effect": "Allow",
"Action": "secretsmanager:GetSecretValue",
"Resource": "arn:aws:secretsmanager:eu-west-1:111122223333:secret:rds!db-a1b2c3d4-*"
},
{
"Sid": "EscribirSoloLosInformes",
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::mercadofresco-informes-analitica/informes/*"
},
{
"Sid": "PublicarSoloEnElTemaDeAvisos",
"Effect": "Allow",
"Action": "sns:Publish",
"Resource": "arn:aws:sns:eu-west-1:111122223333:mercadofresco-exportaciones"
},
{
"Sid": "InterfazDeRedParaAccederALaVPC",
"Effect": "Allow",
"Action": [
"ec2:CreateNetworkInterface",
"ec2:DescribeNetworkInterfaces",
"ec2:DeleteNetworkInterface"
],
"Resource": "*"
}
]
}Dos permisos que deliberadamente NO se incluyen:
-
s3:GetObjectys3:DeleteObjectsobre el bucket de informes. La función solo escribe: no necesita leer ni borrar nada. Excluirlos significa que, si la función se viera comprometida por una dependencia maliciosa, no podría exfiltrar los informes históricos de ventas ni destruirlos. Es el mismo razonamiento que aplicamos en la función de miniaturas. -
Cualquier permiso de
rds:*. Resulta contraintuitivo, porque la función consulta la base de datos, pero conectarse a PostgreSQL con usuario y contraseña no pasa por IAM: es una conexión TCP autenticada por el propio motor. Los permisosrds:*sirven para administrar instancias (crearlas, borrarlas, modificarlas), y la función no debe poder hacer nada de eso. Concederlos sería dar permiso para borrar la base de datos a un proceso que solo necesita leer una tabla. (La excepción sería la autenticación IAM de RDS, que sí requiererds-db:connectsobre un recurso de usuario concreto.)
Detalles adicionales del diseño: el ARN del secreto lleva un comodín final porque Secrets Manager
añade un sufijo aleatorio de seis caracteres al nombre; y los permisos de ec2:*NetworkInterface
son obligatorios y con Resource: "*" cuando la función se conecta a recursos dentro de una VPC —es
una de las poquísimas excepciones legítimas al mínimo privilegio, impuesta por el propio servicio.
Conclusión
Con esta lección MercadoFresco cierra su primer módulo de servicios reales. Sabes qué es la computación sin servidor y, más importante, cuándo no usarla: has comparado Lambda con EC2 y con contenedores en una tabla de criterios y has aplicado la regla de que por encima del 50-60 % de utilización sostenida el modelo deja de compensar. Dominas el modelo de ejecución —manejador, evento, contexto, respuesta— y el ciclo de vida del entorno, del que se deriva la optimización más rentable que existe en Lambda: crear los clientes de boto3 en el ámbito global, fuera del manejador, porque el entorno se reutiliza. Conoces el arranque en frío, qué lo alarga y las cuatro formas de mitigarlo, y sabes que la concurrencia reservada es a la vez una garantía y un freno, el freno que protege a RDS de la capacidad de Lambda para escalar sin límite.
Has entendido la relación memoria-CPU-coste, que es el parámetro que más gente configura mal, y lo has calculado con números: asignar 128 MB "para ahorrar" suele salir igual de caro y ocho veces más lento. Sabes elegir arm64 por un 20 % de ahorro, poner un tiempo de espera ajustado y no guardar contraseñas en variables de entorno.
Y has escrito la función que cierra el círculo abierto en la lección 02-03:
mercadofresco-generar-miniaturas se dispara con el evento s3:ObjectCreated del bucket
mercadofresco-catalogo-fotos, decodifica correctamente la clave con unquote_plus, itera sobre
todos los registros del evento, escribe en un prefijo distinto del de entrada para no crear un bucle
infinito, distingue los errores recuperables de los que no lo son y etiqueta lo que produce. La has
empaquetado con Pillow en una capa compilada para la plataforma correcta, la has desplegado por
consola y por CLI con create-function y update-function-code --publish, y le has dado un rol
de ejecución de mínimo privilegio que solo puede leer bajo productos/, solo puede escribir bajo
miniaturas/ y no puede borrar nada. Has añadido una segunda función,
mercadofresco-estado-pedido, expuesta por Function URL, con la conexión a RDS reutilizada
entre invocaciones, consultas parametrizadas, concurrencia limitada a 20 y errores que no filtran
información al cliente. Sabes leer la línea REPORT de CloudWatch Logs para ajustar la memoria, has
recorrido los orígenes de eventos habituales señalando dónde se estudia cada uno, conoces los
límites reales —15 minutos, 250 MB, 512 MB en /tmp, 6 MB de payload síncrono— y has hecho el
cálculo de precios que demuestra que servir 9 millones de peticiones cuesta menos de 4 dólares al
mes. Por último, sabes qué pasa cuando una función falla en cada modo de invocación y por qué una
cola de mensajes fallidos sin vigilancia es lo mismo que no tenerla.
Recapitulando el módulo entero: MercadoFresco tiene ya en eu-west-1 un grupo de Auto Scaling de la
tienda repartido en dos zonas de disponibilidad, discos EBS con copias automáticas gestionadas por
Data Lifecycle Manager, el catálogo de fotos en S3 con versionado y reglas de ciclo de vida, la base
de datos de pedidos en RDS PostgreSQL con Multi-AZ, réplica de lectura y restauración a un punto en
el tiempo, y dos funciones Lambda que reaccionan a eventos y responden por HTTP. El problema 2,
las copias de seguridad no fiables, está resuelto de principio a fin. Los otros tres están
encaminados pero incompletos.
Y lo que falta es, precisamente, lo que sostiene todo lo demás. Todo esto vive en la VPC por
defecto: las instancias están expuestas donde no deberían, la base de datos comparte espacio de
red con la tienda, no hay nada que reparta el tráfico entre las cuatro instancias del viernes, no
hay HTTPS ni dominio propio, y el 97 % de la factura de S3 sigue siendo transferencia de salida sin
caché. En el módulo 3, «Redes y entrega de contenido», empezando por la lección 03-01 «Amazon
VPC», construiremos la red propia de MercadoFresco: subredes públicas y privadas en dos zonas de
disponibilidad, tablas de rutas, puertas de enlace y una base de datos que deja de ser alcanzable
desde internet. Sobre esa red montaremos después los grupos de seguridad, el balanceador que
completa la solución al pico de los viernes, CloudFront para abaratar y acelerar las fotos, y Route
53 para que mercadofresco.example apunte por fin a la nube.
Curso de AWS
Módulo 1: Introducción a AWS
- ¿Qué es AWS?
- Configuración de tu cuenta de AWS
- Infraestructura global de AWS
- Consola de administración de AWS
- AWS CLI y SDKs
Módulo 2: Servicios principales de AWS
Módulo 3: Redes y entrega de contenido
- Amazon VPC
- Grupos de seguridad y listas de control de acceso
- Elastic Load Balancing
- Amazon CloudFront
- Route 53
Módulo 4: Seguridad e identidad
- AWS Identity and Access Management (IAM)
- AWS Key Management Service (KMS)
- Secrets Manager y Parameter Store
- AWS Shield
- AWS WAF
Módulo 5: Monitorización y gestión
- Amazon CloudWatch
- AWS X-Ray y trazabilidad distribuida
- AWS CloudTrail
- AWS Config
- AWS Trusted Advisor
Módulo 6: Bases de datos
- Cómo elegir la base de datos adecuada
- Amazon DynamoDB
- Amazon Aurora
- Amazon Redshift
- Amazon ElastiCache
Módulo 7: Integración de aplicaciones
- Amazon SQS
- Amazon SNS
- Amazon EventBridge
- AWS Step Functions
- Patrones de integración: idempotencia, reintentos y colas de mensajes fallidos
