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
- Què és la computació sense servidor i en què es diferencia d'EC2
- Model d'execució: gestor, esdeveniment, context i resposta
- Arrencada en fred i arrencada en calent
- Concurrència: reservada, provisionada i el límit del compte
- Configuració: memòria, CPU, temps d'espera, variables i arquitectura
- La funció de les miniatures: el codi
- Empaquetatge amb dependències i capes
- Desplegar per consola i per CLI
- Permisos: el rol d'execució i el mínim privilegi
- Registres a CloudWatch Logs i depuració
- Orígens d'esdeveniments habituals
- Segona funció: consultar l'estat d'una comanda per HTTP
- Límits reals que cal conèixer
- Preus, amb el càlcul concret de MercadoFresco
- Errors, reintents i cues de missatges fallits
- 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:
- La tasca dura menys de 15 minuts? Si no, Lambda queda descartada.
- 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.
- Necessites control del sistema operatiu o estat en disc? Llavors EC2 o contenidors.
- 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òriaArrencada 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:
- Moure feina al codi global i mantenir el paquet petit. Gratis.
- Triar un llenguatge lleuger. Python o Node per a funcions de latència crítica.
- Pujar la memòria. Més CPU accelera la inicialització, i sovint el cost total no puja perquè l'execució dura menys.
- 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-1Sense 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-1Es 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_plussobre la clau. S3 codifica els noms a l'esdeveniment. Sense aquesta línia, qualsevol foto amb espais, accents o enyes donariaNoSuchKey. É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 ambevent["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.
raiseals errors recuperables. Lambda reintenta automàticament les invocacions asíncrones; empassar-se l'excepció amb unreturnfaria que l'errada passés desapercebuda.NoSuchKeyes 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.pyDesplegar per consola i per CLI
Per consola
- Consola → Lambda → regió Irlanda → Crear funció.
- Crear des de zero. Nom:
mercadofresco-generar-miniaturas. Temps d'execució: Python 3.12. Arquitectura: arm64. - Canviar el rol d'execució → crear-ne un de nou (l'ajustarem a l'apartat següent).
- Enganxa el codi a l'editor o puja el ZIP.
- Configuració → Configuració general: memòria 1.024 MB, temps d'espera 30 s.
- Configuració → Variables d'entorn: les quatre que hem definit abans.
- Capes → Afegir una capa →
mercadofresco-imagen. - Disparadors → Afegir disparador → S3 → bucket
mercadofresco-catalogo-fotos, esdevenimentTots els esdeveniments de creació d'objectes, prefixproductos/, sufix.jpg. - 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-1Actualitzar 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:
s3:GetObjectnomés sotaproductos/. Si la funció es veu compromesa, no pot llegir els informes de vendes ni les còpies de la base de dades.s3:PutObjectnomés sotaminiaturas/. 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.- Sense
s3:DeleteObject. La funció no necessita esborrar res, així que no pot. - 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-1Al 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 msCom 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 sobreErrors > 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 | Sí |
| Autenticació | Cap o IAM | IAM, Cognito, JWT, autoritzadors propis |
| Limitació de taxa | No | Sí |
| 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-1Quatre 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:
- Peticions: 0,20 USD per milió.
- 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/mesCost 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/mesCas 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/mesMenys 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-1Tres principis que cal respectar des del primer dia:
- 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.
- Distingir errors recuperables dels que no ho són. Un temps d'espera de xarxa mereix
reintent; un
NoSuchKeyno. Reintentar allò irrecuperable és temps i diners llençats. - 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à ambNoSuchKey. É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 Usedi 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
AdministratorAccessal rol d'execució. Concedeix només les accions i els recursos exactes que la funció necessita. - Oblidar
lambda:add-permissionper 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:
- Llegeix les comandes del dia de la rèplica de lectura de RDS.
- Obté les credencials de la base de dades de Secrets Manager.
- Escriu un fitxer CSV a
s3://mercadofresco-informes-analitica/informes/AAAA/MM/. - Envia una notificació a un tema d'SNS quan acaba.
- 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:
-
s3:GetObjectis3:DeleteObjectsobre 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. -
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 permisosrds:*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 requereixrds-db:connectsobre 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
- Què és AWS?
- Configuració del teu compte d'AWS
- Infraestructura global d'AWS
- Consola d'administració d'AWS
- AWS CLI i SDK
Mòdul 2: Serveis principals d'AWS
Mòdul 3: Xarxes i lliurament de contingut
- Amazon VPC
- Grups de seguretat i llistes de control d'accés
- Elastic Load Balancing
- Amazon CloudFront
- Route 53
Mòdul 4: Seguretat i identitat
- AWS Identity and Access Management (IAM)
- AWS Key Management Service (KMS)
- Secrets Manager i Parameter Store
- AWS Shield
- AWS WAF
Mòdul 5: Monitoratge i gestió
- Amazon CloudWatch
- AWS X-Ray i traçabilitat distribuïda
- AWS CloudTrail
- AWS Config
- AWS Trusted Advisor
Mòdul 6: Bases de dades
- Com triar la base de dades adequada
- Amazon DynamoDB
- Amazon Aurora
- Amazon Redshift
- Amazon ElastiCache
Mòdul 7: Integració d'aplicacions
- Amazon SQS
- Amazon SNS
- Amazon EventBridge
- AWS Step Functions
- Patrons d'integració: idempotència, reintents i cues de missatges fallits
