Tanquem el mòdul amb la peça que falta. Fins ara, tot el que ha muntat MercadoFresco parteix de la mateixa idea: hi ha un servidor encès esperant feina. La instància EC2 de la botiga està encesa encara que sigui dimarts a les 4 de la matinada. La instància RDS també. I es paguen les 730 hores del mes.

Però hi ha tasques que no encaixen en aquest model. Generar la miniatura d'una foto quan el Luis la puja passa vint vegades al dia i dura dos segons. Consultar l'estat d'una comanda és una crida puntual. Processar un fitxer de rutes de repartiment passa un cop cada matí. Tenir un servidor encès 730 hores per a 40 minuts de feina mensual és absurd.

AWS Lambda executa codi només quan passa alguna cosa i només cobra mentre s'executa, amb precisió de mil·lisegons. En aquesta lliçó connectem per fi l'esdeveniment d'S3 que vam deixar configurat a 02-03 i escrivim la funció que genera les miniatures del catàleg de MercadoFresco.

Contingut

  1. Què és la computació sense servidor i en què es diferencia d'EC2
  2. Model d'execució: gestor, esdeveniment, context i resposta
  3. Arrencada en fred i arrencada en calent
  4. Concurrència: reservada, provisionada i el límit del compte
  5. Configuració: memòria, CPU, temps d'espera, variables i arquitectura
  6. La funció de les miniatures: el codi
  7. Empaquetatge amb dependències i capes
  8. Desplegar per consola i per CLI
  9. Permisos: el rol d'execució i el mínim privilegi
  10. Registres a CloudWatch Logs i depuració
  11. Orígens d'esdeveniments habituals
  12. Segona funció: consultar l'estat d'una comanda per HTTP
  13. Límits reals que cal conèixer
  14. Preus, amb el càlcul concret de MercadoFresco
  15. Errors, reintents i cues de missatges fallits
  16. Recapitulació del mòdul: què hi ha ja a AWS i què falta

Què és la computació sense servidor i en què es diferencia d'EC2

«Sense servidor» (serverless) no vol dir que no hi hagi servidors: vol dir que no són teus, no els veus i no els pagues quan no treballen. AWS aprovisiona la capacitat, l'escala i la retira, tot plegat invisible.

EC2 Contenidors (ECS/Fargate) AWS Lambda
Unitat de desplegament Màquina virtual completa Imatge de contenidor Funció
Qui gestiona el SO Tu AWS (amb Fargate) AWS
Temps d'arrencada 30-60 s 10-30 s Mil·lisegons a 1-2 s
Es factura Per segon encesa Per segon de tasca Per ms d'execució
Cost en repòs Tot el preu Tot el preu Zero
Durada màxima Il·limitada Il·limitada 15 minuts
Escalat Auto Scaling (minuts) Servei ECS (segons) Automàtic i instantani
Estat en disc Persistent (EBS) Efímer Efímer (512 MB a /tmp)
Control de l'entorn Total Alt Limitat
Cas típic Aplicacions tradicionals, bases de dades Microserveis, càrregues sostingudes Esdeveniments, tasques curtes, trànsit irregular

Els criteris de tria, en ordre d'utilitat pràctica:

  1. La tasca dura menys de 15 minuts? Si no, Lambda queda descartada.
  2. El trànsit és irregular o per esdeveniments? Lambda guanya de llarg. Si és sostingut i constant 24×7, un contenidor o una EC2 surten més barats.
  3. Necessites control del sistema operatiu o estat en disc? Llavors EC2 o contenidors.
  4. Quant temps del teu equip vols dedicar a infraestructura? Lambda és el mínim possible.

Per a MercadoFresco, la generació de miniatures compleix els quatre criteris de manera perfecta: segons de durada, disparada per esdeveniments esporàdics, sense estat, i sense res a administrar.

Model d'execució: gestor, esdeveniment, context i resposta

Una funció Lambda és una funció normal del teu llenguatge amb una signatura concreta:

def lambda_handler(event, context):
    # event   -> les dades del que ha passat (dict)
    # context -> informació sobre aquesta execució concreta
    return {"resultat": "ok"}
Element Què és
Gestor (handler) El punt d'entrada. Es declara com fitxer.funcio, p. ex. app.lambda_handler
Esdeveniment (event) Diccionari amb les dades del disparador. La seva forma depèn de l'origen: S3, API Gateway i SQS tenen estructures molt diferents
Context (context) Metadades de l'execució: aws_request_id, function_name, memory_limit_in_mb, get_remaining_time_in_millis()
Resposta El que retorna la funció. Amb una invocació síncrona arriba a qui va cridar; amb una d'asíncrona es descarta

El cicle de vida complet, que cal entendre bé perquè d'ell depenen el rendiment i el cost:

sequenceDiagram
    participant E as Esdeveniment (S3)
    participant L as Servei Lambda
    participant M as Entorn d'execucio
    participant F as Codi de la funcio

    E->>L: Passa alguna cosa (es puja una foto)
    L->>L: Busca un entorn disponible
    alt Arrencada en FRED (no hi ha entorn)
        L->>M: Crea microVM Firecracker
        M->>M: Descarrega el codi i les capes
        M->>F: Executa el codi GLOBAL (imports, clients boto3)
        Note over M,F: 100 ms - 2 s. ES FACTURA (des del 2024)
    end
    L->>F: Invoca lambda_handler(event, context)
    F-->>L: Retorna la resposta
    Note over M: L'entorn es CONGELA, no es destrueix
    E->>L: Arriba un altre esdeveniment en pocs minuts
    L->>F: Reutilitza l'entorn: arrencada en CALENT
    Note over M: Despres de ~5-15 min sense us, es destrueix

D'aquí surt l'optimització més rendible de Lambda, i és una línia de codi:

import boto3

# CORRECTE: fora del gestor. S'executa UNA VEGADA per entorn i es reutilitza
# en totes les invocacions següents. Crear un client boto3 costa 100-300 ms.
s3 = boto3.client("s3")

def lambda_handler(event, context):
    # INCORRECTE seria crear el client aqui: es pagarien aquests 200 ms
    # a CADA invocacio.
    ...

I d'aquí surt també el parany corresponent: l'entorn es reutilitza, així que les variables globals persisteixen entre invocacions. És útil per a memòries cau, però desastrós si acumules estat per descuit:

resultats = []   # PERILL: sobreviu entre invocacions

def lambda_handler(event, context):
    resultats.append(event)   # creix sense control fins a esgotar la memòria

Arrencada en fred i arrencada en calent

L'arrencada en fred és el temps que triga AWS a preparar un entorn nou: crear la microVM, descarregar el codi i les capes, inicialitzar l'intèrpret i executar el codi global.

