Saltar a contenido

Changelog:

  • Nueva creación — Julio 2026

Stack Docker para Ingeniería de datos

A lo largo del bloque de ingeniería de datos vamos a necesitar bastantes piezas: un sistema de ficheros distribuido y un almacén de objetos donde dejar los datos, un catálogo que sepa qué tablas hay y dónde viven, uno o varios motores para consultarlos, y un orquestador que lo coordine todo. En vez de instalar cada herramienta por separado, las levantamos mediante Docker Compose, de manera que con un único comando tengamos el entorno completo y reproducible.

La clave de este montaje es que nada arranca por defecto: todos los servicios están detrás de profiles. Encendemos sólo las piezas que necesitamos en cada sesión, lo que ahorra memoria y evita levantar Hadoop entero cuando sólo queremos trastear con DuckDB.

Un stack docente, no de producción

Este montaje prioriza que las cosas funcionen a la primera en el aula. Hemos desactivado los permisos de HDFS, los servicios de Hadoop corren como root y ni JupyterLab ni MinIO piden credenciales robustas. En un entorno real ninguna de esas decisiones sería aceptable, pero aquí nos ahorran fricción para centrarnos en lo que nos interesa.

Arquitectura

El stack se organiza en capas. Arriba, la orquestación con Airflow; debajo, los motores que ejecutan nuestro trabajo; después el catálogo y el origen de datos; luego el almacenamiento, que aquí tiene dos sabores (HDFS y MinIO); y por último el cómputo de YARN. Aparte quedan varios contenedores que se ejecutan una sola vez para dejar el entorno preparado.

Arquitectura del stack Docker de ingeniería de datos
Las cuatro capas del stack, con HDFS y MinIO como almacenamiento y Airflow orquestando el ELT

Si nos fijamos en las flechas, hay tres tipos de tráfico y conviene tenerlos claros desde el principio:

  • Las flechas oscuras son metadatos: Trino le pregunta al metastore qué columnas tiene una tabla y en qué ruta están sus ficheros.
  • Las flechas azules son datos: una vez conocida la ruta, el motor va directamente a por los ficheros al datanode o a MinIO.
  • Las flechas verdes son orquestación: Airflow dispara la tubería dlt → dbt que se ejecuta en el nodo lab.

Esta separación entre dónde está el dato y el dato en sí es la idea central de un lago de datos, y la volveremos a encontrar en Athena sobre S3 con el catálogo de Glue.

Puesta en marcha

Necesitaremos Docker con Docker Compose v2 y unos 8 GB de memoria disponibles.

Este stack tiene bastantes servicios y rara vez los necesitamos todos a la vez: en la sesión de motores de consulta nos interesan HDFS, Hive y Trino, mientras que en las de ingesta y transformación sólo hacen falta MinIO y el nodo de DuckDB/dlt/dbt. Para no arrancar de más, todos los servicios están detrás de profiles de Compose. Esto tiene una consecuencia importante: un docker compose up a secas no arranca nada; hay que indicarle qué profile queremos.

Por ejemplo, para arrancar el stack completo haríamos:

docker-compose --profile all up -d

Profiles disponibles

Hay dos niveles. Los profiles de componente encienden una pieza concreta del stack:

Profile Servicios Para qué
hadoop namenode, datanode, resourcemanager, nodemanager, hdfs-init HDFS + YARN
minio minio, minio-init Object storage (el lago)
hive mysql-metastore, hive-metastore, hiveserver2 Catálogo de Hive
trino trino Motor de consulta sobre el lago
lab mysql-retail, lab, retail-init DuckDB + dlt + dbt (y retail_db)
airflow postgres-airflow, airflow-init, airflow-webserver, airflow-scheduler Orquestación del ELT

Por encima hay tres profiles paraguas que combinan los anteriores, para no tener que recordarlos sesión a sesión:

Profile Equivale a Para qué
lake hadoop + minio + hive + trino Consultar el lago (sesiones 3 a 7)
elt minio + lab + airflow Ingesta, dbt y orquestación (sesiones 8 a 11)
all todo el stack Levantarlo entero

