, decidí que quería pasar de analista de datos a ingeniero de datos.
Como muchas personas que empiezan, me sentí abrumado por la gran cantidad de cosas que pensé que necesitaba aprender. Almacenes de datos, herramientas de orquestación, procesamiento distribuido, sistemas de streaming, plataformas en la nube, infraestructura. La lista parecía interminable.
En lugar de intentar aprender todo de una vez, adopté un enfoque diferente.
Creé una hoja de ruta de autoaprendizaje de 12 meses basada en una idea simple: aprender construyendo.
En lugar de saltar de un tutorial a otro, crearía una serie de pequeños proyectos. Cada proyecto introduciría algunos conceptos nuevos y al mismo tiempo reforzaría lo que había aprendido antes. El objetivo no era crear las aplicaciones más sofisticadas posibles. Se trataba de desarrollar el hábito de pensar como un ingeniero resolviendo un problema a la vez.
El primer proyecto en ese viaje fue una canalización ETL de GitHub. Comenzó como un simple script de Python que obtenía datos del repositorio y los exportaba a un archivo CSV. A medida que aprendí más, lo mejoré gradualmente. Reemplacé los archivos CSV con SQLite, hice que la canalización fuera idempotente para evitar datos duplicados y, finalmente, la automaticé con GitHub Actions.
Cuando terminé, me di cuenta de algo que no había sido obvio cuando comencé.
En realidad, construir la lógica ETL fue la parte fácil.
Las preguntas más difíciles aparecieron una vez que dejé de pensar en un script que se ejecuta una vez y comencé a pensar en un sistema que necesita ejecutarse una y otra vez sin que yo lo vea.
¿Cómo se debe programar? ¿Qué pasa si falla a la mitad? ¿Dónde debería residir la lógica de reintento? ¿Cómo se empaqueta la aplicación para que se ejecute de la misma manera en todas partes?
Esas preguntas me enseñaron mucho más sobre ingeniería de datos que analizar JSON o escribir SQL.
Ese primer proyecto me dejó otro desafío.
GitHub Actions funcionó bien para automatizar una pequeña canalización ETL, pero quería entender qué cambia cuando se utiliza un orquestador de flujo de trabajo creado específicamente para ingeniería de datos. Quería aprender cómo los ingenieros separan la orquestación de la ejecución, cómo las cargas de trabajo en contenedores encajan en esa imagen y cómo se ve realmente una canalización orientada a la producción, incluso a pequeña escala.
Entonces, para mi segundo proyecto, decidí crear un canal de ingesta de RSS automatizado.
A primera vista, es una aplicación bastante simple. Obtiene artículos de una fuente RSS, los analiza en objetos estructurados y los almacena en PostgreSQL.
Pero el objetivo nunca fue crear un lector de RSS.
El objetivo era explorar las decisiones de ingeniería que transforman un script de Python en una canalización de datos confiable.
En este artículo, analizaré esas decisiones, los errores que cometí en el camino y las lecciones que aprendí al construir mi primer canal con Kestra.
¿Por qué construir otro canal ETL?
Después de terminar mi primer proyecto ETL, pensé en pasar a algo completamente diferente.
Quizás un proyecto de almacén de datos. Quizás Apache Spark. Quizás una API con una capa de transformación más compleja.
En lugar de eso, construí… otro canal ETL.
Al principio esto probablemente parezca un paso atrás.
Después de todo, ya había creado una canalización de extracción, la hice idempotente y la programé con GitHub Actions. ¿Por qué repetir el mismo ejercicio?
Porque no estaba intentando aprender un nuevo conjunto de datos. Estaba tratando de aprender una nueva forma de pensar.
Una lección de mi primer proyecto se me quedó grabada.
Escribir la lógica ETL no fue la parte difícil. Lo difícil fue todo lo que lo rodeaba.
¿Cómo se debe ejecutar el oleoducto? ¿Cómo debería recuperarse de los fracasos? ¿Cómo se empaqueta para que se ejecute de manera consistente en cualquier máquina? ¿A dónde pertenece la programación?
Y quizás la pregunta más importante de todas:
¿Dónde deberían terminar las responsabilidades de la aplicación y dónde deberían comenzar las responsabilidades de la capa de orquestación?
Esas preguntas no tienen mucho que ver con los canales RSS o los repositorios de GitHub. Son preguntas de ingeniería y me di cuenta de que podía explorarlas con casi cualquier fuente de datos.
Por eso elegí una fuente RSS.
No porque RSS sea particularmente interesante, sino porque es intencionalmente simple.
La lógica de extracción sólo requiere unas pocas líneas de Python. Eso significaba que podía dedicar menos tiempo a preocuparme por la lógica empresarial y más a pensar en la arquitectura.
Por la misma razón, decidí alejarme de GitHub Actions para este proyecto.
GitHub Actions fue una excelente introducción a la programación. Me mostró cómo automatizar un flujo de trabajo y me dio la primera oportunidad de ejecutar una canalización ETL sin intervención manual.
Pero GitHub Actions no está diseñado específicamente para orquestar flujos de trabajo de datos.
Quería entender qué cambia cuando se utiliza una herramienta creada teniendo en cuenta las canalizaciones de datos.
Eso es lo que me llevó a Kestra.
En lugar de preguntar: "¿Cómo ejecuto este script de Python cada hora?", Me encontré haciendo un conjunto diferente de preguntas.
¿Cómo se deben configurar los reintentos? ¿Cómo se realiza el seguimiento de las ejecuciones del flujo de trabajo? ¿Cómo se deben pasar las variables de entorno a un contenedor? ¿Cómo se ve una ejecución fallida? ¿Cómo se separa la aplicación de la infraestructura que la ejecuta?
Esas preguntas eran exactamente lo que quería explorar.
Al elegir una canalización ETL simple, pude concentrarme en las decisiones de ingeniería en lugar de distraerme con una lógica empresarial complicada.
Mirando hacia atrás, creo que fue la decisión correcta.
Este proyecto no es interesante porque procesa canales RSS.
Es interesante porque construirlo me obligó a pensar en la confiabilidad, la repetibilidad y la orquestación de una manera que mi primer proyecto nunca lo hizo.
La primera decisión arquitectónica: Docker antes que Kestra
Una vez definido el proyecto, mi primer instinto fue lanzarme directamente a Kestra.
Después de todo, la orquestación fue una de las principales razones por las que elegí este proyecto. ¿Por qué no empezar por ahí?
En cambio, hice algo que me ahorró mucha frustración más adelante.
Ignoré a Kestra por completo.
Esto puede parecer contradictorio, pero quería evitar introducir múltiples partes móviles antes de saber que la aplicación principal realmente funcionaba.
Entonces construí el proyecto en capas.
Primero, escribí el ETL de Python.
Su trabajo fue intencionalmente pequeño. Obtenga la fuente RSS, analice cada entrada en un objeto Artículo y guarde los resultados en PostgreSQL. Nada más.
feed = feedparser.parse(RSS_URL) artículos = parse_feed(feed) save_articles(artículos)
Eso es realmente todo lo que hizo la ETL. Intencionalmente mantuve la aplicación pequeña porque el enfoque de este proyecto no era la lógica de transformación. Fue todo lo que pasó a su alrededor.
Una vez que funcionó de manera confiable, centré mi atención en la base de datos.
Quería que las ejecuciones repetidas fueran seguras, así que hice las inserciones idempotentes usando ON CONFLICT DO NOTHING de PostgreSQL. De esa manera, ejecutar la canalización varias veces no crearía filas duplicadas.
INSERTAR EN artículos (…) VALORES (…) SOBRE EL CONFLICTO (id) NO HACER NADA;
Esta línea hizo que el oleoducto fuera seguro para ejecutarse repetidamente. Ya sea que Kestra ejecutara el flujo de trabajo una o cien veces, PostgreSQL se hizo responsable de evitar registros duplicados.
Solo después de que el ETL y la base de datos trabajaron juntos, presenté Docker.
Esa decisión cambió mi forma de pensar sobre el proyecto.
Al principio, Docker me pareció otra herramienta que necesitaba aprender.
Al final del proyecto, me di cuenta de que se había convertido en algo mucho más importante.
Se convirtió en la unidad de ejecución.
DESDE python:3.13-slim WORKDIR /app COPIAR requisitos.txt. EJECUTAR pip install –no-cache-dir -r requisitos.txt COPIAR. . CMD ["python", "fetch_rss.py"]
Empaquetar el ETL de esta manera significó que la aplicación se volviera autónoma. En lugar de pedirle a Kestra que comprenda mi proyecto Python, simplemente podría pedirle que ejecute un contenedor que ya sepa cómo ejecutar la canalización.
En lugar de pensar: "Kestra ejecutará mi script de Python", comencé a pensar: "Kestra ejecutará mi imagen de Docker".
Esa distinción puede parecer sutil, pero cambia por completo la relación entre su aplicación y su capa de orquestación.
Una vez que el ETL se empaquetó en un contenedor, ya no importaba si se ejecutaba en mi computadora portátil, dentro de Kestra o en otra máquina completamente.
El entorno de ejecución era siempre el mismo.
Esa consistencia me dio algo que no tenía antes: confianza.
Antes de presentar Kestra, podía ejecutar el contenedor manualmente y verificar que obtuviera la fuente RSS, se conectara a PostgreSQL y conservara los registros esperados.
Si algo fallaba, sabía que el problema no estaba oculto detrás de otra capa de orquestación.
Ese paso de validación resultó ser increíblemente valioso más tarde.
Durante el desarrollo, tuve problemas con las redes, la configuración de contenedores y la ejecución del flujo de trabajo. Como la imagen de Docker ya se había validado de forma independiente, pude descartar inmediatamente el ETL y centrarme en la capa de orquestación.
Eso facilitó enormemente la depuración.
Mirando hacia atrás, creo que esta fue una de las mayores lecciones del proyecto.
Es tentador conectar todos los componentes lo más rápido posible y esperar que todo funcione.
Un mejor enfoque es validar cada capa antes de introducir la siguiente.
En este proyecto, el orden era el siguiente:
Valide el ETL de Python. Validar la persistencia de PostgreSQL. Valide la imagen de Docker. Finalmente, deje que Kestra organice un contenedor en el que ya confiaba.
Cada capa construida sobre la anterior.
Cuando Kestra entró en escena, no estaba intentando depurar Python, PostgreSQL, Docker y la orquestación al mismo tiempo.
Sólo estaba resolviendo un problema.
Ese enfoque incremental hizo que todo el proyecto pareciera mucho más manejable y es un flujo de trabajo que probablemente continuaré usando en futuros proyectos de ingeniería de datos.
La suposición que resultó ser errónea
Con una imagen de Docker funcional, estaba convencido de que la parte difícil había terminado.
Tenía un ETL de Python que funcionó. Tenía PostgreSQL ejecutándose en Docker. Verifiqué que el contenedor podía recuperar artículos RSS y guardarlos en la base de datos.
Ahora todo lo que Kestra tenía que hacer era ejecutarlo.
O eso pensé.
Mi suposición original era simple.
Kestra señalaría mis archivos Python, ejecutaría el script y todo funcionaría exactamente como lo hizo desde la línea de comando.
No fue así.
Rápidamente descubrí que había una distinción importante que no había apreciado del todo.
Kestra es un orquestador.
No es responsable de crear entornos Python ni de gestionar las dependencias de las aplicaciones. Su responsabilidad es decidir cuándo y cómo se deben ejecutar las cargas de trabajo.
Esa comprensión cambió la dirección del proyecto.
En lugar de tratar a Kestra como otro lugar para ejecutar código Python, comencé a tratarlo como la capa responsable de orquestar una carga de trabajo que ya existía.
Esa carga de trabajo era mi imagen de Docker.
Una vez que hice ese cambio mental, la arquitectura se volvió mucho más limpia.
La ETL se convirtió en una aplicación autónoma. Docker se convirtió en el artefacto de implementación. Kestra se convirtió en el orquestador.
Cada capa tenía una responsabilidad clara y ninguna necesitaba saber cómo trabajaban las demás internamente.
Curiosamente, llegar a ese punto no fue del todo sencillo.
Mi primer instinto fue encontrar una manera para que Kestra ejecutara el proyecto Python directamente. Ese enfoque parecía más simple, pero cuanto más experimentaba con él, más me daba cuenta de que le estaba pidiendo al orquestador que asumiera la responsabilidad de algo que la aplicación ya debería proporcionar.
Una vez que adopté Docker como artefacto de ejecución, el flujo de trabajo se volvió mucho más simple.
Algunos enfoques parecían prometedores al principio, pero introdujeron una complejidad innecesaria. Otros funcionaron, pero no se alinearon con la forma en que Kestra fomenta la ejecución de cargas de trabajo en contenedores.
Finalmente, encontré un flujo de trabajo que me pareció sorprendentemente simple.
En lugar de intentar enseñarle a Kestra cómo ejecutar Python, dejo que Kestra haga lo que mejor sabe hacer.
Lanza un contenedor.
tareas: – id: run_etl tipo: io.kestra.plugin.scripts.shell.Commands contenedorImage: rss-pipeline-etl:latest taskRunner: tipo: io.kestra.plugin.scripts.runner.docker.Docker comandos: – python /app/fetch_rss.py
Al observar el flujo de trabajo ahora, lo que destaca no es la cantidad de YAML que contiene. Es lo poco que Kestra necesita saber sobre mi solicitud. Su única responsabilidad es lanzar un contenedor que ya sepa ejecutar el ETL.
Dentro de ese contenedor, mi aplicación ya sabe exactamente qué hacer.
Ese pequeño cambio arquitectónico resolvió algo más que el problema de ejecución inmediata.
También reforzó una idea que se está convirtiendo en un tema recurrente en mi viaje de aprendizaje.
La buena ingeniería a menudo no se trata de agregar otra capa.
Se trata de darle a cada capa una única responsabilidad y permitirle hacer bien ese trabajo.
Python no debería preocuparse por la orquestación. Kestra no debería preocuparse por las dependencias de Python. Docker no debería saber nada sobre las fuentes RSS.
Cada componente resuelve un problema diferente.
Una vez que dejé de pedirle a una herramienta que resolviera todos los problemas, fue mucho más fácil razonar sobre todo el sistema.
Mirando hacia atrás, este fue probablemente el mayor cambio de mentalidad de todo el proyecto.
No solo aprendí a usar Kestra.
Aprendí lo que realmente significa orquestación.
Una vez que una canalización se ejecuta automáticamente, todo cambia
Hasta este punto, había estado ejecutando la canalización manualmente.
Si algo fallaba, me sentaba frente a mi computadora. Podría leer el error, hacer un cambio y volver a intentarlo.
Esa red de seguridad desaparece en el momento en que un oleoducto comienza a funcionar por sí solo.
Una de las primeras cosas que configuré en Kestra fue un horario simple.
desencadenantes: – id: tipo de programación horaria: io.kestra.plugin.core.trigger.Schedule cron: "0 * * * *"
Sobre el papel, era sólo una expresión cron.
En la práctica, representó un cambio mucho mayor.
El oleoducto ya no dependía de que yo recordara ejecutarlo.
Cada hora, Kestra iniciaba una nueva ejecución, lanzaba el contenedor ETL y procesaba los últimos artículos del feed RSS.
Esto inmediatamente planteó otra pregunta.
¿Qué pasa si una de esas ejecuciones falla?
Durante el desarrollo, introduje deliberadamente fallas para responder esa pregunta. Apunté la canalización a un host de base de datos no válido y observé lo que sucedió.
La primera ejecución falló, exactamente como se esperaba.
Más importante aún, la cosa no se detuvo allí.
Debido a que el flujo de trabajo se configuró con reintentos, Kestra intentó automáticamente la ejecución nuevamente después de un breve retraso.
reintento: tipo: constante maxAttempts: 3 intervalo: PT30S
Ese fue uno de mis momentos favoritos del proyecto.
Una vez que corregí la configuración, el flujo de trabajo se completó correctamente sin necesidad de realizar ningún cambio en la aplicación.
Ese fue uno de mis momentos favoritos del proyecto.
No porque los reintentos sean particularmente complicados, sino porque pusieron de relieve otra separación de responsabilidades.
La ETL no debería decidir si merece otra oportunidad.
Esa es una preocupación de orquestación.
Al trasladar la lógica de reintento a Kestra, la aplicación Python permaneció enfocada en una única responsabilidad: procesar el feed y conservar los resultados.
La capa de orquestación manejó la resiliencia.
La programación introdujo otro desafío que ya había encontrado en mi primer proyecto ETL.
Las ejecuciones repetidas significan intentos repetidos de escribir datos.
Si el mismo artículo RSS aparece en varias ejecuciones cada hora, la canalización no debería insertarlo dos veces.
Afortunadamente, ya había resuelto un problema similar antes.
La capa de base de datos fue diseñada para ser idempotente utilizando ON CONFLICT DO NOTHING de PostgreSQL.
INSERTAR EN artículos (…) VALORES (…) SOBRE EL CONFLICTO (id) NO HACER NADA;
Eso significaba que cada ejecución podía intentar insertar de forma segura los mismos registros sin crear duplicados.
La combinación de programación, reintentos y escrituras idempotentes hizo que la canalización fuera mucho más indulgente.
Si fallaba una ejecución, Kestra podía volver a intentarla.
Si un reintento exitoso encontrara datos que ya habían sido escritos, PostgreSQL simplemente ignoraría los duplicados.
Ninguna capa necesitaba saber lo que estaba haciendo la otra.
Cada uno asumía su propia responsabilidad.
La última mejora fue la visibilidad.
Al principio del desarrollo, mis registros se veían exactamente como se esperaría de un proyecto que aún se estaba depurando.
Imprimí objetos RSS completos en la consola solo para asegurarme de que el analizador estuviera funcionando.
No fue bonito, pero cumplió su propósito.
A medida que la canalización se volvió más estable, esas declaraciones de depuración se volvieron menos útiles.
Los reemplacé con registros que describían la ejecución en lugar de desechar datos sin procesar.
Antes
print(feed.entradas[0])
Después
print("=== RSS PIPELINE START ===") primero = feed.entries[0]print("Vista previa de la primera entrada:") print(f"Título: {first.get('title')}") print(f"Enlace: {first.get('link')}") print(f"Título del feed: {feed.feed.title}") print(f"Artículos recuperados: {len(articles)}") print(f"{len(articles)} artículos guardados en la base de datos.") print("=== RSS PIPELINE END ===")
Cada ejecución ahora cuenta una historia simple.
=== RSS PIPELINE START === Vista previa de la primera entrada: Título: Christian Ledermann: Migrar de mypy a ty y pyrefly Enlace: https://dev.to/… Título del feed: Planet Python Artículos recuperados: 25 Se guardaron 25 artículos en la base de datos. === FIN DEL TUBO RSS ===
Esos cambios no hicieron que la aplicación fuera más inteligente. Lo hicieron más fácil de entender. Y esa es una distinción importante.
Una buena observabilidad no se trata de producir más registros. Se trata de producir los troncos adecuados.
Al final del proyecto, me di cuenta de algo interesante. El código Python no había crecido dramáticamente. La mayor parte del trabajo se había realizado en torno a él.
Mirando hacia atrás, la mayor parte del esfuerzo de ingeniería no se dedicó a hacer que el ETL fuera más inteligente. Se gastó haciéndolo más confiable.
La programación aseguró que funcionara sin mí. Los reintentos lo ayudaron a recuperarse de fallas transitorias. La idempotencia protegió la base de datos de escrituras duplicadas. El registro hizo que cada ejecución fuera más fácil de entender.
Individualmente, ninguno de estos cambios fue particularmente complejo. Juntos, transformaron un simple script de Python en algo que se comportaba mucho más como un sistema de producción.
La arquitectura final
Al final del proyecto, la arquitectura se había asentado en algo que parecía sorprendentemente simple.
Al observar la arquitectura final, es tentador pensar que el proyecto siempre estuvo encaminado en esta dirección.
No lo fue.
Cada capa se añadió sólo después de que la anterior hubiera sido validada.
La ETL fue lo primero. Luego PostgreSQL. Luego Docker. Por último, Kestra.
Ese orden importaba.
Debido a que cada componente ya había sido probado de forma independiente, nunca me encontré depurando Python, Docker, PostgreSQL y Kestra al mismo tiempo. Cada decisión redujo el número de incógnitas en lugar de aumentarlas.
Más importante aún, cada componente terminó con una responsabilidad clara.
Python sabe cómo buscar, analizar y conservar artículos RSS. PostgreSQL sabe cómo almacenar datos de forma segura y evitar duplicados. Docker proporciona un entorno de ejecución consistente. Kestra decide cuándo debe ejecutarse la carga de trabajo y qué debe suceder si falla.
Ninguno de esos componentes intenta hacer el trabajo del otro.
Irónicamente, eso es lo que hizo que el sistema terminado pareciera mucho más simple de lo que esperaba.
Lo que este proyecto cambió en mi forma de pensar
Cuando comencé a aprender ingeniería de datos, asumí que la parte difícil sería escribir código ETL.
En eso se centran la mayoría de los tutoriales para principiantes.
Aprenderá a llamar a una API. Transformas los datos. Lo guardas en alguna parte.
Repetir.
Esas son habilidades valiosas, pero son sólo una parte del panorama.
Este proyecto me enseñó que la ingeniería comienza después de que funciona el guión.
Una vez que se espera que una tubería funcione cada hora, sobreviva fallas transitorias, evite datos duplicados y produzca registros que expliquen lo sucedido, las preguntas se vuelven mucho más interesantes.
Ya no piensas en funciones individuales. Estás pensando en sistemas.
Uno de los mayores cambios de mentalidad para mí fue comprender la diferencia entre ejecución y orquestación.
Al principio, esas ideas parecían casi intercambiables. Ahora se sienten completamente separados.
La aplicación Python debe centrarse en la lógica empresarial. El orquestador debe centrarse en cuándo, dónde y cómo se ejecuta esa aplicación.
Mantener esas responsabilidades separadas hizo que fuera más fácil razonar sobre todo el proyecto.
También cambió mi forma de pensar sobre Docker.
Antes de este proyecto, veía a Docker principalmente como una forma de empaquetar aplicaciones.
Ahora lo veo como un artefacto de implementación.
Una vez que el ETL se empaquetó en un contenedor y se validó de forma independiente, pude dejar de preocuparme por si se comportaría de manera diferente dentro de Kestra.
Esa confianza resultó ser una de las mayores ventajas de contener la aplicación.
Quizás la lección más importante, sin embargo, no tuvo nada que ver con Kestra o Docker. Era el valor de construir incrementalmente.
Cada decisión importante siguió el mismo patrón.
Construye lo más pequeño que funcione. Validarlo. Solo entonces introduce la siguiente capa.
Ese enfoque hizo que el proyecto pareciera mucho menos abrumador que tratar de conectar todo desde el principio.
Mirando hacia atrás, creo que es una lección que llevaré a todos los proyectos futuros, independientemente de la tecnología.
Mirando hacia el futuro
Este canal RSS es solo el segundo proyecto en mi viaje de aprendizaje en ingeniería de datos. En comparación con mi primer canal ETL, el código en sí no es mucho más complejo. Lo que cambió fue la forma en que abordé el problema.
En lugar de preguntar "¿Cómo escribo este guión?", Me encontré haciendo preguntas como:
¿Dónde debería residir esta responsabilidad? ¿Qué pasa si falla? ¿Puedo ejecutarlo repetidamente sin preocuparme por datos duplicados? ¿Puedo confiar en que se ejecutará cuando no esté mirando?
Esas preguntas me llevaron a pensar menos como alguien que escribe código Python y más como alguien que diseña un sistema. Y sospecho que ese es el valor real de los proyectos de construcción.
Cada proyecto enseña una nueva herramienta. Pero los mejores proyectos cambian lentamente tu forma de pensar. Éste ciertamente lo hizo.
Estoy seguro de que el próximo proyecto desafiará un conjunto de suposiciones completamente diferente. Sinceramente, estoy deseando saber cuáles son.
Esto es parte de mi serie continua que documenta mi transición de analista de sistemas a ingeniero de datos. Si me has seguido, gracias.
Conéctese conmigo en LinkedIn, YouTube y Twitter.