Factor Efecte sobre l'arrencada en fred
Llenguatge Python i Node.js: 100-400 ms. Java i .NET: 1-4 s. Go i Rust: < 100 ms
Mida del paquet Com més gran, més triga la descàrrega
Codi d'inicialització Carregar un model gran o connectar a una base de dades allarga l'arrencada
VPC Abans hi afegia 8-10 s; avui la penalització és de desenes de ms
Memòria assignada Més memòria = més CPU = inicialització més ràpida

Quan importa de debò: si la Lambda respon a una petició síncrona d'un usuari (una API), 1,5 segons extra són inacceptables. Si processa un esdeveniment asíncron (la miniatura d'una foto), a ningú no li importa.

Estratègies de mitigació, de menor a major cost:

  1. Moure feina al codi global i mantenir el paquet petit. Gratis.
  2. Triar un llenguatge lleuger. Python o Node per a funcions de latència crítica.
  3. Pujar la memòria. Més CPU accelera la inicialització, i sovint el cost total no puja perquè l'execució dura menys.
  4. Concurrència provisionada. AWS manté N entorns sempre llestos. Elimina l'arrencada en fred del tot, però es paga encara que no s'utilitzi.

Concurrència: reservada, provisionada i el límit del compte

Lambda no encua peticions: crea entorns nous. Si arriben 100 esdeveniments simultanis, s'executen 100 instàncies de la funció alhora. Aquest és el seu gran avantatge i també el seu risc.

Concepte Què és Es paga
Concurrència sota demanda Entorns creats a mesura que arriben els esdeveniments Només l'execució
Límit del compte Sostre per regió, 1.000 per defecte (ampliable)
Concurrència reservada Porció del límit apartada per a una funció Res extra, però resta del total disponible
Concurrència provisionada Entorns preescalfats sempre llestos Sí, encara que estiguin ociosos

La concurrència reservada té un doble efecte que convé entendre:

  • Garanteix que aquella funció sempre podrà fer servir aquella quantitat, encara que d'altres estiguin saturant el compte.
  • Limita aquella funció a aquest màxim. És un fre.

Aquest fre és exactament el que MercadoFresco necessita per protegir la base de dades:

# La funcio que consulta comandes MAI obrira mes de 20 connexions a RDS,
# encara que arribin 5.000 peticions simultanies. Protegeix la base de dades
# de ser el baula que es trenca.
aws lambda put-function-concurrency \
  --function-name mercadofresco-estado-pedido \
  --reserved-concurrent-executions 20 \
  --profile mercadofresco-dev --region eu-west-1

Sense aquest límit, un pic de trànsit podria obrir centenars de connexions a mercadofresco-pedidos i tombar la base de dades. És un cas real i freqüent: Lambda escala, RDS no.

Configuració: memòria, CPU, temps d'espera, variables i arquitectura

Memòria i CPU van juntes

Aquest és el paràmetre que més gent configura malament. A Lambda només s'assigna memòria, de 128 MB a 10.240 MB, i la CPU s'assigna proporcionalment:

Memòria vCPU aproximada Comentari
128 MB 0,08 Mínim. Molt lent per a qualsevol càlcul
512 MB 0,29
1.769 MB 1,00 Un nucli complet. Punt de referència clau
3.008 MB 1,70
10.240 MB 6,00 Màxim, amb multifil real

La conseqüència és contraintuïtiva i mereix un càlcul. Una funció que processa una imatge:

Memòria Durada Cost per 1.000 invocacions
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 memòria pot sortir igual de barat o més barat, i vuit vegades més ràpid. Posar 128 MB «per estalviar» acostuma a ser un error: la funció triga tant que el cost no baixa i la latència es dispara. L'eina AWS Lambda Power Tuning automatitza aquesta anàlisi.

Temps d'espera

D'1 segon a 15 minuts (900 s), amb 3 segons per defecte. Regla: posa'l una mica per damunt del que la funció triga realment, mai al màxim. Un temps d'espera de 900 s en una funció que hauria de trigar 2 s vol dir que una penjada es pagarà durant 15 minuts.

Variables d'entorn

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

Es xifren en repòs automàticament, però són visibles a la consola per a qualsevol amb permís de lectura. No hi posis mai contrasenyes: fes servir Secrets Manager o Parameter Store (lliçó 04-03), com vam fer amb RDS a 02-04.

Arquitectura: x86_64 davant d'arm64 (Graviton)

x86_64 arm64 (Graviton2)
Preu Referència ~20 % més barat
Rendiment Referència Igual o millor en la majoria de càrregues
Compatibilitat Universal Les dependències binàries s'han de compilar per a ARM

Tria arm64 llevat que una dependència t'ho impedeixi. És un 20 % d'estalvi per canviar un paràmetre. L'única precaució és que les biblioteques amb codi natiu —com Pillow, que farem servir ara— s'han d'empaquetar per a l'arquitectura correcta.

La funció de les miniatures: el codi

Arribem a l'objectiu. A la lliçó 02-03 vam configurar la notificació del bucket mercadofresco-catalogo-fotos perquè cada .jpg pujat sota productos/ disparés la funció mercadofresco-generar-miniaturas. L'escriurem.

"""
mercadofresco-generar-miniaturas

Genera una miniatura de 200 px d'amplada cada vegada que es puja una foto de
producte al bucket mercadofresco-catalogo-fotos sota el prefix productos/.

Disparador: esdeveniment s3:ObjectCreated:* filtrat per prefix 'productos/'
            i sufix '.jpg' (configurat a la lliçó 02-03).
Sortida:    objecte al prefix 'miniaturas/' del mateix bucket.

IMPORTANT: el prefix de sortida és DIFERENT del d'entrada. Si escrivíssim
la miniatura a 'productos/' es generaria un esdeveniment nou que tornaria a
invocar aquesta funció: un bucle infinit i molt car.
"""

import io
import logging
import os
import urllib.parse

import boto3
from botocore.exceptions import ClientError
from PIL import Image

# --- Codi GLOBAL: s'executa una sola vegada per entorn d'execució ---
# Crear el client aquí estalvia 100-300 ms a cada invocació posterior.
s3 = boto3.client("s3")

logger = logging.getLogger()
logger.setLevel(os.environ.get("NIVEL_LOG", "INFO"))

PREFIX_MINIATURES = os.environ.get("PREFIJO_MINIATURAS", "miniaturas/")
AMPLADA = int(os.environ.get("ANCHO_MINIATURA", "200"))
QUALITAT_JPEG = 82


