Construyendo en abierto

Un pipeline de datos a la vista de todos.

Diez fases. Cada una con su diagrama, sus decisiones razonadas y su código en GitHub. Sin recortes ni pasos ocultos: el mismo criterio con el que se construye en producción.

La pregunta de negocio

¿Cuánto sube la demanda eléctrica peninsular por cada grado de temperatura, y a qué horas se paga más cara esa energía?

Estado en vivo

Directamente desde GitHub

Estos datos no están escritos a mano: se piden a la API de GitHub cada vez que abres esta página.

Fase actual
—
Commits
—
Último cambio
—
Licencia
Público
Últimos commitscaudalit-pipeline
Consultando GitHub…

Recorrido

Las diez fases

Cada fase cerrada deja su diagrama, su PDF y sus decisiones técnicas documentadas en el repositorio.

00

Caso y dataset

Elección del caso de negocio y verificación de las fuentes de datos.

Cerrada
01

Arquitectura

Diseño end-to-end del pipeline y decisiones de servicios.

Ver documentación →
Cerrada
02

Repositorio y estructura

Estructura del proyecto y frontera de lo que se hace público.

Ver documentación →
Cerrada
03

Terraform: backend, S3, IAM y Athena

Infraestructura base con estado remoto y permisos mínimos.

Ver documentación →
Cerrada
04

Ingesta con Lambda

Dos funciones con reintentos y backoff, escribiendo en S3 raw.

Ver documentación →
Cerrada
05

Transformación con Glue y PySpark

Limpieza, validación y cruce por hora hacia parquet particionado.

Ver documentación →
En curso
06

Consulta con Athena

Las queries que responden la pregunta de negocio.

Pendiente
07

Orquestación con Step Functions

Encadenado de las etapas con reintentos y alertas.

Pendiente
08

CI/CD con GitHub Actions

Validación automática de Terraform en cada pull request.

Pendiente
09

Documentación final

README completo, decisiones y estimación de coste.

Pendiente
10

Teardown

terraform destroy: la infraestructura desaparece, el repositorio queda.

Pendiente

Criterio

Las decisiones que no se ven en el diagrama

Un diagrama enseña qué servicios hay. Lo que separa un pipeline de un script con suerte son las decisiones de debajo. Estas son las de las fases ya cerradas.

Fase 1 · Arquitectura

Elegir la fuente que no te bloquea

Se descartó ESIOS a propósito. Ofrece los mismos datos de Red Eléctrica, pero exige un token que se solicita por correo y tarda días. apidatos.ree.es es pública, sin autenticación y responde al instante. Elegir la fuente que no te bloquea también es arquitectura.

Step Functions en vez de Airflow. Amazon MWAA cuesta del orden de 300 USD al mes aunque no se ejecute un solo DAG, porque mantiene el entorno levantado. Con tres pasos encadenados no se justifica: Step Functions se paga por transición.

Parquet particionado por fecha. Athena factura por terabyte escaneado. Particionar convierte un escaneo completo de la tabla en leer solo los días que la consulta pregunta.

Fase 2 · Repositorio

Un repositorio público solo se cruza en un sentido

El .gitignore va antes del primer commit. Si subes un fichero con credenciales y lo borras al día siguiente, no lo has borrado: sigue en el commit donde entró, y los buscadores ya lo han visto. Sacarlo de verdad obliga a reescribir el historial completo.

El caso clásico en infraestructura como código es el *.tfstate. Terraform guarda ahí el estado real de la infraestructura y puede incluir valores sensibles en texto plano. Es la filtración más habitual en repos de IaC.

Correo privado en los commits. GitHub facilita una dirección @users.noreply. Sin ella, el correo real queda escrito en cada commit de un repositorio público, y hay bots rastreando exactamente eso.

Fase 3 · Infraestructura

El estado nunca en local, y el presupuesto antes que nada

