Servidor disponible solo los jueves de 10:30 a 12:30 y los viernes de 10:00 a 13:00 (hora de Madrid); fuera de ese horario está apagado. Fuera del horario, usa la documentación local del material que te descargaste (el README.md de cada carpeta).
G214 · CUNEF
Inicio / tema5 / 05.07-adicionalaree

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, 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

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

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

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:

    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, sección Limpieza) debe ser el último paso de todo el Tema 5.

Cómo sabes que has terminado


Anterior: Tema 5 · 6 · Programar con EventBridge · Siguiente: Tema 5 · 8 · Adicional B (Euribor)