def _clau_miniatura(clau_origen: str) -> str:
    """Converteix 'productos/frutas/naranjas.jpg' en 'miniaturas/frutas/naranjas.jpg'.

    Es conserva l'estructura de subprefixos per poder localitzar la miniatura
    a partir de l'original sense haver de consultar cap base de dades.
    """
    sense_prefix = clau_origen.split("/", 1)[1] if "/" in clau_origen else clau_origen
    return f"{PREFIX_MINIATURES}{sense_prefix}"


def _generar(dades: bytes) -> bytes:
    """Redimensiona mantenint la proporció i retorna els bytes del JPEG."""
    with Image.open(io.BytesIO(dades)) as img:
        # Les fotos de mòbil porten orientació a les metadades EXIF; sense això
        # algunes miniatures sortirien girades 90 graus.
        img = img.convert("RGB")

        alt = int(img.height * (AMPLADA / img.width))
        # LANCZOS dóna la millor qualitat en reduir. thumbnail() no amplia mai,
        # així que una foto ja petita es deixa tal com està.
        img.thumbnail((AMPLADA, alt), Image.Resampling.LANCZOS)

        sortida = io.BytesIO()
        img.save(sortida, format="JPEG", quality=QUALITAT_JPEG, optimize=True)
        sortida.seek(0)
        return sortida.read()


def lambda_handler(event, context):
    """Punt d'entrada. Un esdeveniment d'S3 pot portar DIVERSOS registres."""
    processades, fallides = 0, 0

    for registre in event.get("Records", []):
        bucket = registre["s3"]["bucket"]["name"]
        # Les claus arriben codificades com a URL: 'a+b.jpg' o '%C3%B1'.
        # Sense unquote_plus, una foto amb espais o enyes donaria NoSuchKey.
        clau = urllib.parse.unquote_plus(registre["s3"]["object"]["key"])

        # Defensa en profunditat: encara que el filtre del bucket ja ho impedeixi,
        # comprovem que no estem processant una miniatura.
        if clau.startswith(PREFIX_MINIATURES):
            logger.warning("S'ignora %s: ja és una miniatura", clau)
            continue

        try:
            logger.info("Processant s3://%s/%s", bucket, clau)
            original = s3.get_object(Bucket=bucket, Key=clau)["Body"].read()

            miniatura = _generar(original)
            clau_desti = _clau_miniatura(clau)

            s3.put_object(
                Bucket=bucket,
                Key=clau_desti,
                Body=miniatura,
                ContentType="image/jpeg",
                CacheControl="public, max-age=604800",   # una setmana de memòria cau
                Metadata={"origen": clau, "generado-por": context.function_name},
                Tagging="Proyecto=mercadofresco&Componente=catalogo&Propietario=luis",
            )

            logger.info(
                "Miniatura creada: s3://%s/%s (%d KB -> %d KB)",
                bucket, clau_desti, len(original) // 1024, len(miniatura) // 1024,
            )
            processades += 1

        except ClientError as e:
            codi = e.response["Error"]["Code"]
            if codi == "NoSuchKey":
                # L'objecte s'ha esborrat entre l'esdeveniment i aquesta execució.
                # No és un error recuperable: reintentar no serviria de res.
                logger.warning("L'objecte %s ja no existeix, s'omet", clau)
                continue
            logger.error("Error d'S3 amb %s: %s", clau, e)
            fallides += 1
            raise          # rellançar activa el reintent automàtic de Lambda

        except Exception as e:
            logger.exception("Error inesperat amb %s: %s", clau, e)
            fallides += 1
            raise

    return {"processades": processades, "fallides": fallides}

Els detalls del codi que marquen la diferència entre un exemple de tutorial i una cosa que aguanta en producció:

  • unquote_plus sobre la clau. S3 codifica els noms a l'esdeveniment. Sense aquesta línia, qualsevol foto amb espais, accents o enyes donaria NoSuchKey. És l'errada número u de les Lambdes d'S3.
  • event["Records"] és una llista. Un sol esdeveniment pot portar diversos registres. Processar-lo amb event["Records"][0] funciona a les proves i falla en producció.
  • Prefix de sortida diferent del d'entrada, més la comprovació explícita. Doble protecció contra el bucle recursiu.
  • raise als errors recuperables. Lambda reintenta automàticament les invocacions asíncrones; empassar-se l'excepció amb un return faria que l'errada passés desapercebuda.
  • NoSuchKey es tracta a part. És un error no recuperable: reintentar-lo tres vegades és temps i diners perduts.
  • Etiquetatge de l'objecte amb l'esquema del projecte, coherent amb el que anem fent.

Empaquetatge amb dependències i capes

Pillow no ve inclòs a l'entorn de Lambda: cal aportar-lo. Hi ha tres maneres i convé saber quan fer servir cadascuna.

Mètode Quan Límit
Codi en línia Funció trivial sense dependències externes S'edita a la consola
Fitxer ZIP El més habitual: codi + dependències 50 MB comprimit, 250 MB descomprimit
Capa (layer) Dependències compartides per diverses funcions 5 capes per funció, 250 MB en total
Imatge de contenidor Dependències enormes (ML, ffmpeg) 10 GB

Per a MercadoFresco, una capa amb Pillow és la tria correcta: la compartiran la funció de miniatures i les que vinguin després, i manté el paquet de codi petit (arrencades en fred més ràpides i edició possible a la consola).

# --- Construir la capa amb Pillow per a arm64 ---
mkdir -p capa-pillow/python

# Es IMPRESCINDIBLE compilar per a la plataforma de desti: Pillow porta codi
# natiu, i una roda de macOS o de x86 fallaria amb "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 per processar fotos de producte (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"

L'estructura de directoris de la capa no és negociable: Python busca les biblioteques a /opt/python, i AWS descomprimeix la capa a /opt. Per això tot ha de penjar d'un directori anomenat exactament python/. Cada llenguatge té la seva ruta (nodejs/node_modules per a Node.js).

# --- Empaquetar el codi de la funcio (sense dependencies: van a la capa) ---
zip funcion-miniaturas.zip lambda_function.py

Desplegar per consola i per CLI

Per consola

  1. Consola → Lambda → regió IrlandaCrear funció.
  2. Crear des de zero. Nom: mercadofresco-generar-miniaturas. Temps d'execució: Python 3.12. Arquitectura: arm64.
  3. Canviar el rol d'execució → crear-ne un de nou (l'ajustarem a l'apartat següent).
  4. Enganxa el codi a l'editor o puja el ZIP.
  5. Configuració → Configuració general: memòria 1.024 MB, temps d'espera 30 s.
  6. Configuració → Variables d'entorn: les quatre que hem definit abans.
  7. Capes → Afegir una capamercadofresco-imagen.
  8. Disparadors → Afegir disparador → S3 → bucket mercadofresco-catalogo-fotos, esdeveniment Tots els esdeveniments de creació d'objectes, prefix productos/, sufix .jpg.
  9. Etiquetes: l'esquema obligatori del projecte.

