# 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` = `CUNEF` y `actividad` = `05.07-adicionalaree`. Si algún día hay que borrar recursos a mano, se localizan todos de golpe con `aws 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](../../README.md), 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](../05.01-infraestructurabase/README.md)**: escribe en su bucket (`$BUCKET`). El rol del Paso 0 solo lee, así que necesitas **tu propio rol** con `s3: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 usan `etl-adicional-a` y `rol-etl-adicional-a`; puedes cambiarlos. ## Cómo llevar los ficheros a CloudShell 1. 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). 2. En CloudShell (región eu-north-1): **Actions → Upload file** y elige `05.07-adicionalaree.zip`. 3. Ejecuta: ```bash 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 con `cd ~/g214-materiales/tema5/05.07-adicionalaree` y, si algún fichero da `Permission 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:** ```bash 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. ```bash 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): ```bash 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 <: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: ```bash 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: ```bash 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: ```bash 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:** ```bash 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: ```sql 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](../05.06-ProgramarEventBridge.md) 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:59` y filtra por mes (como hace el esqueleto corregido). - **`curl -I` da `403`.** REE no admite peticiones `HEAD`; usa `curl -s`. - **`HTTP Error 502/504` o `429`.** La API pública está saturada. El `try/except` del esqueleto lo registra y la función termina limpia con `{"error": ...}`: invócala de nuevo pasado un minuto. - **`AccessDenied` al escribir en S3 (`s3:PutObject`).** Tu rol no tiene la política del paso 2 o `$BUCKET` no es el del stack `cunef-etl`. Repite el paso 2 con `BUCKET` bien definido. - **`KeyError: 'BUCKET'`.** La función no tiene la variable de entorno `BUCKET`: añádela en *Configuration → Environment variables* o en `create-function` (paso 3). - **Athena devuelve 0 filas.** Falta registrar particiones (`MSCK REPAIR TABLE demanda_ree;`) o `LOCATION` no apunta a `s3:///raw/demanda/`. ## Limpieza Hazla cuando termines, **en este orden**, y solo borra lo que hayas creado tú: 1. **Regla de EventBridge** (si creaste la mensual): `aws events remove-targets --rule NOMBRE --ids 1 --region eu-north-1` y `aws events delete-rule --name NOMBRE --region eu-north-1` (o **EventBridge → Rules → Delete**). 2. **La función y su rol:** ```bash 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-a ``` Un error `NoSuchEntity` significa que ese elemento ya no existía. 3. **Athena y Glue:** `DROP TABLE demanda_ree;` en Athena (o `aws 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`. 4. **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](../05.01-infraestructurabase/README.md), sección Limpieza) debe ser el último paso de todo el Tema 5.** ## Cómo sabes que has terminado - [ ] `aws lambda invoke` devolvió `{"filas": 30, "key": ...}` (30 para un mes de 30 días) para los dos meses que cargaste. - [ ] `aws s3 ls s3://$BUCKET/raw/demanda/ --recursive` muestra una partición por mes. - [ ] La tabla `demanda_ree` responde en Athena tras `MSCK 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](../05.06-ProgramarEventBridge.md) · Siguiente: [Tema 5 · 8 · Adicional B (Euribor)](../05.08-adicionalbeuribor/README.md)