El mòdul 5 va acabar dient que els principis no es negocien i que les decisions es prenen mirant el context. Aquest mòdul inverteix el focus: fins ara l'eina va ser un mitjà —gairebé sempre GitHub Actions— i a partir d'aquí és el tema. Comencem per Jenkins per una raó pràctica i una de didàctica. La pràctica: és, amb diferència, el que més probablement et trobaràs ja instal·lat quan entris en una empresa amb més de deu anys de vida; ningú tria Jenkins avui en un projecte nou amb la mateixa freqüència amb què se'l troba heretat. La didàctica: Jenkins és anterior a gairebé tot el que aquest curs dóna per evident —contenidors, pipeline as code, runners efímers, identitat federada— i veure'l resoldre aquests problemes a posteriori, amb empelts sobre un disseny del 2005, explica per què GitLab CI, CircleCI o GitHub Actions es van dissenyar exactament com es van dissenyar. Veurem la seva arquitectura, el seu pecat original, el pipeline de Reservalia traduït a un Jenkinsfile declaratiu complet, com materialitza els temes transversals que ja coneixes, i què costa operar-lo de debò.
Contingut
- D'on ve Jenkins i què explica del seu disseny
- Arquitectura: controlador, agents, executors i etiquetes
- El pecat original: freestyle jobs configurats per interfície
- El pipeline de Reservalia en un
Jenkinsfiledeclaratiu - Scripted pipeline: quan cal Groovy de debò
- Agents efímers: Docker i Kubernetes
- Credencials i secrets
- Shared Libraries: la reutilització a Jenkins
- Multibranch pipelines i organització
- L'ecosistema de plugins: la fortalesa més gran i el risc més gran
- Operació real: actualitzacions, còpies de seguretat i JCasC
- Quan Jenkins és l'elecció correcta i quan és un llast
- Errors Comuns i Consells
- Exercicis
- Conclusió
- D'on ve Jenkins i què explica del seu disseny
Jenkins va néixer el 2005 com a Hudson, dins de Sun Microsystems, escrit per Kohsuke Kawaguchi. El 2011, després del conflicte per la marca que va seguir la compra de Sun per part d'Oracle, la comunitat va bifurcar el projecte i el va anomenar Jenkins. Aquest llinatge no és anecdòtic: explica gairebé totes les seves decisions de disseny.
El 2005 el món de la construcció de programari era diferent en quatre punts concrets:
| Supòsit del 2005 | Conseqüència a Jenkins | Què va passar després |
|---|---|---|
| El servidor de build és una màquina persistent que algú administra | L'estat viu al disc, a JENKINS_HOME |
Van arribar els contenidors i els agents efímers; Jenkins els va haver d'empeltar |
| La configuració es fa amb el ratolí, en una interfície web | Els jobs eren formularis, no fitxers | Va arribar pipeline as code; Jenkins va afegir Jenkinsfile el 2016, onze anys després |
| Cada equip té necessitats molt diferents | Arquitectura de plugins extremadament oberta | Ecosistema enorme… i una superfície de manteniment enorme |
| El programari es desplega cada poques setmanes | Res al nucli sobre entorns, aprovacions o desplegament | Tot això va arribar per plugins, amb qualitat desigual |
D'aquí la caracterització més útil de Jenkins: no és una eina de CI/CD, és un motor d'automatització de propòsit general amb una comunitat de plugins a sobre. Pot fer literalment qualsevol cosa —construir microprogramari, orquestrar proves contra maquinari, moure fitxers per SFTP— i això és alhora el seu superpoder i la raó que dues instal·lacions de Jenkins en dues empreses no s'assemblin gens. A GitHub Actions o GitLab CI, si saps fer-lo servir en una empresa el saps fer servir en una altra; a Jenkins, no necessàriament.
És programari lliure, escrit en Java, que instal·les tu. No existeix cap "Jenkins SaaS" oficial: hi ha ofertes comercials construïdes a sobre (CloudBees), però el model de base és tu operes el servidor. Aquest punt governa tota la resta i tornarà a l'apartat 11 i a la 06-07.
- Arquitectura: controlador, agents, executors i etiquetes
flowchart TD
subgraph C["Controlador · controller"]
UI["Interficie web i API"]
CUA["Cua de builds"]
HOME["JENKINS_HOME<br/>config, historial, plugins"]
end
C -->|"assigna per etiqueta"| A1
C --> A2
C --> A3
subgraph AG["Agents"]
A1["Agent linux-docker<br/>4 executors"]
A2["Agent macos<br/>2 executors"]
A3["Agent k8s efimer<br/>1 pod per build"]
end
El vocabulari de Jenkins traduït al del curs:
| Jenkins | Equivalent al curs | Matís important |
|---|---|---|
| Controller (abans master) | El servei de CI en si | És una JVM amb estat al disc; és el punt únic de fallada |
| Agent (abans slave) | Runner | Es connecta al controlador per JNLP, SSH o s'engega sota demanda |
| Executor | Ranura de concurrència | Un agent amb 4 executors executa 4 builds alhora |
| Node | Màquina (agent o el mateix controlador) | El controlador també pot tenir executors: mala idea |
| Job / Project | Workflow | Unitat configurable |
| Build / Run | Execució de workflow | Numerada (#412), amb historial persistent |
| Stage | Etapa lògica del pipeline | Es veu a la vista Stage View / Blue Ocean |
| Step | Step | Aquí sí que coincideix el nom |
| Workspace | Directori de treball del job a l'agent | Persisteix entre builds si l'agent no és efímer: origen de mil errors |
Tres conseqüències que cal entendre bé:
El controlador no ha d'executar builds. Per defecte ve amb executors al mateix controlador, i és el primer que cal posar a zero. Un build que corre al controlador té accés al sistema de fitxers on viuen les credencials xifrades, la configuració de tots els jobs i l'historial complet; un Jenkinsfile maliciós o simplement descurat pot llegir JENKINS_HOME. A més, un build pesat deixa la interfície sense resposta per a tothom. Regla: zero executors al controlador, sempre.
Les etiquetes són el mecanisme d'assignació. Cada agent declara etiquetes (linux, docker, macos, arm64, gpu) i el pipeline en demana una: agent { label 'linux && docker' }. És l'equivalent conceptual de runs-on amb etiquetes de runner autoallotjat a GitHub Actions (que veuràs a la 06-06), i admet expressions booleanes.
L'espai de treball persistent és la diferència mental més gran respecte de les eines modernes. A GitHub Actions cada job arrenca en una màquina neta; a Jenkins amb agents estàtics, l'espai de treball del job anterior continua allà. Això fa els builds més ràpids (no cal tornar a clonar-ho tot) i menys reproduïbles: fitxers generats que sobreviuen, node_modules d'una altra branca, un dist/ vell que fa passar una prova que hauria de fallar. És el "verd fals" de la 04-04 amb una altra causa. Per això existeix cleanWs() i per això l'apartat 6 sobre agents efímers és tan important.
- El pecat original: freestyle jobs configurats per interfície
Durant els seus primers deu anys, la manera normal de fer servir Jenkins era el freestyle job: entres al web, prems "Element nou", omples un formulari amb el repositori, marques unes caselles, escrius un script de shell en un <textarea> i deses. La configuració es serialitza en un config.xml dins de JENKINS_HOME, al servidor, fora del repositori del projecte.
Els problemes, tots els que la 04-05 va enumerar i algun més:
- No està versionat amb el codi. No hi ha diff, no hi ha revisió al PR, no hi ha
git blamequan el pipeline comença a fallar. La resposta a "qui ha canviat això?" és el registre d'auditoria de Jenkins, si el plugin està instal·lat. - No es pot ramificar. Si una branca necessita un pas extra, no hi ha manera d'expressar-ho: el job és un per a totes les branques, o es duplica el job.
- No es pot revisar. Un canvi al pipeline s'aplica en producció en el moment de prémer "Desa".
- No es pot reproduir. Migrar la instal·lació a una altra màquina significa copiar
JENKINS_HOMEsencer i resar. - Es copia i s'enganxa entre jobs. Quaranta jobs amb el mateix bloc de shell lleugerament diferent és l'estat natural d'una instal·lació amb anys.
Pipeline as Code (04-05) va ser la correcció, i va arribar amb el plugin Pipeline el 2016: el pipeline s'escriu en un fitxer Jenkinsfile a l'arrel del repositori, viatja amb el codi, es ramifica amb ell i es revisa al PR. És exactament la idea que GitLab CI i Travis portaven de naixement, i que Jenkins va haver d'encaixar a posteriori. Encara avui veuràs instal·lacions amb centenars de freestyle jobs; la migració a Jenkinsfile és una de les feines de modernització més habituals, i s'assembla molt a l'enfocament incremental de la 05-04.
- El pipeline de Reservalia en un
Jenkinsfile declaratiu
Jenkinsfile declaratiuAquest és el recurs central del mòdul: el mateix pipeline, sis vegades. El ci.yml de Reservalia té les etapes preparar → qualitat / test (matrix amb sharding) → build → seguretat → publicar (imatge a ECR per digest). Traduït a Jenkins declaratiu:
// Jenkinsfile — Reservalia · CI
pipeline {
agent none // 1
options { // 2
timeout(time: 30, unit: 'MINUTES')
disableConcurrentBuilds(abortPrevious: true) // equivalent a concurrency+cancel-in-progress
buildDiscarder(logRotator(numToKeepStr: '50', artifactNumToKeepStr: '10'))
timestamps()
skipDefaultCheckout()
}
environment { // 3
ECR_REGISTRY = '123456789012.dkr.ecr.eu-west-1.amazonaws.com'
IMATGE = 'reservalia/api'
NODE_ENV = 'test'
}
stages {
stage('Preparar') { // 4
agent { label 'linux && docker' }
steps {
checkout scm
sh 'npm ci'
stash name: 'fonts', includes: '**', excludes: '.git/**' // 5
}
}
stage('Verificació') { // 6
parallel {
stage('Qualitat') {
agent { label 'linux && docker' }
steps {
unstash 'fonts'
sh 'npx prettier --check .'
sh 'npm run lint -- --format checkstyle --output-file informes/eslint.xml'
sh 'npm run typecheck'
}
post {
always { recordIssues tools: [esLint(pattern: 'informes/eslint.xml')] }
}
}
stage('Tests') {
agent { label 'linux && docker' }
matrix { // 7
axes {
axis { name 'SHARD'; values '1', '2', '3', '4' }
}
stages {
stage('Executar shard') {
steps {
unstash 'fonts'
sh """
npm run test -- \
--shard=\${SHARD}/4 \
--reporters=default --reporters=jest-junit
"""
}
post {
always { junit 'informes/junit-*.xml' } // 8
}
}
}
}
}
}
}
stage('Build') {
agent { label 'linux && docker' }
steps {
unstash 'fonts'
sh 'npm run build'
sh '''
docker buildx build \
--file apps/api/Dockerfile \
--cache-from type=registry,ref=${ECR_REGISTRY}/${IMATGE}:cache \
--cache-to type=registry,ref=${ECR_REGISTRY}/${IMATGE}:cache,mode=max \
--tag ${IMATGE}:${GIT_COMMIT} \
--load .
'''
archiveArtifacts artifacts: 'apps/web/dist/**', fingerprint: true // 9
}
}
stage('Seguretat') {
agent { label 'linux && docker' }
steps {
unstash 'fonts'
sh 'npm audit --audit-level=high'
sh "trivy image --severity HIGH,CRITICAL --exit-code 1 ${IMATGE}:${GIT_COMMIT}"
}
}
stage('Publicar') {
when { branch 'main' } // 10
agent { label 'linux && docker' }
steps {
withCredentials([ // 11
string(credentialsId: 'aws-role-ci', variable: 'AWS_ROLE')
]) {
sh '''
aws ecr get-login-password --region eu-west-1 \
| docker login --username AWS --password-stdin ${ECR_REGISTRY}
docker push ${ECR_REGISTRY}/${IMATGE}:${GIT_COMMIT}
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' \
${ECR_REGISTRY}/${IMATGE}:${GIT_COMMIT})
echo "${DIGEST}" > digest.txt
'''
}
archiveArtifacts artifacts: 'digest.txt'
script { env.DIGEST = readFile('digest.txt').trim() } // 12
}
}
stage('Aprovar desplegament a producció') {
when { branch 'main' }
steps {
timeout(time: 4, unit: 'HOURS') { // 13
input message: "Voleu desplegar ${env.DIGEST} en producció?",
ok: 'Desplegar',
submitter: 'equip-plataforma'
}
}
}
}
post { // 14
always { cleanWs() }
failure { slackSend channel: '#reservalia-ci',
message: "CI trencat a ${env.BRANCH_NAME} · ${env.BUILD_URL}" }
fixed { slackSend channel: '#reservalia-ci', message: "CI verd de nou" }
}
}Bloc a bloc, i amb la comparació explícita amb el que ja coneixes:
agent nonea nivell de pipeline obliga que cada stage declari on s'executa. És la pràctica correcta: si hi poses un agent global, el pipeline ocupa un executor durant tota la seva vida, fins i tot mentre espera a l'inputd'aprovació —que pot ser hores—. És un error clàssic de consum de recursos que no té equivalent a GitHub Actions, on cada job demana el seu runner.optionsreuneix el que a GitHub Actions està repartit entretimeout-minutes,concurrencyi la configuració de retenció del repositori.disableConcurrentBuilds(abortPrevious: true)és elcancel-in-progressde la 04-04.buildDiscarderés la política de retenció de la 02-06, i aquí és imprescindible: sense ella, el disc del controlador s'omple, i un Jenkins amb el disc ple no arrenca.environmentdefineix variables per a tot el pipeline; també es pot declarar dins d'un stage. Admetcredentials('id')com a font, que és una drecera còmoda i amb un risc que veurem a l'apartat 7.checkout scmclona la revisió que va disparar el build; ambskipDefaultCheckout()aoptions, controles tu on passa. Sense aquesta opció, cada stage ambagentfa el seu propi checkout automàticament, cosa que de vegades és el que vols i de vegades multiplica clonacions per sis.stash/unstashés el pas d'informació entre stages de la 04-01: comprimeix fitxers al controlador i els recupera en un altre agent. I aquí hi ha el primer límit real: el stash viatja pel controlador, així que un stash de 800 MB denode_modulessatura la xarxa i el disc del controlador. Regla pràctica: stash per a codi i artefactes petits; per al que és gros, un magatzem extern (S3, Nexus, Artifactory) o reconstruir.parallelamb stages imbricats és el fan-out de la 04-01. Cada branca pot tenir el seu propiagent, els seusposti els seuswhen. Per defecte, si una branca falla, les altres s'avorten;paralleladmetfailFast trueper fer-ho explícit.matrixexisteix al declaratiu des del 2019 i funciona com elmatrixde la 02-04, ambaxes, i admetexcludes. Aquí implementa el sharding en quatre particions. Contrapartida honesta: és més verbós que el seu equivalent en YAML i no admet matrius dinàmiques generades per un stage anterior sense baixar ascript.junitpublica els resultats en format JUnit XML i és el que alimenta la vista de tests, l'històric de fallades i la detecció de proves flaky de la 02-04. Va apost { always }perquè els resultats cal publicar-los també quan el build falla, que és justament quan importen.archiveArtifactsambfingerprint: truedesa l'artefacte associat al build i registra el seu hash, de manera que Jenkins pot respondre "en quins builds i en quins jobs s'ha fet servir aquest fitxer exacte?". És la traçabilitat de la 02-06 implementada al mateix servidor. Compte: els artefactes arxivats viuen al disc del controlador, així queartifactNumToKeepStrno és opcional.when { branch 'main' }és el condicional de stage. AdmetchangeRequest(),changeset(equivalent als filtrespathsde la 02-07),expression { }amb Groovy,allOf/anyOf/not, ibeforeAgent trueper avaluar la condició abans de reservar un agent, cosa que estalvia executors.withCredentialsinjecta el secret només dins del bloc, que és el patró correcte (apartat 7).script { }és l'escotilla de sortida cap al pipeline scripted: a dins pots escriure Groovy arbitrari. Fes-ne poc ús; cada blocscriptés un tros de pipeline que ja no és declaratiu i que ningú pot llegir d'una ullada.inputés l'aprovació manual, l'equivalent conceptual dels Environments amb revisors de la 03-02. I aquí hi ha una diferència important de disseny: a GitHub Actions o GitLab, un job esperant aprovació no consumeix runner; a Jenkins, si l'inputés dins d'un stage ambagent, reté l'executor. Per això l'inputd'aquest pipeline és en un stage sense agent, i per això portatimeout: uninputsense timeout pot quedar-se esperant mesos.submitterrestringeix qui pot aprovar.postambalways,success,failure,unstable,changed,fixed,regression,aborted,cleanup.fixediregressionsón especialment útils per a notificacions: avisen només a les transicions, no a cada build, que és el que evita que el canal de Slack es converteixi en soroll que ningú llegeix (03-06).
El que aquest exemple revela per comparació: Jenkins expressa el mateix que el ci.yml, però el graf de dependències és implícit. A GitHub Actions els jobs es relacionen amb needs i l'ordre el dedueix el motor; al declaratiu de Jenkins els stages van en seqüència i el paral·lelisme és explícit i imbricat. És un model d'arbre, no de graf. Per al 90 % dels pipelines tant li fa; per a un fan-in complex de l'estil de la 04-01, es nota.
- Scripted pipeline: quan cal Groovy de debò
Abans del declaratiu existia el scripted pipeline: Groovy pur dins de node { }, sense estructura imposada.
// Scripted: Groovy amb tota la potència i cap barana
node('linux && docker') {
stage('Preparar') {
checkout scm
sh 'npm ci'
}
// Matriu dinàmica: els shards es decideixen en temps d'execució
def numShards = sh(script: 'node scripts/calcular-shards.js', returnStdout: true).trim() as int
def branques = [:]
for (int i = 1; i <= numShards; i++) {
def n = i // captura per valor: sense això, tancament sobre la variable del bucle
branques["shard-${n}"] = {
node('linux && docker') {
unstash 'fonts'
sh "npm run test -- --shard=${n}/${numShards}"
}
}
}
stage('Tests') { parallel branques }
}Quan cal: matrius calculades en temps d'execució, bucles sobre una llista que surt d'una API, lògica condicional complexa, o construir el pipeline sencer programàticament. Quan no: gairebé sempre. El declaratiu cobreix el cas normal, és validable (jenkins-cli declarative-linter), es llegeix sense saber Groovy i és el que entenen les eines de visualització.
Dos avisos concrets si acabes escrivint scripted. Primer, el codi s'executa al controlador, no a l'agent: només el que va dins de sh s'executa a l'agent. Un bucle Groovy pesat consumeix CPU del controlador i afecta tothom. Segon, Jenkins executa aquest Groovy amb CPS (Continuation Passing Style) per poder serialitzar l'estat i sobreviure a reinicis, i això trenca coses que en Groovy normal funcionen: .each {} amb closures problemàtics, variables no serialitzables com ara Matcher o InputStream travessant un sh, i missatges d'error del tipus java.io.NotSerializableException que no diuen res d'útil. És una de les experiències més frustrants de Jenkins i una raó real per quedar-se al declaratiu.
- Agents efímers: Docker i Kubernetes
El problema de l'agent contaminat és el que més builds falsos produeix en instal·lacions antigues: una màquina estàtica on s'han anat acumulant versions de Node instal·lades a mà, memòries cau de tres projectes, /tmp ple i una variable d'entorn que algú va posar el 2019. El build passa en un agent i falla en un altre, i ningú sap per què.
La resposta moderna: que l'agent no existeixi abans del build ni després.
// Opció A: un contenidor per stage
pipeline {
agent none
stages {
stage('Tests') {
agent {
docker {
image 'node:22-bookworm'
label 'linux && docker'
args '-v $HOME/.npm:/root/.npm' // memòria cau d'npm muntada des de l'amfitrió
reuseNode true // fa servir el mateix espai de treball del node
}
}
steps { sh 'npm ci && npm test' }
}
}
}// Opció B: un Pod de Kubernetes per build (plugin kubernetes)
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: node
image: node:22-bookworm
command: ["sleep"]
args: ["infinity"]
resources:
requests: { cpu: "1", memory: "2Gi" }
limits: { cpu: "2", memory: "4Gi" }
- name: buildkit
image: moby/buildkit:rootless # construir sense dimoni privilegiat
securityContext: { runAsUser: 1000 }
'''
}
}
stages {
stage('Test') { steps { container('node') { sh 'npm ci && npm test' } } }
stage('Build') { steps { container('buildkit') { sh 'buildctl build ...' } } }
}
}El plugin de Kubernetes és avui la manera recomanada d'operar Jenkins a escala: el controlador viu al clúster, cada build crea un Pod amb els contenidors que necessita, i el Pod mor en acabar. Avantatges directes: aïllament real, autoescalat de franc (ho fa el clúster), zero deriva de configuració, i cost proporcional a l'ús. És la mateixa idea que els runners efímers que veuràs a la 06-05 i a la 06-06 amb ARC, i és el que més acosta Jenkins al comportament de les eines modernes.
Contrapartida honesta: ara necessites operar Jenkins i un clúster de Kubernetes. Si no tenies Kubernetes, això no simplifica res. I queden detalls fins: el podTemplate s'ha de dimensionar (un requests mal posat omple el clúster o deixa els builds pendents), la memòria cau ja no persisteix entre builds per definició, i arrencar un Pod afegeix desenes de segons al temps de cua que a la 04-04 vas aprendre a mesurar per separat del temps d'execució.
- Credencials i secrets
Jenkins desa les credencials en un magatzem propi, xifrades a JENKINS_HOME amb una clau que també és a JENKINS_HOME —cosa que significa que qui pugui llegir aquest directori les pot desxifrar, i per això el controlador no ha d'executar builds—. Tipus: text secret, usuari/contrasenya, fitxer, clau SSH, certificat, i els que afegeixen els plugins (credencials d'AWS, tokens de GitHub…).
stage('Publicar') {
steps {
withCredentials([
usernamePassword(credentialsId: 'ecr-bot',
usernameVariable: 'REG_USER',
passwordVariable: 'REG_PASS'), // 1
file(credentialsId: 'kubeconfig-prod', variable: 'KUBECONFIG'),
string(credentialsId: 'token-sonar', variable: 'SONAR_TOKEN')
]) {
sh '''
set +x # 2
echo "$REG_PASS" | docker login -u "$REG_USER" --password-stdin "$ECR_REGISTRY"
docker push "$ECR_REGISTRY/$IMATGE:$GIT_COMMIT"
'''
}
}
}withCredentialsacota l'àmbit: la variable existeix només dins del bloc. L'alternativa —environment { REG_PASS = credentials('ecr-bot') }— exposa el secret a tot el pipeline, inclosos stages que no el necessiten, i contradiu el mínim privilegi de la 04-03.set +xdesactiva la traça del shell. Jenkins emmascara automàticament els valors de credencials que apareixen literalment al log (els substitueix per****), però aquest emmascarament és fràgil per disseny: no cobreix el valor transformat. Si fasecho $TOKEN | base64, si el secret es parteix en dues línies, o si un script l'imprimeix codificat en JSON, l'emmascarament no el reconeix i apareix en clar. Aquesta és exactament la lliçó que la 06-04 explicarà amb l'incident de Travis: un secret que entra en un log és un secret cremat, i l'emmascarament és una xarxa de seguretat, no una garantia.
Sobre identitat federada: Jenkins no té OIDC natiu cap a proveïdors cloud de la manera que el té GitHub Actions (03-02). Les opcions són plugins que emeten tokens OIDC, el rol d'instància de la màquina que allotja l'agent (si corre a AWS), o IRSA si corre a EKS. És assolible, però és un muntatge que fas tu, no una casella que marques; i aquest contrast concret —credencials de llarga vida per defecte davant d'identitat federada per defecte— és un dels arguments de pes reals a favor de les eines natives de la plataforma.
- Shared Libraries: la reutilització a Jenkins
És l'equivalent dels workflows reutilitzables i les composite actions de la 04-05. Una Shared Library és un repositori Git amb una estructura fixa:
reservalia-jenkins-lib/
├── vars/
│ ├── prepararNode.groovy # defineix el pas global prepararNode()
│ └── construirIPublicar.groovy
├── src/
│ └── com/reservalia/Ecr.groovy # classes Groovy normals
└── resources/
└── com/reservalia/plantilla-pod.yaml// vars/prepararNode.groovy — equivalent a la composite action preparar-node de la 04-05
def call(Map config = [:]) {
def profunditat = config.get('fetchDepth', 1)
checkout([$class: 'GitSCM',
extensions: [[$class: 'CloneOption', depth: profunditat, shallow: profunditat > 0]],
userRemoteConfigs: scm.userRemoteConfigs,
branches: scm.branches])
sh 'npm ci'
}// Jenkinsfile que la fa servir
@Library('reservalia@v3') _ // 1 · ancorat a un tag, mai a una branca mòbil
pipeline {
agent { label 'linux && docker' }
stages {
stage('Preparar') { steps { prepararNode() } }
stage('Seguretat') { steps { prepararNode(fetchDepth: 0) } }
}
}@Library('reservalia@v3')pot apuntar a una branca, un tag o un SHA. Apuntar amainsignifica que un commit a la llibreria canvia el comportament de tots els pipelines de l'empresa alhora, sense que ningú hagi fusionat res als seus repositoris. És exactament el risc del repositori central de la 04-05 i la raó de fixar per SHA de la 04-03, aquí amb més filada perquè el radi és tota l'organització. Ancora a tag com a mínim; a SHA si l'entorn és sensible.
Dos avisos més. Una llibreria marcada com a trusted global library a la configuració de Jenkins executa Groovy sense el sandbox de seguretat: qui pugui fer merge en aquest repositori executa codi arbitrari al controlador. Tracta-ho com a codi de producció amb CODEOWNERS (02-07). I les llibreries són difícils de provar: existeix JenkinsPipelineUnit per a tests unitaris de pipelines, però és un món a part i poca gent el manté; a la pràctica, la majoria proven en una branca amb un repositori de joguina, que és l'enfocament "provar el pipeline" de la 04-05 en la seva versió artesanal.
- Multibranch pipelines i organització
El Multibranch Pipeline és el tipus de job que fa que Jenkins s'assembli a les eines modernes: apunta a un repositori, Jenkins descobreix totes les branques que contenen un Jenkinsfile, crea un job per branca automàticament, l'executa a cada push i esborra el job quan la branca desapareix. Amb el plugin adequat, fa el mateix amb els pull requests (change requests), cosa que habilita les previsualitzacions per PR de la 02-07.
Un nivell més amunt hi ha l'Organization Folder (GitHub Organization, GitLab Group, Bitbucket Team): apunta a l'organització sencera i descobreix repositoris amb Jenkinsfile. És el que evita crear jobs a mà quan tens vuitanta repositoris.
| Tipus de job | Quan fer-lo servir |
|---|---|
| Freestyle | Mai en projectes nous; només manteniment del que s'ha heretat |
| Pipeline (single) | Automatitzacions soltes sense repositori propi (tasques nocturnes, operacions) |
| Multibranch Pipeline | El cas normal: un repositori amb Jenkinsfile |
| Organization Folder | Molts repositoris amb la mateixa convenció |
Detall operatiu que mossega: el descobriment de branques es fa per sondeig o per webhook. Amb sondeig, Jenkins pregunta al servidor Git cada X minuts, cosa que afegeix latència i, amb centenars de repositoris, castiga el servidor Git. Amb webhook, la reacció és immediata però el controlador ha de ser accessible des del servidor Git, cosa que en una instal·lació on-premise darrere del tallafoc no sempre és trivial. És un problema que les eines SaaS integrades senzillament no tenen.
- L'ecosistema de plugins: la fortalesa més gran i el risc més gran
Jenkins té més de mil vuit-cents plugins. Pràcticament qualsevol cosa que vulguis integrar ja en té un: una consola sèrie per a microprogramari, un sistema de tiquets antic, un dispositiu de signatura per maquinari, un mainframe. Aquest és l'argument més sòlid a favor de Jenkins el 2026, i no és petit: quan la teva integració rara no existeix en cap altra eina, a Jenkins existeix.
El preu, amb la mateixa honestedat:
- Qualitat desigual. Al costat de plugins mantinguts per empreses hi ha plugins amb un sol mantenidor que no toca el codi des de fa cinc anys, i cap senyal visible a la interfície que t'ho digui abans d'instal·lar-lo.
- Dependències entre plugins i amb el nucli. Actualitzar un plugin pot exigir actualitzar Jenkins, que pot trencar-ne tres més. L'actualització es converteix en un problema de resolució de dependències fet a mà.
- Superfície de CVE. Els avisos de seguretat de Jenkins són freqüents i molts afecten plugins, no el nucli. Un plugin sense manteniment amb una vulnerabilitat coneguda és una decisió: el treus o hi convius. Això és gestió de dependències de la 04-02 aplicada al teu propi sistema de CI.
- Estat global compartit. Els plugins comparteixen la JVM del controlador: un amb una fuita de memòria degrada tot Jenkins.
Higiene mínima, que és feina real i recurrent: inventari de plugins instal·lats amb justificació de per què hi és cadascun, desinstal·lar el que no es fa servir (la majoria d'instal·lacions en tenen desenes), subscriure's als avisos de seguretat del projecte Jenkins, i una finestra mensual d'actualització amb un entorn de proves on assajar-la primer.
- Operació real: actualitzacions, còpies de seguretat i JCasC
La pregunta que gairebé mai es fa en triar Jenkins és qui l'opera. No és una pregunta retòrica: és una fracció de persona amb nom i cognoms.
| Tasca | Freqüència | Cost típic |
|---|---|---|
| Actualitzar Jenkins i plugins | Mensual | Mitja jornada, més incidències |
| Revisar avisos de seguretat | Continu | Hores al mes |
Còpies de seguretat de JENKINS_HOME i prova de restauració |
Diària / trimestral | Automatitzable, però la prova s'ha de fer |
| Gestió d'agents (disc ple, agent desconnectat) | Setmanal | Interrupcions impredictibles |
| Gestió d'usuaris i permisos | Continu | Depèn de la mida |
| Recuperar el servei quan el controlador cau | Quan toca | Tothom aturat mentre dura |
Sobre les còpies: JENKINS_HOME ho és tot —configuració, credencials, historial, plugins, artefactes arxivats—. I la regla de la 03-05 aplica igual aquí: una còpia que no s'ha restaurat mai no és una còpia, és una esperança. Assaja la restauració en una màquina neta com a mínim cada trimestre.
La resposta moderna a "la configuració viu a config.xml en un servidor" és JCasC (Jenkins Configuration as Code): descriure la configuració del mateix Jenkins en YAML versionat.
# jenkins.yaml — configuració del controlador com a codi
jenkins:
systemMessage: "Jenkins de Reservalia · gestionat per JCasC · no editar per la interfície"
numExecutors: 0 # 1 · zero executors al controlador
authorizationStrategy:
roleBased:
roles:
global:
- name: "lectura"
permissions: ["Overall/Read", "Job/Read"]
entries: [{ group: "reservalia-tothom" }]
clouds:
- kubernetes: # 2 · agents efímers declarats aquí
name: "k8s"
namespace: "jenkins-agents"
jenkinsUrl: "http://jenkins.jenkins.svc.cluster.local:8080"
containerCapStr: "40"
unclassified:
location:
url: "https://jenkins.reservalia.example/"
credentials:
system:
domainCredentials:
- credentials:
- string:
id: "token-sonar"
scope: GLOBAL
secret: "${SONAR_TOKEN}" # 3 · des de variable d'entorn o gestor de secrets- La política de l'apartat 2, ara escrita i revisable en un PR.
- Tot el núvol d'agents descrit al mateix fitxer: reconstruir el controlador des de zero deixa de ser arqueologia.
- Els secrets no s'escriuen al YAML: s'interpolen des de l'entorn, normalment des d'un gestor de secrets extern, tal com es va establir a la 04-03.
Amb JCasC més un Jenkins desplegat per Helm a Kubernetes, el controlador esdevé reemplaçable, que és justament el que la 03-03 demanava de qualsevol infraestructura. Avís realista: la migració d'una instal·lació existent a JCasC no és automàtica ni ràpida, i hi ha plugins que no exposen la seva configuració a JCasC. Es fa per increments, exactament amb l'enfocament de la 05-04.
- Quan Jenkins és l'elecció correcta i quan és un llast
Sense fanatisme en cap direcció. Continua sent l'elecció correcta quan:
- Has d'executar en maquinari que controles tu: laboratori de dispositius, bancs de proves de microprogramari, màquines amb llicències fixades a un equip concret, GPU pròpies, arquitectures rares.
- El codi no pot sortir de la xarxa corporativa, per normativa o contracte, i un runner autoallotjat d'un SaaS no és acceptable perquè el pla de control continua sent a fora.
- Necessites una integració que només existeix com a plugin de Jenkins, i escriure-la des de zero en una altra eina costa més que operar Jenkins.
- Tens diversos sistemes de control de versions alhora —Git, i encara Subversion o Perforce en algun lloc—: Jenkins és agnòstic i les eines natives de plataforma no ho són.
- Ja tens una instal·lació sana, amb
Jenkinsfile, JCasC i agents efímers, i l'equip la sap operar. Migrar costa mesos i el benefici pot no compensar (la 06-07 hi posa números).
És un llast quan:
- Ningú l'opera de debò. Un Jenkins sense amo es degrada sol: plugins sense actualitzar, disc ple, agents morts, jobs freestyle que ningú sap què fan.
- Tot el teu codi és a GitHub o GitLab i no tens cap restricció especial: estàs pagant el cost d'operar un servidor i sincronitzar identitats per obtenir el que la plataforma ja et dóna integrat.
- L'equip és petit. La fracció de persona que consumeix l'operació és un percentatge enorme d'un equip de cinc, i aquest temps surt directament del producte. És l'argument que en Diego plantejaria a Reservalia, i tindria raó.
- La instal·lació és de l'era freestyle i ningú ha començat la migració: cada canvi fa por i el pipeline és exactament el contrari de la documentació executable que la 04-05 defensa.
Errors Comuns i Consells
Executar builds al controlador. És l'error número u i té conseqüències de seguretat i de disponibilitat alhora. Posa-hi numExecutors: 0 i no ho revisis mai més.
No posar buildDiscarder. El disc del controlador s'omple d'artefactes i historial, i un Jenkins amb el disc ple no arrenca ni deixa restaurar res. Política de retenció a tots els pipelines, des del primer.
Confiar en l'emmascarament de secrets. Emmascara coincidències literals. Transformat, trossejat o codificat, el secret surt en clar. Tracta qualsevol secret que hagi pogut tocar un log com a compromès i rota'l.
Un input dins d'un stage amb agent. Reté un executor durant hores o dies. Posa'l en un stage sense agent i sempre amb timeout.
Abusar de script { }. Cada bloc converteix un tros de pipeline declaratiu en Groovy que només entén qui el va escriure, i t'exposa als errors de serialització CPS. Si un pipeline té cinc blocs script, la lògica pertany a un script del repositori (scripts/*.sh, npm run) invocat amb sh —la lliçó de portabilitat que la 06-07 desenvoluparà—.
Stash de directoris enormes. El stash passa pel controlador. node_modules en un stash és una manera eficaç de tombar Jenkins per a tothom.
Apuntar @Library a main. Un merge a la llibreria canvia el pipeline de tota l'organització sense que ningú ho hagi demanat. Ancora a tag o SHA.
Espais de treball bruts. Sense agents efímers, fes servir cleanWs() a post i desconfia de qualsevol fallada que "només passa a l'agent 3".
Consell de migració des de freestyle: no converteixis els quaranta jobs alhora. Tria el més executat, escriu el seu Jenkinsfile, fes-lo córrer en paral·lel amb el freestyle durant dues setmanes comparant resultats, i només llavors apaga el vell. És el pipeline paral·lel de la 05-04 aplicat aquí.
Exercicis
Exercici 1. El pipeline de Reservalia a Jenkins triga 22 minuts, però l'execució real en suma 9. Investigant hi trobes: agent { label 'linux' } a nivell de pipeline, l'stage d'aprovació amb input dins d'aquest àmbit, stash de 640 MB (inclou node_modules), i quatre stages que fan el seu propi checkout scm perquè no hi ha skipDefaultCheckout(). Diagnostica cada causa i escriu el Jenkinsfile corregit en les seves parts rellevants.
Exercici 2. La Marta vol que l'equip deixi de repetir el bloc de preparació i el de publicació als sis repositoris de Reservalia. Dissenya la Shared Library: estructura de fitxers, el contingut de vars/publicarImatge.groovy amb paràmetres, com es versiona, com es referencia des dels Jenkinsfile, qui la pot modificar i com es prova un canvi sense trencar els sis repositoris alhora.
Exercici 3. En Diego pregunta: "Ja tenim GitHub Actions funcionant. Per què hauríem de muntar Jenkins?". Alhora, l'empresa acaba de signar amb un client sanitari que exigeix que el codi font no surti de la seva xarxa i que les proves d'integració s'executin contra un dispositiu físic connectat per USB al seu centre de dades. Escriu la resposta tècnica: quines opcions hi ha, què costa cadascuna i què recomanes.
Solucions
Solució 1.
| Símptoma | Causa | Correcció |
|---|---|---|
| El pipeline reté un executor 13 min de més | agent global + input a dins |
agent none global, agent per stage, input en stage sense agent i amb timeout |
| Transferències lentes entre stages | stash de 640 MB amb node_modules a través del controlador |
Stash només de fonts i artefactes de build; node_modules es reconstrueix amb npm ci sobre memòria cau, o es munta com a volum |
| Quatre clonacions redundants | Checkout implícit per stage amb agent |
options { skipDefaultCheckout() } i checkout scm explícit on cal |
Stage de publicació reservant agent a main i a branques |
Falta condició primerenca | when { beforeAgent true; branch 'main' } |
pipeline {
agent none
options {
skipDefaultCheckout()
timeout(time: 30, unit: 'MINUTES')
buildDiscarder(logRotator(numToKeepStr: '50', artifactNumToKeepStr: '10'))
}
stages {
stage('Preparar') {
agent { label 'linux && docker' }
steps {
checkout scm
sh 'npm ci'
stash name: 'fonts', includes: 'apps/**,packages/**,package*.json,tsconfig*.json'
// node_modules NO va al stash: cada agent fa npm ci amb memòria cau muntada
}
}
stage('Verificació') {
parallel {
stage('Qualitat') {
agent { docker { image 'node:22-bookworm'; args '-v $HOME/.npm:/root/.npm' } }
steps { unstash 'fonts'; sh 'npm ci && npm run lint && npm run typecheck' }
}
stage('Tests') {
agent { docker { image 'node:22-bookworm'; args '-v $HOME/.npm:/root/.npm' } }
steps { unstash 'fonts'; sh 'npm ci && npm test' }
post { always { junit 'informes/junit-*.xml' } }
}
}
}
stage('Publicar') {
when { beforeAgent true; branch 'main' }
agent { label 'linux && docker' }
steps { unstash 'fonts'; sh './scripts/publicar.sh' }
}
stage('Aprovació') {
when { beforeAgent true; branch 'main' }
steps {
timeout(time: 4, unit: 'HOURS') {
input message: 'Desplegar en producció', submitter: 'equip-plataforma'
}
}
}
}
post { always { node('linux') { cleanWs() } } }
}El resultat esperat no és "9 minuts" —sempre hi ha cua i arrencada de contenidors—, però sí eliminar els ~13 minuts d'espera i transferència que no eren feina. I una observació de mètode que val més que la correcció concreta: el diagnòstic va sortir de separar temps de cua, temps de transferència i temps d'execució, exactament la instrumentació de la 04-04. Sense aquesta separació, la conclusió hauria estat "Jenkins és lent".
Solució 2. Estructura:
reservalia-jenkins-lib/ ├── vars/ │ ├── prepararNode.groovy │ ├── publicarImatge.groovy │ └── pipelineServeiNode.groovy # pipeline sencer parametritzat ├── src/com/reservalia/Ecr.groovy ├── test/ # JenkinsPipelineUnit └── CODEOWNERS # @reservalia/plataforma
// vars/publicarImatge.groovy
def call(Map cfg) {
def repositori = cfg.repositori ?: error('Falta el paràmetre repositori')
def dockerfile = cfg.get('dockerfile', 'Dockerfile')
def publicar = cfg.get('publicar', false)
def registre = cfg.get('registre', env.ECR_REGISTRY)
sh """
docker buildx build --file ${dockerfile} \
--cache-from type=registry,ref=${registre}/${repositori}:cache \
--cache-to type=registry,ref=${registre}/${repositori}:cache,mode=max \
--tag ${registre}/${repositori}:${env.GIT_COMMIT} \
${publicar ? '--push' : '--load'} .
"""
if (publicar) {
def digest = sh(returnStdout: true, script:
"docker buildx imagetools inspect ${registre}/${repositori}:${env.GIT_COMMIT} " +
"--format '{{.Manifest.Digest}}'").trim()
currentBuild.description = "digest ${digest}"
return digest // qui la crida promociona per digest (02-06)
}
}Versionat: tags semàntics (v1, v1.3.0) i els Jenkinsfile referencien @Library('reservalia@v1'), mai @main. Un canvi incompatible puja a v2 i els repositoris migren quan poden, que és el mateix contracte de la 04-05.
Govern: CODEOWNERS amb l'equip de plataforma com a revisor obligatori; la llibreria marcada com a trusted executa codi sense sandbox al controlador, així que el llistó de revisió és el de codi de producció amb accés privilegiat.
Com es prova un canvi sense trencar els sis repositoris: tres capes. (a) Tests unitaris amb JenkinsPipelineUnit per a la lògica pura —quina ordre es genera amb quins paràmetres—, que s'executen al CI de la mateixa llibreria. (b) Un repositori de joguina, reservalia-lib-sandbox, el Jenkinsfile del qual apunta a @main i exerceix tots els vars a cada commit de la llibreria: és el canari. (c) Desplegament progressiu del tag nou: es puja v2, un repositori real l'adopta durant una setmana, i només llavors es propaga. És la migració progressiva de la 04-05 i explica també per què ancorar a main és tan perillós: elimina les capes (b) i (c) de cop.
Solució 3. La pregunta d'en Diego és la correcta i la resposta no és "munta Jenkins".
Opcions damunt la taula:
| Opció | Què implica | Cost | Riscos |
|---|---|---|---|
| A. Runners autoallotjats de GitHub Actions a la xarxa del client | S'instal·la un runner amb accés al dispositiu USB, amb l'etiqueta usb-lab. El codi es clona dins de la xarxa; el pla de control (logs, secrets, orquestració) continua a GitHub |
Baix: una màquina i un agent | El client ha d'acceptar que les metadades i els logs surten a un SaaS. Si la seva exigència és literal sobre el codi font, pot valer; si cobreix tot el flux, no |
| B. Jenkins on-premise només per a aquest client | Instal·lació petita a la seva xarxa, amb Jenkinsfile i agent amb el dispositiu |
Alt: nova instal·lació per operar, actualitzar, respatllar i vigilar | Segona eina per mantenir; dos pipelines que divergeixen |
| C. GitHub Enterprise Server on-premise | Tota la plataforma dins de la seva xarxa | Molt alt: llicències i operació d'una plataforma completa | Només té sentit si són molts clients així, no un |
| D. Pipeline híbrid | CI normal a GitHub Actions; només la prova contra maquinari es dispara en un Jenkins mínim del client, que retorna el resultat com a check | Mitjà | Dos sistemes, però cadascun petit i amb una frontera clara |
Recomanació: començar per A, perquè la majoria de les exigències de "el codi no surt de la nostra xarxa" se satisfan amb un runner autoallotjat, i cal llegir el contracte abans de dissenyar res —aquesta és la primera acció, no una consideració posterior—. Si en llegir-lo resulta que la restricció cobreix el pla de control, llavors D: un Jenkins mínim, desplegat amb JCasC, zero executors al controlador, agent amb el dispositiu, abast limitat a la prova de maquinari, i la resta del pipeline sense tocar.
La resposta directa a en Diego, que és la que importa: no es munta Jenkins perquè sigui millor, es munta perquè hi ha una restricció concreta —el dispositiu físic i la xarxa del client— que l'eina actual no pot satisfer, i es munta el mínim que resol aquesta restricció. Afegir Jenkins "per si de cas" o "per tenir-ho tot en un lloc" afegiria mitja jornada mensual d'operació a l'equip de plataforma sense resoldre cap problema que avui existeixi. I cal posar números al contracte: si el client sanitari factura menys que el cost anual d'operar la instal·lació, la conversa que toca és la de la 05-04 amb el negoci, no una decisió tècnica.
Conclusió
Jenkins és un motor d'automatització de propòsit general del 2005 al qual se li van anar afegint, un a un, tots els conceptes que aquest curs dóna per evidents: pipeline as code va arribar el 2016 amb el Jenkinsfile, els agents efímers amb els plugins de Docker i Kubernetes, la configuració reproduïble amb JCasC. Cada empelt funciona, i cap se sent natiu; aquesta és la diferència entre un disseny que ho tenia des del principi i un que ho va incorporar després. A canvi ofereix dues coses que cap altra eina del mòdul iguala: control total sobre on i com s'executa, i un ecosistema de plugins on la teva integració rara probablement ja existeix.
El que cal endur-se'n: el controlador mai executa builds; el declaratiu cobreix el 90 % dels casos i el scripted és l'excepció, no el punt de partida; els agents efímers resolen el problema de l'entorn contaminat i són avui la manera sensata d'operar-lo; withCredentials acota el secret i l'emmascarament del log no és una garantia; les Shared Libraries són els workflows reutilitzables de la 04-05 amb el mateix risc de radi si s'ancoren a una branca mòbil; i operar Jenkins és una fracció de persona real que cal nomenar abans de triar-lo, no descobrir després.
L'eina següent resol el problema oposat. Si Jenkins és un motor que ho integra tot però no porta res, GitLab CI/CD és una plataforma que ho porta tot integrat: repositori, CI, registre de contenidors, entorns, anàlisi de seguretat i incidències en un mateix producte, amb el pipeline definit en un .gitlab-ci.yml que va néixer sent pipeline as code i amb un model d'execució que va evolucionar de les etapes fixes al graf. Veurem què es guanya amb aquesta integració, què es paga per ella, i el pipeline de Reservalia traduït per segona vegada.
Curs de CI/CD: Integració i Desplegament Continu
Mòdul 1: Introducció al CI/CD
- Conceptes Bàsics de CI/CD
- Beneficis del CI/CD
- Eines Populars de CI/CD
- El Projecte del Curs: l'Aplicació que Automatitzarem
- Mètriques DORA: Com es Mesura el Lliurament de Programari
Mòdul 2: Integració Contínua (CI)
- Introducció a la Integració Contínua
- Configuració d'un Entorn de CI
- Automatització de la Construcció
- Proves Automatitzades
- Qualitat de Codi i Anàlisi Estàtica
- Artefactes, Versionat i Promoció
- Integració amb el Control de Versions
Mòdul 3: Desplegament Continu (CD)
- Introducció al Desplegament Continu
- Automatització del Desplegament
- Infraestructura com a Codi i Entorns Reproduïbles
- Estratègies de Desplegament
- Feature Flags, Rollback i Recuperació davant Errors
- Monitoratge i Retroalimentació
Mòdul 4: Pràctiques Avançades de CI/CD
- Pipelines de CI/CD
- Gestió de Dependències
- Seguretat en CI/CD
- Escalabilitat i Rendiment
- Pipeline as Code: Plantilles, Reutilització i Proves del Pipeline
- Bases de Dades al Pipeline: Migracions Segures
Mòdul 5: Implementació de CI/CD en Projectes Reals
- Cas d'Estudi: Projecte Web
- Cas d'Estudi: Aplicació Mòbil
- Cas d'Estudi: Microserveis
- Cas d'Estudi: Modernitzar un Projecte Legacy
Mòdul 6: Eines i Tecnologies
- Jenkins
- GitLab CI/CD
- CircleCI
- Travis CI
- Docker i Kubernetes
- GitHub Actions a Fons
- Comparativa i Criteris per Triar Eina
Mòdul 7: Exercicis Pràctics
- Exercici 1: Configuració d'un Pipeline Bàsic
- Exercici 2: Integració de Proves Automatitzades
- Exercici 3: Desplegament en un Entorn de Producció
- Exercici 4: Monitoratge i Retroalimentació
- Exercici 5: Enfortir el Pipeline amb Seguretat i Secrets
- Projecte Final: Pipeline Complet d'Extrem a Extrem