A continuación se muestra un diagrama de cómo se relacionan los profiles de componente y los profiles paraguas, y qué servicios comparten:

Relación entre profiles y contenedores
Profiles de componente, profiles paraguas y servicios compartidos

Las fuentes viajan con lake

Además de los cuatro componentes, lake arranca también mysql-retail y retail-init. El motivo es que en la sesión de motores de consulta Trino federa contra retail_db mediante su conector MySQL, así que la fuente tiene que estar disponible. El nodo lab en sí, en cambio, no forma parte de lake.

Arrancar según la sesión

Así pues, el primer paso es elegir qué profile queremos levantar según la sesión que vayamos a trabajar:

# Ingesta, dbt, ELT y orquestación (sesiones 8 a 11): MinIO + nodo lab + Airflow
docker-compose --profile elt up -d

# Consultar el lago (sesiones 3 a 7): HDFS + Hive + Trino
docker-compose --profile lake up -d

# Todo el stack
docker-compose --profile all up -d

# Estado y logs
docker compose ps
docker compose logs -f hive-metastore trino

Los profiles son combinables. Por ejemplo, en la sesión de motores de consulta, para comparar Trino y DuckDB sobre el mismo lago, añadimos el nodo lab al escenario lake:

docker-compose --profile lake, lab up -d

El arranque está encadenado mediante healthcheck y depends_on, de modo que cada servicio espera a que el anterior esté realmente listo. En el escenario lake el orden es namenode → datanode → hdfs-init → hive-metastore → hiveserver2 → trino; en elt, minio y mysql → retail-init → lab.

Los init acaban en exited

Los contenedores hdfs-init, minio-init, retail-init y airflow-init hacen su trabajo y terminan. Verlos como exited (0) en docker compose ps es lo correcto, no es un error.

Para el día a día, stop/start en vez de down/up

Una vez construido el stack, para apagarlo y encenderlo entre sesiones usa docker compose stop y docker compose start. Reserva docker compose down para cuando quieras recrearlo de cero, porque elimina también la red y los contenedores.

Servicios y puertos

Una vez arrancado, según el profile activo tendremos disponibles algunas de estas interfaces:

Servicio Puerto Interfaz
namenode 9870 http://localhost:9870
datanode 9864 http://localhost:9864
resourcemanager 8088 http://localhost:8088
nodemanager 8042 http://localhost:8042
minio 9001 http://localhost:9001 (consola) · 9000 (API S3)
trino 8080 http://localhost:8080
lab 8888 http://localhost:8888
airflow-webserver 8082 http://localhost:8082
hiveserver2 10002 http://localhost:10002
hive-metastore 9083 Thrift
mysql-metastore 3306 MySQL (catálogo)
mysql-retail 3307 MySQL (origen retail_db)

Los contenedores

Vamos a recorrer cada pieza explicando qué hace y en qué sesión la utilizamos.

namenode y datanode

Forman HDFS. El namenode es el cerebro: no almacena ni un solo byte de nuestros ficheros, sino el árbol de directorios y, para cada fichero, la lista de bloques que lo componen y en qué nodos están. El datanode es donde sí viven los bloques, y los sirve directamente a quien se los pida.

Cuando un cliente quiere leer un fichero, el namenode le responde con la ubicación de los bloques, y a partir de ahí el cliente habla directamente con el datanode. Este detalle, que parece menor, explica buena parte del comportamiento de HDFS. Hemos fijado la replicación a 1 porque sólo tenemos un datanode; en un clúster real el valor por defecto es 3.

resourcemanager y nodemanager

Forman YARN, el gestor de recursos del ecosistema Hadoop. El resourcemanager decide qué trabajos se ejecutan y con cuántos recursos; el nodemanager es quien realmente los lanza.

docker compose exec -it resourcemanager \
  hadoop jar /opt/hadoop/share/hadoop/mapreduce/hadoop-mapreduce-examples-*.jar pi 2 10