Per 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

# Permetre que S3 invoqui la funcio. Sense aixo, l'esdeveniment es dispara
# i no passa res, sense cap missatge d'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

Actualitzar el codi després d'un canvi:

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ó immutable numerada. Combinada amb àlies (produccion, pruebas), permet desplegaments graduals dirigint, per exemple, el 10 % del trànsit a la versió nova. És una altra peça contra el problema 4 de MercadoFresco, els desplegaments arriscats, que s'aborda de ple al mòdul 8.

Provar sense pujar res 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 retorna els últims 4 KB del registre codificats en base64: la manera més ràpida de depurar sense sortir del terminal.

Avís de cost. El nivell gratuït de Lambda és permanent, no de 12 mesos: 1 milió de peticions i 400.000 GB-segon al mes, per sempre. Aquesta pràctica no et costarà res. Tot i així, esborra la funció en acabar si no hi continues: aws lambda delete-function --function-name mercadofresco-generar-miniaturas. I recorda eliminar també la notificació del bucket i la capa si no les fas servir.

Permisos: el rol d'execució i el mínim privilegi

Una funció Lambda no té credencials pròpies: assumeix un rol d'execució d'IAM, i els permisos d'aquest rol són exactament el que la funció pot fer. Ni més, ni menys.

La política de confiança, que defineix qui pot assumir el rol:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"Service": "lambda.amazonaws.com"},
    "Action": "sts:AssumeRole"
  }]
}

