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

  1. Qué es la computación sin servidor y en qué se diferencia de EC2
  2. Modelo de ejecución: manejador, evento, contexto y respuesta
  3. Arranque en frío y arranque en caliente
  4. Concurrencia: reservada, provisionada y el límite de la cuenta
  5. Configuración: memoria, CPU, tiempo de espera, variables y arquitectura
  6. La función de las miniaturas: el código
  7. Empaquetado con dependencias y capas
  8. Desplegar por consola y por CLI
  9. Permisos: el rol de ejecución y el mínimo privilegio
  10. Registros en CloudWatch Logs y depuración
  11. Orígenes de eventos habituales
  12. Segunda función: consultar el estado de un pedido por HTTP
  13. Límites reales que hay que conocer
  14. Precios, con el cálculo concreto de MercadoFresco
  15. Errores, reintentos y colas de mensajes fallidos
  16. 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 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:

  1. ¿La tarea dura menos de 15 minutos? Si no, Lambda queda descartada.
  2. ¿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.
  3. ¿Necesitas control del sistema operativo o estado en disco? Entonces EC2 o contenedores.
  4. ¿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 memoria

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

  1. Mover trabajo al código global y mantener el paquete pequeño. Gratis.
  2. Elegir un lenguaje ligero. Python o Node para funciones de latencia crítica.
  3. Subir la memoria. Más CPU acelera la inicialización, y a menudo el coste total no sube porque la ejecución dura menos.
  4. 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-1

Sin 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-1

Se 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_plus sobre la clave. S3 codifica los nombres en el evento. Sin esta línea, cualquier foto con espacios, tildes o eñes daría NoSuchKey. Es el fallo número uno de las Lambdas de S3.
  • event["Records"] es una lista. Un solo evento puede traer varios registros. Procesarlo con event["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.
  • raise en los errores recuperables. Lambda reintenta automáticamente las invocaciones asíncronas; tragarse la excepción con un return haría que el fallo pasara desapercibido.
  • NoSuchKey se 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.py

Desplegar por consola y por CLI

Por consola

  1. Consola → Lambda → región IrlandaCrear función.
  2. Crear desde cero. Nombre: mercadofresco-generar-miniaturas. Tiempo de ejecución: Python 3.12. Arquitectura: arm64.
  3. Cambiar el rol de ejecución → crear uno nuevo (lo ajustaremos en el apartado siguiente).
  4. Pega el código en el editor o sube el ZIP.
  5. Configuración → Configuración general: memoria 1.024 MB, tiempo de espera 30 s.
  6. Configuración → Variables de entorno: las cuatro que definimos antes.
  7. Capas → Añadir una capamercadofresco-imagen.
  8. Desencadenadores → Añadir desencadenador → S3 → bucket mercadofresco-catalogo-fotos, evento Todos los eventos de creación de objetos, prefijo productos/, sufijo .jpg.
  9. 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-1

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

  1. s3:GetObject solo bajo productos/. Si la función se ve comprometida, no puede leer los informes de ventas ni las copias de la base de datos.
  2. s3:PutObject solo bajo miniaturas/. 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.
  3. Sin s3:DeleteObject. La función no necesita borrar nada, así que no puede.
  4. 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-1

Al 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 ms

Có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 sobre Errors > 0 es 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
Autenticación Ninguna o IAM IAM, Cognito, JWT, autorizadores propios
Limitación de tasa No
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-1

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

  1. Peticiones: 0,20 USD por millón.
  2. 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/mes

Coste 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/mes

Caso 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/mes

Menos 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-1

Tres principios que hay que respetar desde el primer día:

  1. 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.
  2. Distinguir errores recuperables de los que no lo son. Un tiempo de espera de red merece reintento; un NoSuchKey no. Reintentar lo irrecuperable es tiempo y dinero tirados.
  3. Vigilar la DLQ. Una cola de fallidos que nadie mira es lo mismo que no tenerla. Una alarma sobre ApproximateNumberOfMessagesVisible > 0 es 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á con NoSuchKey. 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 Used y 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 AdministratorAccess al rol de ejecución. Concede solo las acciones y los recursos exactos que la función necesita.
  • Olvidar lambda:add-permission para 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:

  1. Lee los pedidos del día de la réplica de lectura de RDS.
  2. Obtiene las credenciales de la base de datos de Secrets Manager.
  3. Escribe un fichero CSV en s3://mercadofresco-informes-analitica/informes/AAAA/MM/.
  4. Envía una notificación a un tema de SNS cuando termina.
  5. 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:

  1. s3:GetObject y s3:DeleteObject sobre 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.

  2. 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 permisos rds:* 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í requiere rds-db:connect sobre 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

Módulo 2: Servicios principales de AWS

Módulo 3: Redes y entrega de contenido

Módulo 4: Seguridad e identidad

Módulo 5: Monitorización y gestión

Módulo 6: Bases de datos

Módulo 7: Integración de aplicaciones

Módulo 8: Herramientas para desarrolladores

Módulo 9: Infraestructura como código y gobierno de cuentas

Módulo 10: Contenedores en AWS

Módulo 11: Mejores prácticas y gestión de costos

© Copyright 2026. Todos los derechos reservados