Tema 5 · 7 · Ejercicio adicional A: demanda eléctrica (REE) a S3 con Athena
Presentación del Tema 5: «Ejercicio A · ETL de la demanda eléctrica (REE)» (ejercicios adicionales; solo si tu docente te lo pide). Duración: a tu ritmo (el entorno se prepara en 15-20 min). Coste: prácticamente nulo (Lambda, unos pocos ficheros en S3, consultas de Athena); lo que cuesta es la infraestructura base de la carpeta 05.01.
Etiquetas de lo que creas. Todo recurso que crees a mano (máquina, volumen, bucket, base de datos, tabla, función...) ponle estas etiquetas, al crearlo (apartado Tags / Etiquetas) o después desde su pestaña de etiquetas:
curso=G214,universidad=CUNEFyactividad=05.07-adicionalaree. Si algún día hay que borrar recursos a mano, se localizan todos de golpe conaws resourcegroupstaggingapi get-resources --region eu-north-1 --tag-filters Key=actividad,Values=05.07-adicionalaree. Más detalle (cómo etiquetar con la CLI, cómo buscar y qué hacer en una emergencia): README principal, sección «Etiquetas».
Qué vas a hacer
Un ETL completo sobre una API pública real: una Lambda propia pide a la API REData de Red Eléctrica (REE) la demanda eléctrica diaria de un mes (JSON), la guarda en S3 particionada por año y mes, y la consultas con Glue/Athena. Esta actividad no trae script ni plantilla: creas tú la función Lambda siguiendo la receta de abajo. Este README prepara el entorno, te da el esqueleto corregido del código (adicional-a-ree.py, el mismo) y te dice cómo comprobarlo; el diseño completo, las consultas y las reglas son el entregable y los haces tú.
Qué se crea en AWS: una función Lambda propia, un rol de IAM propio, la partición de ficheros raw/demanda/anio=AAAA/mes=MM/demanda.json en el bucket del Paso 0 y (entregable) una tabla de Athena y una regla de EventBridge mensual.
Qué contiene esta carpeta
| Fichero | Para qué sirve | En qué paso se usa |
|---|---|---|
adicional-a-ree.py |
Esqueleto corregido de la Lambda: pide el mes (por defecto el anterior) a REE y escribe raw/demanda/anio=AAAA/mes=MM/demanda.json en el bucket. No es la solución completa: faltan la tabla de Athena, la regla de EventBridge y el resto del entregable. No lo despliega ningún script. |
Paso 3 (es el código de tu función) |
README.md |
Este documento. | Siempre |
Cómo funciona el esqueleto: sin parámetros pide el mes anterior completo (lo que hará la regla mensual). Para cargar otro mes, invócalo con {"mes": "2025-09"}. Pide del día 1 al último día a las T23:59 y filtra por mes (si pidieras hasta el día 1 del mes siguiente, REE devolvería ese día incompleto y la partición saldría con una fila de más). Guarda fecha y mwh en JSON por líneas; es idempotente (repetir no duplica). La API devuelve percentage = 1 en todas las filas de esta serie, así que no se guarda: si quieres el peso de cada día, calcúlalo en Athena sobre la suma del mes.
Antes de empezar
- Depende de Tema 5 · 1 · Infraestructura base: escribe en su bucket (
$BUCKET). El rol del Paso 0 solo lee, así que necesitas tu propio rol cons3:PutObject(paso 2). - Región eu-north-1 (Estocolmo).
- Para la parte de Athena, haber configurado antes una ubicación de resultados (Athena → Settings → Manage) o usar el grupo de trabajo del Tema 4 si sigue existiendo.
- Usa nombres de función y rol que no choquen con los del laboratorio (no reutilices
crypto-price-tracker*). Aquí se usanetl-adicional-ayrol-etl-adicional-a; puedes cambiarlos.
Cómo llevar los ficheros a CloudShell
- En tu PC, comprime esta carpeta entera (
05.07-adicionalaree): clic derecho → Comprimir en archivo ZIP (Windows 11), Enviar a > Carpeta comprimida en zip (Windows 10) o Comprimir (Mac). - En CloudShell (región eu-north-1): Actions → Upload file y elige
05.07-adicionalaree.zip. - Ejecuta:
cd ~
unzip -o 05.07-adicionalaree.zip && cd 05.07-adicionalaree && chmod -R u+rwX .
ls
Si ya subiste
g214-materiales.zip(README principal), no necesitas este zip: entra concd ~/g214-materiales/tema5/05.07-adicionalareey, si algún fichero daPermission denied, ejecuta allíchmod -R u+rwX . && chmod +x *.sh.
Qué verás: adicional-a-ree.py y README.md. (No hay .sh, no hace falta chmod.)
Paso a paso
0. Recupera las variables del Paso 0:
OUT() { aws cloudformation describe-stacks --stack-name cunef-etl --region eu-north-1 \
--query "Stacks[0].Outputs[?OutputKey=='$1'].OutputValue" --output text; }
BUCKET=$(OUT BucketName)
echo "Bucket : $BUCKET"
Qué verás: el nombre de tu bucket. Si sale vacío, el stack cunef-etl no existe: vuelve a la carpeta 05.01.
1. Prueba la API desde CloudShell antes de programar. Pon la URL entre comillas (lleva &) y en una sola línea. No uses curl -I: REE responde 403 a las peticiones HEAD aunque la API funcione.
curl -s "https://apidatos.ree.es/es/datos/demanda/evolucion?start_date=2026-01-01T00:00&end_date=2026-01-31T23:59&time_trunc=day" | head -c 800
Qué verás: un JSON con included[0].attributes.values: 31 valores diarios para enero.
2. Crea un rol propio con permiso de escritura en el bucket (una sola vez):
ROLE=rol-etl-adicional-a
cat > confianza.json <<'EOF'
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]}
EOF
ROLE_ARN=$(aws iam create-role --role-name $ROLE \
--assume-role-policy-document file://confianza.json --query Role.Arn --output text)
aws iam attach-role-policy --role-name $ROLE \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
echo "$ROLE_ARN"
sleep 10
cat > politica.json <<EOF
{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:PutObject"],"Resource":"arn:aws:s3:::$BUCKET/*"}]}
EOF
aws iam put-role-policy --role-name $ROLE --policy-name permisos-mios \
--policy-document file://politica.json
Qué verás: el ARN del rol (arn:aws:iam::<cuenta>:role/rol-etl-adicional-a). Los demás comandos no imprimen nada. El rol base solo puede escribir logs; la política añade únicamente s3:PutObject sobre tu bucket (los scripts 2, 3 y 4 de las otras carpetas hacen algo parecido con sus roles).
3. Empaqueta y crea la función. Sin librerías extra (el código solo usa boto3 y la librería estándar). Sube el timeout a 60 s:
cp adicional-a-ree.py lambda_function.py
zip -q funcion.zip lambda_function.py
aws lambda create-function --function-name etl-adicional-a --runtime python3.12 \
--handler lambda_function.lambda_handler --role "$ROLE_ARN" \
--zip-file fileb://funcion.zip --timeout 60 --memory-size 256 \
--environment "Variables={BUCKET=$BUCKET}" --region eu-north-1
Qué verás: un JSON con la configuración de la función ("FunctionName": "etl-adicional-a", "Timeout": 60). Si create-function dice que el rol no se puede asumir, espera unos segundos y repite (el rol acaba de crearse). Para volver a subir el código después de cambiarlo:
aws lambda update-function-code --function-name etl-adicional-a \
--zip-file fileb://funcion.zip --region eu-north-1
aws lambda wait function-updated --function-name etl-adicional-a --region eu-north-1
4. Invócala y mira los logs. Para la variación interanual necesitas dos particiones (un mes y el mismo mes de hace un año): invoca una vez por mes con el parámetro mes. Ajusta los meses a "el mes anterior" y "el mismo mes de hace un año" según el día en que lo hagas:
for MES in 2025-09 2026-09; do
aws lambda invoke --function-name etl-adicional-a --cli-binary-format raw-in-base64-out \
--payload "{\"mes\": \"$MES\"}" --region eu-north-1 salida.json && cat salida.json
done
aws logs tail /aws/lambda/etl-adicional-a --since 10m --region eu-north-1
Si la primera invocación falla con AccessDenied en PutObject: la política que acabas de añadir tarda unos segundos en propagarse (probado: ocurre justo después de crear el rol). Espera 10-15 segundos y repite la invocación.
Qué verás: una línea por mes con {"filas": 30, "key": "raw/demanda/anio=2026/mes=09/demanda.json"} (referencia: la invocación de un mes de 30 días devuelve "filas": 30). Si tu función lanza una excepción, invoke muestra "FunctionError": "Unhandled" y el detalle está en salida.json y en los logs.
5. Comprueba la partición en S3:
aws s3 ls s3://$BUCKET/raw/demanda/ --recursive
Qué verás: un fichero demanda.json por cada mes que hayas invocado, bajo raw/demanda/anio=AAAA/mes=MM/.
6. Crea la tabla de Athena (sustituye TU_BUCKET; anio y mes son las carpetas anio=AAAA/mes=MM que escribe la función, y los ficheros son JSON por líneas con fecha y mwh), registra las particiones y consulta:
CREATE EXTERNAL TABLE demanda_ree (fecha string, mwh double)
PARTITIONED BY (anio string, mes string)
ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe'
LOCATION 's3://TU_BUCKET/raw/demanda/';
MSCK REPAIR TABLE demanda_ree;
Es la misma plantilla de JsonSerDe que la tabla de la carpeta 05.03 (paso 7a), que sí se probó en una cuenta real; esta sentencia concreta también (con dos meses: 30 filas por partición tras MSCK REPAIR TABLE). Si prefieres un Crawler sobre raw/demanda/, también vale. Las tres consultas del ejercicio (demanda media por mes, día de mayor demanda y variación interanual) son el entregable y las escribes tú. Los resultados de Athena se guardan en la ubicación que configuraste (por ejemplo athena-results/) y también hay que borrarlos al terminar.
7. Entregable (lo haces tú): el código ya está; faltan las tres consultas de Athena y la regla mensual de EventBridge (ver la carpeta 05.06 para los comandos de una regla; aquí usarías cron(...) o rate(...) mensual en lugar de cada minuto y tu función etl-adicional-a).
Errores frecuentes
Task timed out after 3.00 seconds. La función se creó con el tiempo máximo por defecto (3 s). Súbelo a 60 s:aws lambda update-function-configuration --function-name etl-adicional-a --timeout 60 --region eu-north-1(o Configuration → General configuration). Con el comando del paso 3 ya lo llevas.- La partición del mes sale con 31 filas y la última es del mes siguiente. La URL pide hasta el día 1 del mes siguiente y REE devuelve ese día incompleto. Pide hasta el último día a las
T23:59y filtra por mes (como hace el esqueleto corregido). curl -Ida403. REE no admite peticionesHEAD; usacurl -s.HTTP Error 502/504o429. La API pública está saturada. Eltry/exceptdel esqueleto lo registra y la función termina limpia con{"error": ...}: invócala de nuevo pasado un minuto.AccessDeniedal escribir en S3 (s3:PutObject). Tu rol no tiene la política del paso 2 o$BUCKETno es el del stackcunef-etl. Repite el paso 2 conBUCKETbien definido.KeyError: 'BUCKET'. La función no tiene la variable de entornoBUCKET: añádela en Configuration → Environment variables o encreate-function(paso 3).- Athena devuelve 0 filas. Falta registrar particiones (
MSCK REPAIR TABLE demanda_ree;) oLOCATIONno apunta as3://<bucket>/raw/demanda/.
Limpieza
Hazla cuando termines, en este orden, y solo borra lo que hayas creado tú:
-
Regla de EventBridge (si creaste la mensual):
aws events remove-targets --rule NOMBRE --ids 1 --region eu-north-1yaws events delete-rule --name NOMBRE --region eu-north-1(o EventBridge → Rules → Delete). -
La función y su rol:
aws lambda delete-function --function-name etl-adicional-a --region eu-north-1 aws iam detach-role-policy --role-name rol-etl-adicional-a \ --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole aws iam delete-role-policy --role-name rol-etl-adicional-a --policy-name permisos-mios aws iam delete-role --role-name rol-etl-adicional-aUn error
NoSuchEntitysignifica que ese elemento ya no existía. -
Athena y Glue:
DROP TABLE demanda_ree;en Athena (oaws glue delete-table --database-name default --name demanda_ree --region eu-north-1); si creaste un Crawler,aws glue delete-crawler --name NOMBRE --region eu-north-1; si creaste un grupo de trabajo propio,aws athena delete-work-group --work-group NOMBRE --recursive-delete-option --region eu-north-1. Los resultados de Athena:aws s3 rm s3://BUCKET/athena-results/ --recursive. -
Opcional: el grupo de logs (
aws logs delete-log-group --log-group-name /aws/lambda/etl-adicional-a --region eu-north-1) y los ficheros de CloudShell (cd ~ && rm -rf 05.07-adicionalaree 05.07-adicionalaree.zip).
Los ficheros raw/demanda/ están en el bucket del Paso 0 y se borran con la infraestructura base. El borrado de la infraestructura base (carpeta 05.01, sección Limpieza) debe ser el último paso de todo el Tema 5.
Cómo sabes que has terminado
- [ ]
aws lambda invokedevolvió{"filas": 30, "key": ...}(30 para un mes de 30 días) para los dos meses que cargaste. - [ ]
aws s3 ls s3://$BUCKET/raw/demanda/ --recursivemuestra una partición por mes. - [ ] La tabla
demanda_reeresponde en Athena trasMSCK REPAIR TABLE. - [ ] Entregaste lo que pide la diapositiva (código, tabla/consultas y regla de EventBridge).
- [ ] Has hecho la limpieza de esta actividad.
Anterior: Tema 5 · 6 · Programar con EventBridge · Siguiente: Tema 5 · 8 · Adicional B (Euribor)