Jenkins no és (només) una eina de CI/CD
Un controlador, un exèrcit d'agents i més de 2.000 plugins per connectar-hi gairebé qualsevol cosa. Què és Jenkins de veritat, i quan té sentit fer-lo servir en lloc d'una altra eina.
·12 min de lectura
Quan algú explica Jenkins de pressa, sol dir «és l’eina que compila el codi quan fas push». És cert, però és una descripció tan curta que amaga el que fa que Jenkins segueixi viu vint anys després: no és una eina de CI/CD amb algun plugin a sobre, és un motor genèric de programació de tasques, amb un ecosistema de connectors tan gran que la gent el fa servir per gairebé qualsevol cosa que s’hagi d’executar sola, amb regularitat o en resposta a un esdeveniment. Que la majoria de casos siguin «compilar i desplegar codi» és estadística, no una limitació de l’eina.
Aquest article el faig servir per plantar Jenkins al costat de n8n, que és el «fil conductor» de la resta d’articles d’aquest blog. Val la pena distingir-los bé, perquè no competeixen pel mateix terreny: on n8n destaca connectant aplicacions i processos de negoci, Jenkins destaca executant codi de manera repetible, versionada i distribuïda. Un enginyer que sap Groovy i vol el pipeline dins del mateix repositori que el codi hi trobarà casa seva; algú que ha de connectar un CRM amb un full de càlcul sense escriure una línia de codi, no.
D’on ve
Jenkins va néixer com Hudson, un projecte que Kohsuke Kawaguchi va començar a Sun Microsystems l’estiu del 2004, amb la primera versió publicada el febrer del 2005. Quan Oracle va comprar Sun el 2010, va sorgir una disputa sobre com es gestionava el projecte i, sobretot, sobre qui tenia el control de la marca «Hudson». El gener del 2011, la comunitat va votar (amb més d’un 90 % a favor) separar-se’n i continuar sota un nom nou: Jenkins, amb llicència MIT. La primera versió amb aquest nom, la 1.396, va sortir l’11 de febrer del 2011. Kawaguchi hi va continuar, amb la majoria de la comunitat. Hudson es va quedar a Oracle, que el va cedir a la Fundació Eclipse, i avui és pràcticament un projecte inactiu.
El 2019, Jenkins es va incorporar a la Continuous Delivery Foundation, sota el paraigua de la Linux Foundation, i hi va «graduar-se» (el nivell més alt de maduresa del seu procés) l’agost del 2020. És a dir: no és el projecte d’una empresa, és un projecte de fundació amb governança oberta, i això explica en part per què segueix rebent actualitzacions després de dues dècades.
Com funciona per sota: controlador i agents
Jenkins no executa les tasques directament al mateix procés que serveix la interfície web. Es divideix en dues peces:
- El controlador: gestiona la configuració, els plugins, la interfície, i decideix quina tasca s’executa on i quan. No hauria d’executar feina pesant ell mateix.
- Els agents: màquines (físiques, virtuals o contenidors) que reben feina del controlador i l’executen. Cada agent té un nombre d’«executors», que és quantes tasques pot córrer alhora.
Aquesta separació és el que fa que Jenkins escali: pots tenir desenes d’agents especialitzats (un amb Windows per compilar una aplicació de escriptori, un altre amb GPU per entrenar un model, un altre efímer que neix i mor amb cada tasca dins d’un contenidor) i el controlador es limita a repartir feina i mantenir l’estat. També és el que li dona resiliència: un agent que peta no s’enduu el controlador amb ell, només la tasca que tenia assignada.
Pipeline com a codi, no com a clics
Fins al 2016, un job de Jenkins es configurava clicant en una interfície web i quedava emmagatzemat com XML al servidor. Amb Jenkins 2.0 va arribar el canvi real: el Pipeline as Code, la possibilitat de definir tot el flux (compilar, provar, desplegar) en un fitxer de text anomenat Jenkinsfile, escrit en una DSL basada en Groovy i guardat al mateix repositori que el codi. Això vol dir que el pipeline es revisa, es versiona i es fa code review igual que qualsevol altre canvi.
Hi ha dues sintaxis. La Scripted Pipeline és Groovy pur, flexible fins a l’extrem però amb una corba d’aprenentatge real. La Declarative Pipeline, que va arribar a versió estable el febrer del 2017, ofereix una estructura més tancada i llegible, pensada perquè no calgui saber Groovy per escriure un pipeline normal:
pipeline {
agent any
stages {
stage('Checkout') {
steps { checkout scm }
}
stage('Build') {
steps { sh 'npm install && npm run build' }
}
stage('Test') {
steps { sh 'npm test' }
}
stage('Deploy') {
when { branch 'main' }
steps { sh './deploy.sh' }
}
}
post {
failure {
mail to: 'equip@empresa.com', subject: 'Build trencat', body: "Revisa ${env.BUILD_URL}"
}
}
}
Cada stage és un pas visible al tauler (i a Blue Ocean, la interfície visual que Jenkins va estrenar el 2016 per fer aquests pipelines més llegibles d’un cop d’ull). El bloc post és el que en un context d’automatització de processos ja coneixes bé: què fer quan alguna cosa falla, no només quan tot va bé. El mateix principi que la gestió d’errors a n8n: un pipeline que no avisa quan peta és pitjor que no tenir-ne cap.
Més enllà de compilar codi: la part de dades
Aquí és on Jenkins deixa de ser «l’eina del departament de programari» i es converteix en una peça útil per a qualsevol automatització que necessiti executar-se amb regularitat, distribuir càrrega, o reintentar-se sola si falla. Un Jenkinsfile no sap ni li importa si el que executa és npm run build o un script de Python que carrega un Excel a una base de dades: per a Jenkins, totes dues coses són un pas dins d’un stage.
Un cas real: l’Excel de cada nit
Aquest patró apareix constantment en clients amb un sistema vell que ningú vol tocar: cada nit, algú (o algun procés del propi sistema legacy) deixa un Excel en una carpeta compartida amb l’activitat del dia. Algú l’ha de recollir, validar-lo, i deixar la base de dades «de veritat» (la que fan servir la resta d’aplicacions) al dia, sense duplicar files si el job es torna a executar, sense petar en silenci si el format canvia, i amb un rastre de què s’ha carregat i quan.
El Jenkinsfile orquestra els passos i decideix què fer quan un falla; la lògica de cada pas viu en scripts de Python normals i corrents, no en Groovy:
pipeline {
agent { label 'python' }
triggers {
cron('H 3 * * *')
}
environment {
DB_URL = credentials('db-connexio')
}
stages {
stage('Recull') {
steps {
sh 'sftp -b batch.txt legacy@servidor:/exports/ ./entrada/'
archiveArtifacts artifacts: 'entrada/*.xlsx', fingerprint: true
}
}
stage('Valida i carrega') {
steps {
catchError(buildResult: 'UNSTABLE', stageResult: 'UNSTABLE') {
sh 'python carrega.py entrada/*.xlsx --db $DB_URL --execucio $BUILD_NUMBER'
}
}
}
}
post {
failure {
mail to: 'dades@empresa.com', subject: "Càrrega nocturna fallida (#${env.BUILD_NUMBER})", body: "Revisa ${env.BUILD_URL}console"
}
unstable {
mail to: 'dades@empresa.com', subject: "Càrrega amb avisos de qualitat (#${env.BUILD_NUMBER})", body: "Revisa ${env.BUILD_URL}console"
}
}
}
archiveArtifacts guarda l’Excel original tal com ha arribat, amb la seva empremta (fingerprint): si mai cal reconstruir per què una fila té un valor concret, el fitxer font hi és, no cal refiar-se de la memòria de ningú. La resposta a «què fa quan falla» no és una: failure és quan el pas s’atura de veritat (falta una columna, el servidor SFTP no respon). Per al cas intermedi, «s’ha carregat però amb avisos», el catchError(buildResult: 'UNSTABLE', ...) que embolcalla el pas atrapa un error de sortida del script i marca l’execució com a UNSTABLE en lloc de FAILURE, sense aturar el pipeline. La diferència entre «atura-ho tot» i «avisa i continua» és el que fa que aquestes alertes es prenguin seriosament en lloc d’ignorar-se per costum.
carrega.py és on passa la feina de veritat: llegir l’Excel, validar-lo amb dos nivells de comprovació, i carregar-lo de manera que executar-lo dues vegades amb el mateix fitxer no dupliqui res.
import sys, hashlib
from datetime import datetime, timezone
import pandas as pd
from sqlalchemy import create_engine, text
fitxer, db_url, execucio = sys.argv[1], sys.argv[2], sys.argv[3]
df = pd.read_excel(fitxer, dtype=str)
# Comprovacions dures: si fallen, no es carrega res.
esperades = {'client_id', 'data_factura', 'import', 'estat'}
if not esperades.issubset(df.columns):
sys.exit(f"Falten columnes: {esperades - set(df.columns)}")
if df['client_id'].isna().any():
sys.exit(f"{df['client_id'].isna().sum()} files sense client_id")
# Comprovacions toves: es carrega igualment, però es marca l'execució.
duplicats = df.duplicated(subset=['client_id', 'data_factura']).sum()
qualitat_ok = duplicats <= len(df) * 0.02 # llindar del 2 %
engine = create_engine(db_url)
with engine.begin() as conn:
for _, fila in df.drop_duplicates(subset=['client_id', 'data_factura']).iterrows():
conn.execute(text("""
INSERT INTO factures (client_id, data_factura, import, estat)
VALUES (:client_id, :data_factura, :import, :estat)
ON CONFLICT (client_id, data_factura)
DO UPDATE SET import = EXCLUDED.import, estat = EXCLUDED.estat, actualitzat_el = now()
"""), fila.to_dict())
conn.execute(text("""
INSERT INTO auditoria_carregues
(execucio, fitxer, hash_fitxer, files_llegides, files_duplicades, executat_el)
VALUES (:execucio, :fitxer, :hash, :llegides, :duplicades, :ara)
"""), {
'execucio': execucio, 'fitxer': fitxer,
'hash': hashlib.sha256(open(fitxer, 'rb').read()).hexdigest(),
'llegides': len(df), 'duplicades': int(duplicats),
'ara': datetime.now(timezone.utc),
})
if not qualitat_ok:
print(f"AVÍS: {duplicats} duplicats, per sobre del llindar del 2 %")
sys.exit(1)
Tres decisions fan que aquest script sigui robust i no només funcional:
ON CONFLICT ... DO UPDATEés el que fa la càrrega idempotent: tornar a executar el mateix Excel no crea files noves ni duplica factures, només actualitza les que ja hi eren. És el mateix principi que les integracions que aguanten: un procés que es pot re-executar sense por és molt més fàcil de mantenir que un que només pots córrer un cop.- Les comprovacions es divideixen en dures i toves, no totes iguals. Una columna que falta és un motiu real per aturar-ho tot; un 3 % de files duplicades pot ser normal alguns dies i sospitós d’altres, així que es carrega igualment però es marca l’execució perquè algú hi doni un cop d’ull, en comptes de fer petar el job cada vegada que hi ha soroll normal a les dades.
- La taula
auditoria_carreguesno és opcional: guarda el hash del fitxer font (per saber exactament quina versió es va processar), quantes files va llegir i quantes va marcar com a dubtoses, i quan. Sis mesos després, quan algú pregunti «per què aquesta factura té aquest import», la resposta no és «no ho sé», és una fila concreta d’aquesta taula.
El mateix esquelet (recollir, validar amb dos nivells, carregar de manera idempotent, deixar rastre) serveix igual si en lloc d’un Excel és una API, un CSV per SFTP o l’extracció d’una base de dades d’un tercer. El que canvia és el connector d’entrada; la disciplina de validació i auditoria es queda igual.
El que costa admetre: els contres
Un article «molt tècnic» que només parli de virtuts no és honest. Jenkins té tres problemes reals, i val la pena dir-los abans de recomanar-lo:
La qualitat dels plugins és desigual. L’ecosistema té més de 2.000 connectors publicats, i és alhora la seva gran força i la seva gran feblesa: molts els manté una sola persona, en el seu temps lliure, i no tots segueixen el ritme de les versions noves de Jenkins. Abans d’adoptar-ne un que no sigui dels oficials (Git, Pipeline, Credentials), val la pena mirar quan es va actualitzar per última vegada.
Groovy no és gratis. La Declarative Pipeline redueix el problema, però en el moment que necessites lògica una mica més complexa (un bucle, una condició que no encaixa a when), acabes escrivint Groovy de veritat, i és un llenguatge que ni els desenvolupadors web ni els de dades solen conèixer.
El manteniment és teu. Jenkins és autoallotjat per naturalesa: tu instal·les, actualitzes, apedaces i escales el controlador. Comparat amb un servei gestionat com GitHub Actions o GitLab CI, on aquesta feina la fa algú altre, és el mateix intercanvi que ja apareix quan es parla de n8n autoallotjat en producció: més control, més responsabilitat operativa.
Jenkins o n8n: no és la mateixa pregunta
La pregunta «Jenkins o n8n» surt sovint, i gairebé sempre és mal plantejada, perquè normalment ja hi ha una resposta abans de fer-se-la: qui construirà i mantindrà el flux.
Si qui l’ha de mantenir és un equip d’enginyeria còmode escrivint codi, que vol el pipeline versionat al mateix repositori que allò que orquestra, i el que orquestra és sobretot build, test, desplegament o processament de dades per lots: Jenkins. Si qui l’ha de mantenir ve del negoci, no escriu codi cada dia, i el que cal és connectar aplicacions SaaS, moure dades entre sistemes o disparar accions quan passa alguna cosa a un CRM: n8n.
No és una qüestió de quina eina és millor. És la mateixa lliçó que es repeteix a cada projecte d’automatització que faig: tries la tecnologia segons la forma del problema, no al revés. Un martell excel·lent no converteix un cargol en un clau.