Estado remoto en S3 con bloqueo nativo. Terraform 1.10 introdujo use_lockfile, que implementa el bloqueo sobre el propio S3. Ya no hace falta una tabla de DynamoDB solo para eso: un recurso menos que crear, pagar y recordar destruir.

El huevo y la gallina del bucket de estado. El bucket que guarda el estado no puede guardar su propio estado. Se crea una vez desde un módulo bootstrap con estado local, lleva prevent_destroy y sobrevive al teardown final.

IAM sin políticas gestionadas de AWS. Las políticas gestionadas son cómodas y casi siempre conceden de más. Aquí cada rol lleva una política propia acotada a los ARN concretos con los que trabaja.

El presupuesto con alerta se crea antes que el primer recurso. Una alerta de coste que se configura después de la factura no sirve de nada.

Fase 4 · Ingesta

Reintentar ante un error 400, que es justo lo que no hay que hacer

La API de Red Eléctrica devuelve 400 Error Interno de forma intermitente para peticiones perfectamente formadas, y a la siguiente responde 200 con los mismos parámetros. Un 400 normalmente significa que la petición está mal y reintentar es un antipatrón; aquí, no reintentar produce un pipeline que funciona unos días sí y otros no.

Espera exponencial con jitter. 1,5s, 3s, 6s, 12s, 24s, más un aleatorio de hasta un segundo. El jitter no es estético: sin él, varias ejecuciones que fallan a la vez reintentan sincronizadas y golpean el origen en el mismo instante.

Cero dependencias externas. requests no viene en el runtime de Lambda y empaquetarlo obliga a montar un paso de build. urllib de la biblioteca estándar hace lo mismo para este caso.

Clave determinista en S3. Reprocesar un día sobrescribe su fichero en vez de duplicarlo.

Fase 5 · Transformación

El bug silencioso era el arreglo

Open-Meteo entrega las horas sin desfase y, en otro campo, el desfase que ha aplicado. Consultada en septiembre, para el 1 de marzo declara utc_offset_seconds: 7200, cuando Madrid ese día está en UTC+1. La primera lectura fue que el campo estaba mal y había que ignorarlo, resolviendo la hora local con las reglas de Europe/Madrid.

Esa lectura era falsa. La API es coherente consigo misma: construye la serie aplicando el desfase que declara, así que etiqueta − utc_offset_seconds da el UTC correcto siempre. Reinterpretar la etiqueta como hora de Madrid era lo que desplazaba el dato una hora: 21 horas mal de 24 el 1 de marzo, 19 de 24 el 15 de enero, y ninguna en julio, cuando Madrid sí está en UTC+2.

La solución no fue corregir el desfase, fue no tenerlo. La ingesta pide los datos en timezone=UTC y la marca ya es un instante. El parseo rechaza en alto cualquier payload que no venga en UTC: el problema nunca fue equivocarse de huso, fue que equivocarse no producía ningún síntoma.

Y 28 tests en verde no lo vieron. Comparaban la implementación en PySpark contra la implementación de referencia en Python, y las dos aplicaban la misma regla. Si la regla es falsa, las dos fallan igual. Ahora hay una comprobación que contrasta la salida contra el JSON crudo de la fuente, y se ha verificado que falla si se reintroduce la conversión anterior.

Un día no siempre tiene 24 horas. El domingo que adelanta tiene 23 y el que atrasa tiene 25. Validar contra 24 fijo marca como incompletos dos días al año que están perfectos.

Proyección de particiones en vez de crawler. Un crawler de Glue programado factura DPU cada vez que se ejecuta, lo mire alguien o no. La proyección deduce las particiones del patrón de la ruta: coste cero y nada que mantener.

Stack

Con qué está construido

TerraformAWS LambdaAmazon S3 AWS GluePySparkGlue Data Catalog Amazon AthenaStep FunctionsCloudWatch GitHub ActionsPythonSQL

Fuentes de datos: Red Eléctrica (demanda y precio horarios) y Open-Meteo (temperatura horaria). Ambas públicas y sin token. Región eu-west-1.