¿Para qué queremos YARN si vamos a usar Trino?

Para nada imprescindible, y de hecho Trino no lo usa. Lo mantenemos porque es la forma más directa de ver cómo se planifica y ejecuta un trabajo distribuido en el ecosistema Hadoop clásico, que es justo lo que estudiamos en la sesión de Hadoop.

minio y minio-init

MinIO es un almacén de objetos compatible con la API de S3: nuestro lago de datos "moderno", la alternativa a HDFS. Todo lo que aprendimos sobre S3 aplica aquí, pero corriendo en local. Ofrece una consola web en el puerto 9001 y la API S3 en el 9000.

El contenedor de un solo uso minio-init crea los buckets raw-data, processed y warehouse, que es la separación por capas habitual en un lago (datos crudos, procesados y servidos).

HDFS y MinIO conviven a propósito

Tener los dos almacenamientos nos permite contrastar el enfoque clásico (HDFS, con el cómputo cerca del dato) frente al moderno (object storage, con cómputo y almacenamiento separados). Trino puede consultar tablas que viven en cualquiera de los dos.

mysql-metastore y mysql-retail

Tenemos dos contenedores MySQL, y es importante entender por qué están separados, porque cumplen papeles que no tienen nada que ver:

  • mysql-metastore aloja la base de datos metastore, donde Hive guarda su catálogo. Aquí sólo hay metadatos: qué tablas existen, qué columnas tienen y en qué ruta viven sus ficheros. Ni un solo dato de negocio.
  • mysql-retail aloja la base de datos retail_db, que hace de origen operacional. Es la base de datos transaccional de la que dlt extrae los datos, igual que haríamos en un caso real.

Podríamos haberlo montado todo en un único MySQL, y de hecho es tentador porque son la misma tecnología. Pero mezclar el catálogo con el dato de origen confunde dos conceptos que conviene tener bien diferenciados: una cosa es dónde apuntan las tablas y otra muy distinta de dónde vienen los datos. Separarlos también hace que cada profile sea preciso: mysql-metastore sólo aparece donde hay catálogo (hive, lake), y mysql-retail sólo donde se necesita el origen (lab, elt, airflow, lake).

hive-metastore

Es el catálogo. Guarda, para cada tabla, su esquema de columnas, el formato de los ficheros y la ruta donde se encuentran. Lo interesante es que no almacena datos: sólo apunta a ellos, tanto si viven en HDFS como en MinIO.

Su valor está en que es un catálogo compartido. Una tabla creada desde Trino es inmediatamente visible desde HiveServer2, y lo mismo ocurriría con Spark, tal y como veremos en la sesión de conectividad y catálogo.

El driver JDBC no viene incluido

La imagen oficial de Hive no incorpora el conector de MySQL. Por eso el stack construye una imagen propia que añade mysql-connector-j a /opt/hive/lib. Si alguna vez montas un metastore desde cero y ves errores de driver no encontrado, ya sabes por dónde van los tiros.

hiveserver2

Nos permite lanzar HiveQL mediante JDBC, típicamente con beeline. Es la puerta de entrada "clásica" al catálogo y la usamos en las sesiones de Hive I y Hive II.

docker compose exec -it hiveserver2 beeline -u "jdbc:hive2://localhost:10000/"
SHOW DATABASES;
SELECT count(*) FROM retail.orders;

trino

Trino es un motor de consultas SQL distribuido pensado para el lago de datos. Se conecta al metastore para saber qué tablas hay y dónde están, y después lee los ficheros directamente de HDFS o MinIO. Su gran virtud es la federación: puede consultar en la misma sentencia datos que viven en sistemas distintos, incluido retail_db en MySQL.

docker compose exec -it trino trino
CREATE SCHEMA hive.demo
  WITH (location = 'hdfs://namenode:8020/user/hive/warehouse/demo.db');

CREATE TABLE hive.demo.nation WITH (format = 'PARQUET') AS
  SELECT * FROM tpch.tiny.nation;

SELECT count(*) FROM hive.demo.nation;