I la política de permisos, que defineix què pot fer. Aquí hi ha el principi de mínim privilegi aplicat amb 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/*"
    }
  ]
}

Quatre decisions deliberades, i cadascuna evita un problema real:

  1. s3:GetObject només sota productos/. Si la funció es veu compromesa, no pot llegir els informes de vendes ni les còpies de la base de dades.
  2. s3:PutObject només sota miniaturas/. Encara que hi hagués una errada lògica, la funció no pot sobreescriure les fotos originals. És l'última defensa contra el bucle recursiu: fins i tot si el filtre del bucket fallés, l'escriptor no té permís sobre el prefix d'entrada.
  3. Sense s3:DeleteObject. La funció no necessita esborrar res, així que no pot.
  4. Els registres acotats al seu propi grup de registres, no a *.

Comparat amb la pràctica habitual d'adjuntar AmazonS3FullAccess «perquè funcioni», la diferència entre un incident menor i una filtració completa és en aquestes tres instruccions. La lògica completa d'IAM, els rols, les polítiques i la seva avaluació és la lliçó 04-01.

Registres a CloudWatch Logs i depuració

Cada funció escriu automàticament en un grup de registres anomenat /aws/lambda/<nom-de-la-funció>. Tot el que imprimeixis amb print() o logger.info() acaba allà.

# Seguir els registres en directe mentre proves
aws logs tail /aws/lambda/mercadofresco-generar-miniaturas --follow \
  --profile mercadofresco-dev --region eu-west-1

# Buscar nomes els errors de l'ultima 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ó, Lambda escriu una línia REPORT que és or pur per ajustar la configuració:

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

Com es llegeix:

Camp Què diu Què fer-ne
Duration Temps real d'execució Si creix, alguna cosa s'ha degradat
Billed Duration El que es factura (arrodonit al ms) Base del càlcul de cost
Memory Size El que has assignat 1.024 MB
Max Memory Used El que realment ha fet servir 187 MB d'1.024: sobra memòria
Init Duration Temps de l'arrencada en fred Només apareix en arrencades fredes

En aquest cas la temptació és baixar la memòria a 256 MB. Compte: recorda que la CPU és proporcional. Amb 256 MB la funció trigaria uns 7 segons en lloc d'1,8, i el cost seria pràcticament el mateix amb quatre vegades més latència. La decisió correcta és provar 512 MB i mesurar.

Altres dues capacitats bàsiques de depuració:

  • Mètriques automàtiques a CloudWatch: Invocations, Errors, Duration, Throttles, ConcurrentExecutions. Una alarma sobre Errors > 0 és el mínim exigible en producció.
  • Registres estructurats en JSON: configurant el format de registre en JSON, els camps es poden consultar amb CloudWatch Logs Insights sense analitzar text.

El monitoratge a fons —taulers, alarmes, retenció, Logs Insights— és la lliçó 05-01, i el traçat distribuït d'una petició que travessa diverses funcions i serveis és 05-02.

Orígens d'esdeveniments habituals

Lambda s'integra amb més de 200 serveis. Aquests són els que importen en començar:

Origen Invocació Estructura de l'esdeveniment Cas a MercadoFresco Es veu a
Amazon S3 Asíncrona Records[].s3 Generar miniatures 02-03 i aquesta lliçó
API Gateway / Function URL Síncrona requestContext, body Punt d'enllaç d'estat de comanda Aquesta lliçó
Amazon SQS Sondeig per lots Records[].body Processar comandes del pic dels divendres Mòdul 7 (07-01)
Amazon SNS Asíncrona Records[].Sns Avisar d'una incidència de repartiment Mòdul 7 (07-02)
Amazon EventBridge Asíncrona detail, source Tasca programada cada matí Mòdul 7 (07-03)
DynamoDB Streams Sondeig per lots Records[].dynamodb Reaccionar a canvis de dades Mòdul 6 (06-02)
CloudWatch Logs Asíncrona Dades comprimides Alertar davant de patrons d'error Mòdul 5 (05-01)

Les tres formes d'invocació tenen conseqüències molt diferents en el comportament davant d'errors:

Mode Qui espera resposta Reintents automàtics Exemple
Síncrona L'invocador espera Cap (els gestiona el client) API Gateway
Asíncrona Ningú no espera 2 reintents S3, SNS
Sondeig (poll) Lambda consulta la font Fins que caduqui o vagi a la cua de fallits SQS, DynamoDB Streams

Segona funció: consultar l'estat d'una comanda per HTTP

La primera funció reaccionava a un esdeveniment intern. Ara exposarem una funció a internet perquè l'aplicació mòbil de MercadoFresco consulti l'estat d'una comanda.

Hi ha dues maneres de posar HTTP davant d'una Lambda:

Function URL API Gateway
Configuració Una casella Un servei a part que cal configurar
Cost Gratis ~1 USD per milió de peticions
Domini propi No
Autenticació Cap o IAM IAM, Cognito, JWT, autoritzadors propis
Limitació de taxa No
Diverses rutes No, una sola URL Sí, encaminament complet
Quan Prototips, webhooks interns Producció

Comencem amb Function URL per simplicitat, sabent que producció demanarà API Gateway.

"""
mercadofresco-estado-pedido

Retorna l'estat d'una comanda a partir del seu identificador.
Invocació: Function URL (HTTP GET), p. ex. /?pedido=48213

Concurrència reservada = 20: la base de dades RDS no pot suportar
centenars de connexions simultànies, així que es posa el fre aquí.
"""

import json
import logging
import os

import boto3
import psycopg

logger = logging.getLogger()
logger.setLevel("INFO")

# --- Codi global: s'executa una vegada per entorn ---
# El secret es llegeix UNA VEGADA i es reutilitza a les invocacions següents.
_secrets = boto3.client("secretsmanager")
_credencials = json.loads(
    _secrets.get_secret_value(SecretId=os.environ["MF_SECRETO_BD"])["SecretString"]
)

DSN = (
    f"host={os.environ['MF_BD_HOST']} port=5432 dbname=pedidos "
    f"user={_credencials['username']} password={_credencials['password']} "
    f"sslmode=require connect_timeout=3"
)

# Una connexió per entorn d'execució, reutilitzada entre invocacions.
# És la raó per la qual la concurrència reservada limita les connexions a RDS.
_connexio = None


def _obtenir_connexio():
    global _connexio
    if _connexio is None or _connexio.closed:
        _connexio = psycopg.connect(DSN)
    return _connexio


def _resposta(codi: int, cos: dict) -> dict:
    """Format de resposta que espera una Function URL o API Gateway."""
    return {
        "statusCode": codi,
        "headers": {
            "Content-Type": "application/json; charset=utf-8",
            "Cache-Control": "no-store",
        },
        "body": json.dumps(cos, ensure_ascii=False),
    }


def lambda_handler(event, context):
    # Els paràmetres de consulta arriben a queryStringParameters (pot ser None).
    parametres = event.get("queryStringParameters") or {}
    comanda_id = parametres.get("pedido")

    if not comanda_id or not comanda_id.isdigit():
        return _resposta(400, {"error": "Falta el paràmetre 'pedido' o no és numèric"})

    try:
        con = _obtenir_connexio()
        with con.cursor() as cur:
            # Consulta parametritzada: MAI concatenar l'identificador al SQL.
            cur.execute(
                """
                SELECT id, estado, creado_en, entrega_estimada, ciudad
                FROM pedidos
                WHERE id = %s
                """,
                (int(comanda_id),),
            )
            fila = cur.fetchone()

        if fila is None:
            return _resposta(404, {"error": "Comanda no trobada"})

        return _resposta(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 connexió pot haver mort per una commutació de RDS (lliçó 02-04).
        # Es descarta perquè la invocació següent en creï una de nova.
        logger.error("Error de connexió a la base de dades: %s", e)
        global _connexio
        _connexio = None
        return _resposta(503, {"error": "Servei temporalment no disponible"})

    except Exception:
        logger.exception("Error inesperat consultant la comanda %s", comanda_id)
        # No retornar mai el detall de l'excepció al client: filtra informació.
        return _resposta(500, {"error": "Error intern"})
# Crear la Function URL. AuthType NONE la deixa publica: nomes per a proves.
# En produccio es posa API Gateway amb autenticacio al davant.
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 per protegir RDS
aws lambda put-function-concurrency \
  --function-name mercadofresco-estado-pedido \
  --reserved-concurrent-executions 20 \
  --profile mercadofresco-dev --region eu-west-1

Quatre decisions de disseny que mereixen atenció:

  • La connexió a la base de dades viu a l'àmbit global i es reutilitza. Obrir una connexió a PostgreSQL costa desenes de mil·lisegons; fer-ho a cada invocació multiplicaria la latència i saturaria RDS.
  • Concurrència reservada de 20 com a sostre de connexions. Sense això, un pic podria obrir centenars de connexions i tombar mercadofresco-pedidos. És la lliçó de 02-04 aplicada: Lambda escala, RDS no. (Per a casos exigents existeix RDS Proxy, que agrupa connexions; queda fora de l'abast d'aquesta lliçó.)
  • Consulta parametritzada amb %s, mai concatenació de cadenes. Injecció SQL evitada.
  • Els errors interns no es detallen al client: es registren a CloudWatch i es retorna un missatge genèric.

Límits reals que cal conèixer

Límit Valor Conseqüència pràctica
Durada màxima 15 minuts Un procés més llarg necessita Step Functions (07-04), ECS o EC2
Memòria 128 MB a 10.240 MB La CPU va lligada a aquest valor
Paquet ZIP comprimit 50 MB (pujada directa) Per damunt, pujar des d'S3
Paquet descomprimit + capes 250 MB El límit que més molesta. Alternativa: imatge de contenidor (10 GB)
Espai a /tmp 512 MB per defecte, fins a 10.240 MB Suficient per a les fotos; configurable amb --ephemeral-storage
Payload síncron 6 MB d'entrada i de sortida No retornis fitxers grans: desa'ls a S3 i retorna una URL prefirmada (02-03)
Payload asíncron 256 KB L'esdeveniment d'S3 hi cap de sobres
Concurrència per regió 1.000 per defecte Ampliable amb una sol·licitud de quota
Variables d'entorn 4 KB en total No hi posis fitxers de configuració
Capes per funció 5 Agrupa dependències relacionades

El límit dels 15 minuts és el que més decisions d'arquitectura condiciona. Si el procés nocturn que recomprimeix les 40.000 fotos del catàleg triga 3 hores, no és una feina per a una sola Lambda: o es reparteix en 40.000 invocacions de dos segons (que sí que encaixen perfectament), o s'executa en una tasca de contenidor. Repartir és gairebé sempre la resposta correcta amb Lambda.

Preus, amb el càlcul concret de MercadoFresco

Lambda cobra dos conceptes:

  1. Peticions: 0,20 USD per milió.
  2. Durada: 0,0000166667 USD per GB-segon (x86_64; arm64 és un 20 % menys).

I el nivell gratuït és permanent, no de 12 mesos: 1 milió de peticions i 400.000 GB-segon al mes, sempre.

Cas 1: la funció de miniatures. El Luis puja unes 20 fotos al dia (600 al mes), la funció fa servir 1.024 MB i triga 1,8 segons:

Peticions:  600 → dins del milió gratuït                = 0,00 USD
Durada:     600 × 1,8 s × 1 GB = 1.080 GB-segon
            → dins dels 400.000 gratuïts                = 0,00 USD
--------------------------------------------------------------
Total                                                   = 0,00 USD/mes

Cost zero. L'equivalent en una EC2 t3.micro encesa per fer el mateix serien uns 7,50 USD al mes. Aquest és exactament el cas d'ús per al qual existeix Lambda.

Cas 2: el punt d'enllaç d'estat de comandes, en l'escenari realista. MercadoFresco rep unes 900.000 consultes al mes, la funció fa servir 512 MB i triga 120 ms, en arm64:

Peticions:  900.000 → dins del milió gratuït                = 0,00 USD
Durada:     900.000 × 0,12 s × 0,5 GB = 54.000 GB-s
            → dins dels 400.000 gratuïts                    = 0,00 USD
--------------------------------------------------------------
Total                                                       = 0,00 USD/mes

Cas 3: creixement a tres ciutats més (problema 3). Deu vegades el trànsit: 9 milions de consultes al mes:

Peticions:  (9.000.000 − 1.000.000) / 1.000.000 × 0,20   =  1,60 USD
Durada:     9.000.000 × 0,12 × 0,5 = 540.000 GB-segon
            (540.000 − 400.000) × 0,0000166667           =  2,33 USD
Descompte arm64 (−20 % sobre la durada)                  = −0,47 USD
------------------------------------------------------------------------
Total                                                     ≈ 3,46 USD/mes

Menys de 4 dòlars al mes per servir 9 milions de peticions, sense cap servidor per administrar, apedaçar o vigilar. Quan la càrrega és irregular i per esdeveniments, el model sense servidor és difícilment batible.

El punt d'equilibri, per tenir-lo present: a partir d'una utilització sostinguda superior al 50-60 % d'un servidor, un contenidor o una EC2 reservada surten més barats. Lambda és cara si la fas servir com si fos un servidor sempre encès.

Errors, reintents i cues de missatges fallits

Què passa quan la funció falla depèn del mode d'invocació:

Mode Comportament davant d'error
Síncrona (Function URL, API Gateway) L'error es retorna al client. Sense reintents
Asíncrona (S3, SNS, EventBridge) Lambda reintenta 2 vegades amb espera creixent; si continua fallant, l'esdeveniment es descarta
Sondeig (SQS, DynamoDB Streams) Reintenta fins que el missatge caduca o va a la cua de missatges fallits

«L'esdeveniment es descarta» és la frase perillosa. Si la funció de miniatures falla tres vegades amb una foto concreta, aquella foto es queda sense miniatura i ningú no se n'assabenta. La protecció és una destinació d'error o una cua de missatges fallits (dead-letter queue, DLQ): un lloc on acaben els esdeveniments que no s'han pogut processar, per revisar-los i reprocessar-los.

# Configurar destinacions: els fallits van a una cua SQS, els exits s'ignoren.
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 principis que cal respectar des del primer dia:

  1. Idempotència. El lliurament és «almenys una vegada»: la mateixa foto es pot processar dues vegades. La nostra funció ho compleix perquè regenerar una miniatura sobre si mateixa dóna el mateix resultat. Si la funció incrementés un comptador o cobrés un pagament, caldria protegir-la explícitament.
  2. Distingir errors recuperables dels que no ho són. Un temps d'espera de xarxa mereix reintent; un NoSuchKey no. Reintentar allò irrecuperable és temps i diners llençats.
  3. Vigilar la DLQ. Una cua de fallits que ningú no mira és el mateix que no tenir-ne. Una alarma sobre ApproximateNumberOfMessagesVisible > 0 és obligatòria.

Aquí només deixem plantejat el concepte. SQS s'estudia a la lliçó 07-01, i els patrons complets d'idempotència, reintents i cues de missatges fallits són la lliçó 07-05.

Recapitulació del mòdul: què hi ha ja a AWS i què falta

Aquest és el punt del curs on convé aixecar la vista. MercadoFresco va començar el mòdul 2 amb un servidor físic a l'oficina; l'acaba amb això:

flowchart TB
    subgraph AWS["Compte 111122223333 - eu-west-1 (Irlanda)"]
        subgraph COMPUTO["Computacio"]
            ASG["Auto Scaling Group<br/>asg-mercadofresco-tienda<br/>2-4 instancies t3.micro<br/>en dues AZ (02-01)"]
            L1["Lambda<br/>generar-miniaturas (02-05)"]
            L2["Lambda<br/>estado-pedido (02-05)"]
        end
        subgraph DATOS["Dades"]
            S3["S3 mercadofresco-catalogo-fotos<br/>versionatge + cicle 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 -->|"esdeveniment ObjectCreated"| L1
        L1 --> S3
        L2 --> RDS
    end
    U["Clients de MercadoFresco"] --> ASG
    U --> L2

El que està resolt i el que no:

Problema Estat On es va resoldre
1. Caigudes dels divendres Parcial: hi ha Auto Scaling, falta el balancejador que reparteixi el trànsit 02-01; es completa a 03-03
2. Còpies no fiables Resolt: snapshots EBS amb DLM, versionatge d'S3, còpies automàtiques de RDS amb PITR 02-02, 02-03, 02-04
3. No poder créixer a més ciutats Parcial: la infraestructura ja escala, falta distribució global i arquitectura desacoblada 02-01, 02-05; mòduls 3, 6 i 7
4. Desplegaments arriscats Iniciat: plantilles de llançament versionades, versions i àlies de Lambda 02-01, 02-05; es resol al mòdul 8

I el que encara no existeix, que és molt:

  • Xarxa pròpia. Tot està a la VPC per defecte, amb les instàncies exposades. Falta dissenyar subxarxes públiques i privades, taules de rutes, i treure la base de dades a una subxarxa sense sortida a internet. Mòdul 3.
  • Balancejador de càrrega. Les quatre instàncies de l'ASG no reben trànsit repartit, i no hi ha HTTPS ni domini propi. 03-03 i 03-05.
  • Distribució de contingut. El 97 % de la factura d'S3 era transferència de sortida: CloudFront ho arregla. 03-04.
  • Identitat i secrets amb rigor. Hem fet servir rols i Secrets Manager de passada; falta l'estructura completa d'IAM, el xifratge amb claus pròpies i la protecció davant d'atacs. Mòdul 4.
  • Observabilitat. Tenim mètriques soltes i algun registre. Falta un sistema de taulers, alarmes, traces i auditoria de qui va fer què. Mòdul 5.
  • Infraestructura com a codi. Tot ho hem creat a mà o amb comandes soltes: no és reproduïble ni revisable. Mòdul 9.

Errors Habituals i Consells

  • Crear els clients de boto3 dins del gestor. Es paguen 100-300 ms a cada invocació. Sempre a l'àmbit global.
  • No descodificar la clau d'S3 amb unquote_plus. Qualsevol foto amb espais o enyes fallarà amb NoSuchKey. És l'errada número u de les Lambdes disparades per S3.
  • Processar només event["Records"][0]. Un esdeveniment pot portar diversos registres. Itera sempre.
  • Escriure al mateix prefix que dispara la funció. Bucle infinit. Filtra per prefix i restringeix els permisos d'escriptura del rol, com a defensa doble.
  • Assignar 128 MB «per estalviar». La CPU és proporcional a la memòria: la funció triga tant que el cost no baixa i la latència es dispara. Mesura amb Max Memory Used i prova diversos valors.
  • Posar el temps d'espera a 900 s per defecte. Una penjada es pagarà durant 15 minuts.
  • Desar estat en variables globals sense control. L'entorn es reutilitza i una llista global creix fins a esgotar la memòria.
  • Donar AdministratorAccess al rol d'execució. Concedeix només les accions i els recursos exactes que la funció necessita.
  • Oblidar lambda:add-permission per a l'origen de l'esdeveniment. El disparador no funciona i no apareix cap error visible enlloc.
  • Obrir una connexió a RDS per invocació sense límit de concurrència. Un pic de trànsit tomba la base de dades. Connexió a l'àmbit global i concurrència reservada.
  • No configurar destinació d'error ni DLQ en invocacions asíncrones. Els esdeveniments fallits es descarten en silenci.
  • Retornar fitxers grans a la resposta. El límit síncron és de 6 MB. Desa a S3 i retorna una URL prefirmada.
  • Consell: comença en arm64 llevat que una dependència ho impedeixi. És un 20 % d'estalvi per canviar un paràmetre.

Exercicis

Exercici 1: triar la tecnologia de còmput

Per a cada càrrega de MercadoFresco, decideix entre Lambda, EC2/contenidor o una combinació, i justifica-ho amb els criteris de la lliçó:

  • A) La botiga web PHP, que atén trànsit de manera contínua amb un pic els divendres.
  • B) Generar el PDF de la factura quan una comanda es marca com a lliurada (unes 900 al dia, 1,5 s cadascuna).
  • C) Recomprimir les 40.000 fotos del catàleg, un procés que avui triga 3 hores seguides.
  • D) Una feina que cada nit a les 3:00 exporta les comandes del dia a S3; triga 90 segons.
  • E) Un servei de recomanació que carrega un model de 4 GB a memòria i respon en menys de 50 ms.

Exercici 2: calcular i optimitzar el cost

La funció de miniatures està configurada amb 1.024 MB i triga 1.800 ms. Les proves amb AWS Lambda Power Tuning donen aquests resultats:

Memòria Durada
512 MB 3.400 ms
1.024 MB 1.800 ms
1.769 MB 1.150 ms
3.008 MB 1.050 ms

MercadoFresco creix i ara es processen 200.000 fotos al mes (ja fora del nivell gratuït de durada). Calcula el cost de durada de cada configuració en arm64 (0,0000133334 USD per GB-segon) i decideix quina triaries, tenint en compte també la latència.

Exercici 3: escriure el rol de mínim privilegi

MercadoFresco afegeix una tercera funció, mercadofresco-exportar-pedidos, que cada nit:

  1. Llegeix les comandes del dia de la rèplica de lectura de RDS.
  2. Obté les credencials de la base de dades de Secrets Manager.
  3. Escriu un fitxer CSV a s3://mercadofresco-informes-analitica/informes/AAAA/MM/.
  4. Envia una notificació a un tema d'SNS quan acaba.
  5. Escriu els seus registres a CloudWatch Logs.

Escriu la política de permisos del rol d'execució aplicant el mínim privilegi, i explica dos permisos que deliberadament no hi inclous encara que podrien semblar necessaris.

Solucions

Solució 1.

Cas Tria Justificació
A) Botiga web PHP EC2 amb Auto Scaling (el que ja vam muntar a 02-01) Trànsit continu, aplicació monolítica amb estat en disc, codi heretat. La utilització sostinguda fa que Lambda surti més cara i el redisseny no compensa
B) PDF de factura Lambda 900 × 1,5 s = 22,5 minuts de còmput al dia. És una tasca curta, per esdeveniments i sense estat: el cas canònic. Una EC2 estaria ociosa el 98 % del temps
C) Recomprimir 40.000 fotos Lambda, però repartida 3 hores seguides superen el límit de 15 minuts. La solució no és canviar de tecnologia: és repartir en 40.000 invocacions de ~2 s que corren en paral·lel, i la feina total passa de 3 hores a uns minuts. Alternativa vàlida: una tasca de contenidor si el procés no es pot paral·lelitzar
D) Exportació nocturna de 90 s Lambda + EventBridge programat Molt per sota dels 15 minuts, s'executa un cop al dia. Mantenir un servidor per a 90 segons diaris no té sentit. EventBridge es veu a 07-03
E) Recomanador amb model de 4 GB Contenidor a ECS/Fargate El model excedeix el límit de 250 MB del paquet (es podria fer servir imatge de contenidor de 10 GB), però el problema real és l'arrencada en fred: carregar 4 GB triga segons i el requisit és de 50 ms. Necessita un procés sempre encès amb el model a memòria. Mòdul 10

Solució 2.

Cost de durada en arm64, per a 200.000 invocacions:

GB-segon = invocacions × (memoria_GB) × (durada_s)
Cost     = (GB-segon − 400.000 gratuïts) × 0,0000133334
Memòria GB Durada GB-segon Facturable Cost
512 MB 0,50 3,4 s 340.000 0 (dins del gratuït) 0,00 USD
1.024 MB 1,00 1,8 s 360.000 0 (dins del gratuït) 0,00 USD
1.769 MB 1,73 1,15 s 397.900 0 (dins del gratuït) 0,00 USD
3.008 MB 2,94 1,05 s 617.400 217.400 2,90 USD

Resultat revelador: les tres primeres configuracions costen exactament el mateix, zero, perquè totes caben al nivell gratuït de 400.000 GB-segon. La decisió, per tant, no és econòmica sinó de latència i de marge.

Tria: 1.769 MB. Raons:

  • És la més ràpida de les tres gratuïtes: 1,15 s davant de 3,4 s, gairebé tres vegades millor.
  • 1.769 MB correspon a un vCPU complet, el punt on Pillow deixa d'estar limitat per CPU.
  • Es queda en 397.900 GB-segon, molt just respecte al límit. Si el volum creix, començarà a costar: a 300.000 fotos serien 596.850 GB-segon, és a dir 2,62 USD al mes. Continua sent irrellevant.
  • 3.008 MB es descarta: només millora 100 ms sobre 1.769 MB (la funció ja no està limitada per CPU) i multiplica el consum de GB-segon, sortint-se del nivell gratuït.

La lliçó general: per damunt d'un vCPU, continuar pujant memòria deixa d'accelerar i comença a costar. El punt òptim acostuma a ser a prop de 1.769 MB per a feines d'un sol fil.

Solució 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 deliberadament NO s'inclouen:

  1. s3:GetObject i s3:DeleteObject sobre el bucket d'informes. La funció només escriu: no necessita llegir ni esborrar res. Excloure'ls significa que, si la funció es veiés compromesa per una dependència maliciosa, no podria exfiltrar els informes històrics de vendes ni destruir-los. És el mateix raonament que vam aplicar a la funció de miniatures.

  2. Qualsevol permís de rds:*. Resulta contraintuïtiu, perquè la funció consulta la base de dades, però connectar-se a PostgreSQL amb usuari i contrasenya no passa per IAM: és una connexió TCP autenticada pel motor mateix. Els permisos rds:* serveixen per administrar instàncies (crear-les, esborrar-les, modificar-les), i la funció no ha de poder fer res d'això. Concedir-los seria donar permís per esborrar la base de dades a un procés que només necessita llegir una taula. (L'excepció seria l'autenticació IAM de RDS, que sí que requereix rds-db:connect sobre un recurs d'usuari concret.)

Detalls addicionals del disseny: l'ARN del secret porta un comodí final perquè Secrets Manager afegeix un sufix aleatori de sis caràcters al nom; i els permisos d'ec2:*NetworkInterface són obligatoris i amb Resource: "*" quan la funció es connecta a recursos dins d'una VPC —és una de les poquíssimes excepcions legítimes al mínim privilegi, imposada pel servei mateix.

Conclusió

Amb aquesta lliçó MercadoFresco tanca el seu primer mòdul de serveis reals. Saps què és la computació sense servidor i, més important, quan no fer-la servir: has comparat Lambda amb EC2 i amb contenidors en una taula de criteris i has aplicat la regla que per damunt del 50-60 % d'utilització sostinguda el model deixa de compensar. Domines el model d'execució —gestor, esdeveniment, context, resposta— i el cicle de vida de l'entorn, del qual es deriva l'optimització més rendible que existeix a Lambda: crear els clients de boto3 a l'àmbit global, fora del gestor, perquè l'entorn es reutilitza. Coneixes l'arrencada en fred, què l'allarga i les quatre maneres de mitigar-la, i saps que la concurrència reservada és alhora una garantia i un fre, el fre que protegeix RDS de la capacitat de Lambda d'escalar sense límit.

Has entès la relació memòria-CPU-cost, que és el paràmetre que més gent configura malament, i l'has calculat amb números: assignar 128 MB «per estalviar» acostuma a sortir igual de car i vuit vegades més lent. Saps triar arm64 per un 20 % d'estalvi, posar un temps d'espera ajustat i no desar contrasenyes a variables d'entorn.

I has escrit la funció que tanca el cercle obert a la lliçó 02-03: mercadofresco-generar-miniaturas es dispara amb l'esdeveniment s3:ObjectCreated del bucket mercadofresco-catalogo-fotos, descodifica correctament la clau amb unquote_plus, itera sobre tots els registres de l'esdeveniment, escriu en un prefix diferent del d'entrada per no crear un bucle infinit, distingeix els errors recuperables dels que no ho són i etiqueta el que produeix. L'has empaquetada amb Pillow en una capa compilada per a la plataforma correcta, l'has desplegada per consola i per CLI amb create-function i update-function-code --publish, i li has donat un rol d'execució de mínim privilegi que només pot llegir sota productos/, només pot escriure sota miniaturas/ i no pot esborrar res. Has afegit una segona funció, mercadofresco-estado-pedido, exposada per Function URL, amb la connexió a RDS reutilitzada entre invocacions, consultes parametritzades, concurrència limitada a 20 i errors que no filtren informació al client. Saps llegir la línia REPORT de CloudWatch Logs per ajustar la memòria, has recorregut els orígens d'esdeveniments habituals assenyalant on s'estudia cadascun, coneixes els límits reals —15 minuts, 250 MB, 512 MB a /tmp, 6 MB de payload síncron— i has fet el càlcul de preus que demostra que servir 9 milions de peticions costa menys de 4 dòlars al mes. Finalment, saps què passa quan una funció falla en cada mode d'invocació i per què una cua de missatges fallits sense vigilància és el mateix que no tenir-ne.

Recapitulant el mòdul sencer: MercadoFresco té ja a eu-west-1 un grup d'Auto Scaling de la botiga repartit en dues zones de disponibilitat, discs EBS amb còpies automàtiques gestionades per Data Lifecycle Manager, el catàleg de fotos a S3 amb versionatge i regles de cicle de vida, la base de dades de comandes a RDS PostgreSQL amb Multi-AZ, rèplica de lectura i restauració a un punt en el temps, i dues funcions Lambda que reaccionen a esdeveniments i responen per HTTP. El problema 2, les còpies de seguretat no fiables, està resolt de cap a peus. Els altres tres estan encaminats però incomplets.

I el que falta és, precisament, el que sosté tota la resta. Tot això viu a la VPC per defecte: les instàncies estan exposades on no haurien d'estar-ho, la base de dades comparteix espai de xarxa amb la botiga, no hi ha res que reparteixi el trànsit entre les quatre instàncies del divendres, no hi ha HTTPS ni domini propi, i el 97 % de la factura d'S3 continua sent transferència de sortida sense memòria cau. Al mòdul 3, «Xarxes i lliurament de contingut», començant per la lliçó 03-01 «Amazon VPC», construirem la xarxa pròpia de MercadoFresco: subxarxes públiques i privades en dues zones de disponibilitat, taules de rutes, passarel·les i una base de dades que deixa de ser abastable des d'internet. Sobre aquesta xarxa muntarem després els grups de seguretat, el balancejador que completa la solució al pic dels divendres, CloudFront per abaratir i accelerar les fotos, i Route 53 perquè mercadofresco.example apunti per fi al núvol.

Curs d'AWS

Mòdul 1: Introducció a AWS

Mòdul 2: Serveis principals d'AWS

Mòdul 3: Xarxes i lliurament de contingut

Mòdul 4: Seguretat i identitat

Mòdul 5: Monitoratge i gestió

Mòdul 6: Bases de dades

Mòdul 7: Integració d'aplicacions

Mòdul 8: Eines per a desenvolupadors

Mòdul 9: Infraestructura com a codi i govern de comptes

Mòdul 10: Contenidors a AWS

Mòdul 11: Millors pràctiques i gestió de costos

© Copyright 2026. Tots els drets reservats