Con estas tres sentencias hemos hecho el recorrido completo: Trino ha escrito ficheros parquet, ha registrado la tabla en el metastore de Hive y ha vuelto a leerla.

lab

Es nuestro nodo de trabajo, y probablemente el que más usaremos. Contiene las tres piezas de un flujo ELT moderno:

  • dlt para la extracción y carga (EL).
  • DuckDB como motor analítico donde aterrizan y se consultan los datos.
  • dbt para las transformaciones (T).

Incluye además boto3 y el CLI de AWS para hablar con MinIO, JupyterLab en http://localhost:8888 y el CLI de DuckDB, de modo que podemos trabajar tanto con cuadernos como con SQL puro.

Lo utilizamos en las sesiones de formatos de datos, SQL analítico con DuckDB, del HDFS al object storage y en las sesiones dedicadas a dbt.

Contenedores nativos en Apple Silicon

Las imágenes de Hadoop y Hive sólo se publican para amd64, así que en un Mac con chip M se ejecutan emuladas. Las de lab, mysql, minio, trino, Airflow y Postgres, en cambio, son multiarquitectura y corren de forma nativa.

airflow

Es nuestro orquestador. Mientras que en el nodo lab lanzamos el ELT a mano (primero dlt, luego dbt), Airflow nos permite encadenar esos pasos en un DAG, programarlos, reintentarlos si fallan y ver de un vistazo qué se ha ejecutado y con qué resultado. Lo trabajamos en las sesiones dedicadas a la orquestación del pipeline.

El profile airflow levanta cuatro contenedores:

Contenedor Función
postgres-airflow Backend de metadatos (Postgres 16)
airflow-init Migra la base de datos y crea el usuario admin
airflow-webserver Interfaz web en http://localhost:8082
airflow-scheduler Planificador, con LocalExecutor

La imagen de Airflow incorpora dlt y dbt (vía _PIP_ADDITIONAL_REQUIREMENTS), de modo que las tareas del DAG ejecutan exactamente el mismo pipeline que probamos en el lab. El usuario administrador se crea en el primer arranque con credenciales admin / admin.

Por qué un Postgres aparte para Airflow

Airflow guarda su propio estado (ejecuciones, tareas, conexiones) en una base de datos que consulta y actualiza constantemente. Le damos un Postgres dedicado en lugar de reutilizar el mysql del metastore para no mezclar el estado del orquestador con nuestros datos, igual que haríamos en producción.

hdfs-init, minio-init, retail-init y airflow-init

Son contenedores de un solo uso. hdfs-init crea en HDFS los directorios /user/hive/warehouse y /tmp con los permisos adecuados; minio-init crea en MinIO los tres buckets del lago; retail-init carga la base de datos retail_db en MySQL; y airflow-init migra la base de metadatos de Airflow y crea el usuario admin.

Este patrón, un contenedor que prepara el entorno y termina, es muy habitual y evita tener que documentar pasos manuales que alguien acabará olvidando.

El recorrido ELT completo

Con el profile elt levantado, podemos recorrer las tres letras de ELT de principio a fin.

COMPOSE_PROFILES=elt docker compose up -d

# EL: dlt replica mysql-retail -> DuckDB, en el esquema retail_raw
docker compose exec lab python /workspace/dlt/mysql_to_duckdb.py

# T: dbt transforma, de staging a marts
docker compose exec -w /workspace/dbt/retail lab dbt run

# Consultamos el resultado
docker compose exec lab duckdb /workspace/data/retail.duckdb \
  -c "SELECT * FROM main_marts.ventas_por_ciudad;"

Fijémonos en el reparto de responsabilidades: dlt no transforma nada, se limita a traer los datos tal cual están en origen. Y dbt no sabe nada de extracción, sólo transforma lo que ya está cargado. Esa separación es precisamente lo que distingue ELT de ETL, concepto que introducimos en la sesión de ingesta de datos. Una vez que el recorrido funciona a mano, es cuando lo llevamos a un DAG de Airflow para automatizarlo.

Referencias