El módulo 5 terminó diciendo que los principios no se negocian y las decisiones se toman mirando el contexto. Este módulo invierte el foco: hasta ahora la herramienta fue un medio —casi siempre GitHub Actions— y a partir de aquí es el tema. Empezamos por Jenkins por una razón práctica y una didáctica. La práctica: es, con diferencia, lo que más probablemente te encontrarás ya instalado cuando entres en una empresa con más de diez años de vida; nadie elige Jenkins hoy en un proyecto nuevo con la misma frecuencia con la que se lo encuentra heredado. La didáctica: Jenkins es anterior a casi todo lo que este curso da por evidente —contenedores, pipeline as code, runners efímeros, identidad federada— y verlo resolver esos problemas a posteriori, con injertos sobre un diseño de 2005, explica por qué GitLab CI, CircleCI o GitHub Actions se diseñaron exactamente como se diseñaron. Vamos a ver su arquitectura, su pecado original, el pipeline de Reservalia traducido a un Jenkinsfile declarativo completo, cómo materializa los temas transversales que ya conoces, y qué cuesta operarlo de verdad.

Contenido

  1. De dónde viene Jenkins y qué explica de su diseño
  2. Arquitectura: controlador, agentes, ejecutores y etiquetas
  3. El pecado original: freestyle jobs configurados por UI
  4. El pipeline de Reservalia en un Jenkinsfile declarativo
  5. Scripted pipeline: cuándo hace falta Groovy de verdad
  6. Agentes efímeros: Docker y Kubernetes
  7. Credenciales y secretos
  8. Shared Libraries: la reutilización en Jenkins
  9. Multibranch pipelines y organización
  10. El ecosistema de plugins: la mayor fortaleza y el mayor riesgo
  11. Operación real: actualizaciones, copias de seguridad y JCasC
  12. Cuándo Jenkins es la elección correcta y cuándo es un lastre
  13. Errores Comunes y Consejos
  14. Ejercicios
  15. Conclusión

  1. De dónde viene Jenkins y qué explica de su diseño

Jenkins nació en 2005 como Hudson, dentro de Sun Microsystems, escrito por Kohsuke Kawaguchi. En 2011, tras el conflicto por la marca que siguió a la compra de Sun por Oracle, la comunidad bifurcó el proyecto y lo llamó Jenkins. Ese linaje no es trivia: explica casi todas sus decisiones de diseño.

En 2005 el mundo de la construcción de software era distinto en cuatro puntos concretos:

Supuesto de 2005 Consecuencia en Jenkins Qué pasó después
El servidor de build es una máquina persistente que alguien administra El estado vive en disco, en JENKINS_HOME Llegaron los contenedores y los agentes efímeros; Jenkins tuvo que injertarlos
La configuración se hace con el ratón, en una interfaz web Los jobs eran formularios, no ficheros Llegó pipeline as code; Jenkins añadió Jenkinsfile en 2016, once años después
Cada equipo tiene necesidades muy distintas Arquitectura de plugins extremadamente abierta Enorme ecosistema… y una superficie de mantenimiento enorme
El software se despliega cada pocas semanas Nada en el núcleo sobre entornos, aprobaciones o despliegue Todo eso llegó por plugins, con calidad desigual

De ahí la caracterización más útil de Jenkins: no es una herramienta de CI/CD, es un motor de automatización de propósito general con una comunidad de plugins encima. Puede hacer literalmente cualquier cosa —construir firmware, orquestar pruebas contra hardware, mover ficheros por SFTP— y eso es a la vez su superpoder y la razón de que dos instalaciones de Jenkins en dos empresas no se parezcan en nada. En GitHub Actions o GitLab CI, si sabes usarlo en una empresa lo sabes usar en otra; en Jenkins, no necesariamente.

Es software libre, escrito en Java, que instalas tú. No existe "Jenkins SaaS" oficial: hay ofertas comerciales construidas encima (CloudBees), pero el modelo de base es tú operas el servidor. Ese punto gobierna todo lo demás y volverá en el apartado 11 y en la 06-07.

  1. Arquitectura: controlador, agentes, ejecutores y etiquetas

flowchart TD
    subgraph C["Controlador · controller"]
      UI["Interfaz web y API"]
      COLA["Cola de builds"]
      HOME["JENKINS_HOME<br/>config, historial, plugins"]
    end
    C -->|"asigna por etiqueta"| A1
    C --> A2
    C --> A3
    subgraph AG["Agentes"]
      A1["Agente linux-docker<br/>4 ejecutores"]
      A2["Agente macos<br/>2 ejecutores"]
      A3["Agente k8s efimero<br/>1 pod por build"]
    end

El vocabulario de Jenkins traducido al del curso:

Jenkins Equivalente en el curso Matiz importante
Controller (antes master) El servicio de CI en sí Es una JVM con estado en disco; es el punto único de fallo
Agent (antes slave) Runner Se conecta al controlador por JNLP, SSH o se lanza bajo demanda
Executor Ranura de concurrencia Un agente con 4 ejecutores corre 4 builds a la vez
Node Máquina (agente o el propio controlador) El controlador también puede tener ejecutores: mala idea
Job / Project Workflow Unidad configurable
Build / Run Ejecución de workflow Numerado (#412), con historial persistente
Stage Etapa lógica del pipeline Se ve en la vista Stage View / Blue Ocean
Step Step Aquí sí coincide el nombre
Workspace Directorio de trabajo del job en el agente Persiste entre builds si el agente no es efímero: origen de mil bugs

Tres consecuencias que hay que entender bien:

El controlador no debe ejecutar builds. Por defecto viene con ejecutores en el propio controlador, y es lo primero que hay que poner a cero. Un build que corre en el controlador tiene acceso al sistema de ficheros donde viven las credenciales cifradas, la configuración de todos los jobs y el historial completo; un Jenkinsfile malicioso o simplemente descuidado puede leer JENKINS_HOME. Además, un build pesado deja la interfaz sin respuesta para todo el mundo. Regla: cero ejecutores en el controlador, siempre.

Las etiquetas son el mecanismo de asignación. Cada agente declara etiquetas (linux, docker, macos, arm64, gpu) y el pipeline pide una: agent { label 'linux && docker' }. Es el equivalente conceptual de runs-on con etiquetas de runner autoalojado en GitHub Actions (que verás en la 06-06), y admite expresiones booleanas.

El workspace persistente es la diferencia mental más grande respecto a las herramientas modernas. En GitHub Actions cada job arranca en una máquina limpia; en Jenkins con agentes estáticos, el workspace del job anterior sigue ahí. Eso hace los builds más rápidos (no hay que volver a clonar todo) y menos reproducibles: ficheros generados que sobreviven, node_modules de otra rama, un dist/ viejo que hace pasar una prueba que debería fallar. Es el "verde falso" de la 04-04 con otra causa. Por eso existe cleanWs() y por eso el apartado 6 sobre agentes efímeros es tan importante.

  1. El pecado original: freestyle jobs configurados por UI

Durante sus primeros diez años, la forma normal de usar Jenkins era el freestyle job: entras en la web, pulsas "Nuevo elemento", rellenas un formulario con el repositorio, marcas unas casillas, escribes un script de shell en un <textarea> y guardas. La configuración se serializa en un config.xml dentro de JENKINS_HOME, en el servidor, fuera del repositorio del proyecto.

Los problemas, todos los que la 04-05 enumeró y alguno más:

  • No está versionado con el código. No hay diff, no hay revisión en PR, no hay git blame cuando el pipeline empieza a fallar. La respuesta a "¿quién cambió esto?" es el registro de auditoría de Jenkins, si está instalado el plugin.
  • No se puede ramificar. Si una rama necesita un paso extra, no hay forma de expresarlo: el job es uno para todas las ramas, o se duplica el job.
  • No se puede revisar. Un cambio en el pipeline se aplica en producción en el momento de pulsar "Guardar".
  • No se puede reproducir. Migrar la instalación a otra máquina significa copiar JENKINS_HOME entero y rezar.
  • Se copia y pega entre jobs. Cuarenta jobs con el mismo bloque de shell ligeramente distinto es el estado natural de una instalación con años.

Pipeline as Code (04-05) fue la corrección, y llegó con el plugin Pipeline en 2016: el pipeline se escribe en un fichero Jenkinsfile en la raíz del repositorio, viaja con el código, se ramifica con él y se revisa en el PR. Es exactamente la idea que GitLab CI y Travis traían de nacimiento, y que Jenkins tuvo que retro-encajar. Todavía hoy verás instalaciones con cientos de freestyle jobs; la migración a Jenkinsfile es uno de los trabajos de modernización más comunes, y se parece mucho al enfoque incremental de la 05-04.

  1. El pipeline de Reservalia en un Jenkinsfile declarativo

Este es el recurso central del módulo: el mismo pipeline, seis veces. El ci.yml de Reservalia tiene las etapas prepararcalidad / test (matrix con sharding) → buildseguridadpublicar (imagen a ECR por digest). Traducido a Jenkins declarativo:

// Jenkinsfile — Reservalia · CI
pipeline {
    agent none                                              // 1

    options {                                               // 2
        timeout(time: 30, unit: 'MINUTES')
        disableConcurrentBuilds(abortPrevious: true)        // equivalente 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'
        IMAGEN       = 'reservalia/api'
        NODE_ENV     = 'test'
    }

    stages {
        stage('Preparar') {                                 // 4
            agent { label 'linux && docker' }
            steps {
                checkout scm
                sh 'npm ci'
                stash name: 'fuentes', includes: '**', excludes: '.git/**'   // 5
            }
        }

        stage('Verificación') {                             // 6
            parallel {
                stage('Calidad') {
                    agent { label 'linux && docker' }
                    steps {
                        unstash 'fuentes'
                        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('Ejecutar shard') {
                                steps {
                                    unstash 'fuentes'
                                    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 'fuentes'
                sh 'npm run build'
                sh '''
                    docker buildx build \
                      --file apps/api/Dockerfile \
                      --cache-from type=registry,ref=${ECR_REGISTRY}/${IMAGEN}:cache \
                      --cache-to   type=registry,ref=${ECR_REGISTRY}/${IMAGEN}:cache,mode=max \
                      --tag ${IMAGEN}:${GIT_COMMIT} \
                      --load .
                '''
                archiveArtifacts artifacts: 'apps/web/dist/**', fingerprint: true   // 9
            }
        }

        stage('Seguridad') {
            agent { label 'linux && docker' }
            steps {
                unstash 'fuentes'
                sh 'npm audit --audit-level=high'
                sh "trivy image --severity HIGH,CRITICAL --exit-code 1 ${IMAGEN}:${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}/${IMAGEN}:${GIT_COMMIT}
                        DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' \
                                  ${ECR_REGISTRY}/${IMAGEN}:${GIT_COMMIT})
                        echo "${DIGEST}" > digest.txt
                    '''
                }
                archiveArtifacts artifacts: 'digest.txt'
                script { env.DIGEST = readFile('digest.txt').trim() }   // 12
            }
        }

        stage('Aprobar despliegue a producción') {
            when { branch 'main' }
            steps {
                timeout(time: 4, unit: 'HOURS') {           // 13
                    input message: "¿Desplegar ${env.DIGEST} en producción?",
                          ok: 'Desplegar',
                          submitter: 'equipo-plataforma'
                }
            }
        }
    }

    post {                                                  // 14
        always  { cleanWs() }
        failure { slackSend channel: '#reservalia-ci',
                            message: "CI roto en ${env.BRANCH_NAME} · ${env.BUILD_URL}" }
        fixed   { slackSend channel: '#reservalia-ci', message: "CI verde de nuevo" }
    }
}

Bloque a bloque, y con la comparación explícita con lo que ya conoces:

  1. agent none a nivel de pipeline obliga a que cada stage declare dónde corre. Es la práctica correcta: si pones un agente global, el pipeline ocupa un ejecutor durante toda su vida, incluso mientras espera en el input de aprobación —que puede ser horas—. Es un error clásico de consumo de recursos que no tiene equivalente en GitHub Actions, donde cada job pide su runner.
  2. options reúne lo que en GitHub Actions está repartido entre timeout-minutes, concurrency y la configuración de retención del repositorio. disableConcurrentBuilds(abortPrevious: true) es el cancel-in-progress de la 04-04. buildDiscarder es la política de retención de la 02-06, y aquí es imprescindible: sin ella, el disco del controlador se llena, y un Jenkins con el disco lleno no arranca.
  3. environment define variables para todo el pipeline; también se puede declarar dentro de un stage. Admite credentials('id') como fuente, que es un atajo cómodo y con un riesgo que veremos en el apartado 7.
  4. checkout scm clona la revisión que disparó el build; con skipDefaultCheckout() en options, controlas tú dónde ocurre. Sin esa opción, cada stage con agent hace su propio checkout automáticamente, lo cual a veces es lo que quieres y a veces multiplica clonaciones por seis.
  5. stash/unstash es el paso de información entre stages de la 04-01: comprime ficheros en el controlador y los recupera en otro agente. Y aquí está el primer límite real: el stash viaja por el controlador, así que un stash de 800 MB de node_modules satura la red y el disco del controlador. Regla práctica: stash para código y artefactos pequeños; para lo grande, un almacén externo (S3, Nexus, Artifactory) o reconstruir.
  6. parallel con stages anidados es el fan-out de la 04-01. Cada rama puede tener su propio agent, sus post y sus when. Por defecto, si una rama falla, las demás se abortan; parallel admite failFast true para hacerlo explícito.
  7. matrix existe en el declarativo desde 2019 y funciona como el matrix de la 02-04, con axes, y admite excludes. Aquí implementa el sharding en cuatro particiones. Contrapartida honesta: es más verboso que su equivalente en YAML y no admite matrices dinámicas generadas por un stage anterior sin bajar a script.
  8. junit publica los resultados en formato JUnit XML y es lo que alimenta la vista de tests, el histórico de fallos y la detección de flaky de la 02-04. Va en post { always } porque los resultados hay que publicarlos también cuando el build falla, que es justo cuando importan.
  9. archiveArtifacts con fingerprint: true guarda el artefacto asociado al build y registra su hash, de modo que Jenkins puede responder "¿en qué builds y en qué jobs se usó este fichero exacto?". Es la trazabilidad de la 02-06 implementada en el propio servidor. Ojo: los artefactos archivados viven en el disco del controlador, así que artifactNumToKeepStr no es opcional.
  10. when { branch 'main' } es el condicional de stage. Admite changeRequest(), changeset (equivalente a los filtros paths de la 02-07), expression { } con Groovy, allOf/anyOf/not, y beforeAgent true para evaluar la condición antes de reservar un agente, lo que ahorra ejecutores.
  11. withCredentials inyecta el secreto solo dentro del bloque, que es el patrón correcto (apartado 7).
  12. script { } es la escotilla de escape al pipeline scripted: dentro puedes escribir Groovy arbitrario. Úsala poco; cada bloque script es un trozo de pipeline que ya no es declarativo y que nadie puede leer de un vistazo.
  13. input es la aprobación manual, el equivalente conceptual de los Environments con revisores de la 03-02. Y aquí hay una diferencia importante de diseño: en GitHub Actions o GitLab, un job esperando aprobación no consume runner; en Jenkins, si el input está dentro de un stage con agent, retiene el ejecutor. Por eso el input de este pipeline está en un stage sin agente, y por eso lleva timeout: un input sin timeout puede quedarse esperando meses. submitter restringe quién puede aprobar.
  14. post con always, success, failure, unstable, changed, fixed, regression, aborted, cleanup. fixed y regression son especialmente útiles para notificaciones: avisan solo en las transiciones, no en cada build, que es lo que evita que el canal de Slack se convierta en ruido que nadie lee (03-06).

Lo que este ejemplo revela por comparación: Jenkins expresa lo mismo que el ci.yml, pero el grafo de dependencias es implícito. En GitHub Actions los jobs se relacionan con needs y el orden lo deduce el motor; en el declarativo de Jenkins los stages van en secuencia y el paralelismo es explícito y anidado. Es un modelo de árbol, no de grafo. Para el 90 % de los pipelines da igual; para un fan-in complejo del estilo de la 04-01, se nota.

  1. Scripted pipeline: cuándo hace falta Groovy de verdad

Antes del declarativo existía el scripted pipeline: Groovy puro dentro de node { }, sin estructura impuesta.

// Scripted: Groovy con toda la potencia y ninguna barandilla
node('linux && docker') {
    stage('Preparar') {
        checkout scm
        sh 'npm ci'
    }
    // Matriz dinámica: los shards se deciden en tiempo de ejecución
    def numShards = sh(script: 'node scripts/calcular-shards.js', returnStdout: true).trim() as int
    def ramas = [:]
    for (int i = 1; i <= numShards; i++) {
        def n = i                                   // captura por valor: sin esto, cierre sobre la variable del bucle
        ramas["shard-${n}"] = {
            node('linux && docker') {
                unstash 'fuentes'
                sh "npm run test -- --shard=${n}/${numShards}"
            }
        }
    }
    stage('Tests') { parallel ramas }
}

Cuándo hace falta: matrices calculadas en tiempo de ejecución, bucles sobre una lista que sale de una API, lógica condicional compleja, o construir el pipeline entero programáticamente. Cuándo no: casi siempre. El declarativo cubre el caso normal, es validable (jenkins-cli declarative-linter), se lee sin saber Groovy y es el que entienden las herramientas de visualización.

Dos avisos concretos si acabas escribiendo scripted. Primero, el código corre en el controlador, no en el agente: solo lo que va dentro de sh corre en el agente. Un bucle Groovy pesado consume CPU del controlador y afecta a todos. Segundo, Jenkins ejecuta ese Groovy con CPS (Continuation Passing Style) para poder serializar el estado y sobrevivir a reinicios, y eso rompe cosas que en Groovy normal funcionan: .each {} con closures problemáticos, variables no serializables como Matcher o InputStream cruzando un sh, y mensajes de error del tipo java.io.NotSerializableException que no dicen nada útil. Es una de las experiencias más frustrantes de Jenkins y una razón real para quedarse en el declarativo.

  1. Agentes efímeros: Docker y Kubernetes

El problema del agente contaminado es el que más builds falsos produce en instalaciones antiguas: una máquina estática donde se han ido acumulando versiones de Node instaladas a mano, cachés de tres proyectos, /tmp lleno y una variable de entorno que alguien puso en 2019. El build pasa en un agente y falla en otro, y nadie sabe por qué.

La respuesta moderna: que el agente no exista antes del build ni después.

// Opción A: un contenedor por stage
pipeline {
    agent none
    stages {
        stage('Tests') {
            agent {
                docker {
                    image 'node:22-bookworm'
                    label 'linux && docker'
                    args '-v $HOME/.npm:/root/.npm'    // caché de npm montada desde el host
                    reuseNode true                      // usa el mismo workspace del nodo
                }
            }
            steps { sh 'npm ci && npm test' }
        }
    }
}
// Opción B: un Pod de Kubernetes por 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 sin daemon privilegiado
                    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 es hoy la forma recomendada de operar Jenkins a escala: el controlador vive en el clúster, cada build crea un Pod con los contenedores que necesita, y el Pod muere al terminar. Ventajas directas: aislamiento real, autoescalado gratis (lo hace el clúster), cero deriva de configuración, y coste proporcional al uso. Es la misma idea que los runners efímeros que verás en la 06-05 y en la 06-06 con ARC, y es lo que más acerca Jenkins al comportamiento de las herramientas modernas.

Contrapartida honesta: ahora necesitas operar Jenkins y un clúster de Kubernetes. Si no tenías Kubernetes, esto no simplifica nada. Y quedan detalles finos: el podTemplate hay que dimensionarlo (un requests mal puesto llena el clúster o deja los builds pendientes), la caché ya no persiste entre builds por definición, y arrancar un Pod añade decenas de segundos al tiempo de cola que en la 04-04 aprendiste a medir por separado del tiempo de ejecución.

  1. Credenciales y secretos

Jenkins guarda las credenciales en un almacén propio, cifradas en JENKINS_HOME con una clave que también está en JENKINS_HOME —lo que significa que quien pueda leer ese directorio puede descifrarlas, y por eso el controlador no debe ejecutar builds—. Tipos: texto secreto, usuario/contraseña, fichero, clave SSH, certificado, y los que añaden plugins (credenciales de 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/$IMAGEN:$GIT_COMMIT"
            '''
        }
    }
}
  1. withCredentials acota el ámbito: la variable existe solo dentro del bloque. La alternativa —environment { REG_PASS = credentials('ecr-bot') }— expone el secreto a todo el pipeline, incluidos stages que no lo necesitan, y contradice el mínimo privilegio de la 04-03.
  2. set +x desactiva la traza del shell. Jenkins enmascara automáticamente los valores de credenciales que aparecen literalmente en el log (los sustituye por ****), pero ese enmascarado es frágil por diseño: no cubre el valor transformado. Si haces echo $TOKEN | base64, si el secreto se parte en dos líneas, o si un script lo imprime codificado en JSON, el enmascarado no lo reconoce y aparece en claro. Esta es exactamente la lección que la 06-04 contará con el incidente de Travis: un secreto que entra en un log es un secreto quemado, y el enmascarado es una red de seguridad, no una garantía.

Sobre identidad federada: Jenkins no tiene OIDC nativo hacia proveedores cloud del modo en que lo tiene GitHub Actions (03-02). Las opciones son plugins que emiten tokens OIDC, el rol de instancia de la máquina que aloja el agente (si corre en AWS), o IRSA si corre en EKS. Es alcanzable, pero es un montaje que haces tú, no una casilla que marcas; y ese contraste concreto —credenciales de larga vida por defecto frente a identidad federada por defecto— es uno de los argumentos de peso reales a favor de las herramientas nativas de la plataforma.

  1. Shared Libraries: la reutilización en Jenkins

Es el equivalente de los workflows reutilizables y las composite actions de la 04-05. Una Shared Library es un repositorio Git con una estructura fija:

reservalia-jenkins-lib/
├── vars/
│   ├── prepararNode.groovy       # define el paso global prepararNode()
│   └── construirYPublicar.groovy
├── src/
│   └── com/reservalia/Ecr.groovy # clases Groovy normales
└── resources/
    └── com/reservalia/plantilla-pod.yaml
// vars/prepararNode.groovy — equivalente a la composite action preparar-node de la 04-05
def call(Map config = [:]) {
    def profundidad = config.get('fetchDepth', 1)
    checkout([$class: 'GitSCM',
              extensions: [[$class: 'CloneOption', depth: profundidad, shallow: profundidad > 0]],
              userRemoteConfigs: scm.userRemoteConfigs,
              branches: scm.branches])
    sh 'npm ci'
}
// Jenkinsfile que la usa
@Library('reservalia@v3') _        // 1 · anclado a un tag, nunca a una rama móvil

pipeline {
    agent { label 'linux && docker' }
    stages {
        stage('Preparar') { steps { prepararNode() } }
        stage('Seguridad') { steps { prepararNode(fetchDepth: 0) } }
    }
}
  1. @Library('reservalia@v3') puede apuntar a una rama, un tag o un SHA. Apuntar a main significa que un commit en la librería cambia el comportamiento de todos los pipelines de la empresa a la vez, sin que nadie haya fusionado nada en sus repositorios. Es exactamente el riesgo del repositorio central de la 04-05 y la razón de fijar por SHA de la 04-03, aquí con más filo porque el radio es toda la organización. Ancla a tag como mínimo; a SHA si el entorno es sensible.

Dos avisos más. Una librería marcada como trusted global library en la configuración de Jenkins ejecuta Groovy sin el sandbox de seguridad: quien pueda hacer merge en ese repositorio ejecuta código arbitrario en el controlador. Trátalo como código de producción con CODEOWNERS (02-07). Y las librerías son difíciles de probar: existe JenkinsPipelineUnit para tests unitarios de pipelines, pero es un mundo aparte y poca gente lo mantiene; en la práctica, la mayoría prueba en una rama con un repositorio de juguete, que es el enfoque "probar el pipeline" de la 04-05 en su versión artesanal.

  1. Multibranch pipelines y organización

El Multibranch Pipeline es el tipo de job que hace que Jenkins se parezca a las herramientas modernas: apunta a un repositorio, Jenkins descubre todas las ramas que contienen un Jenkinsfile, crea un job por rama automáticamente, lo ejecuta en cada push y borra el job cuando la rama desaparece. Con el plugin adecuado, hace lo mismo con los pull requests (change requests), lo que habilita las previsualizaciones por PR de la 02-07.

Un nivel más arriba está el Organization Folder (GitHub Organization, GitLab Group, Bitbucket Team): apunta a la organización entera y descubre repositorios con Jenkinsfile. Es lo que evita crear jobs a mano cuando tienes ochenta repositorios.

Tipo de job Cuándo usarlo
Freestyle Nunca en proyectos nuevos; solo mantenimiento de lo heredado
Pipeline (single) Automatizaciones sueltas sin repositorio propio (tareas nocturnas, operaciones)
Multibranch Pipeline El caso normal: un repositorio con Jenkinsfile
Organization Folder Muchos repositorios con la misma convención

Detalle operativo que muerde: el descubrimiento de ramas se hace por sondeo o por webhook. Con sondeo, Jenkins pregunta al servidor Git cada X minutos, lo que añade latencia y, con cientos de repositorios, castiga al servidor Git. Con webhook, la reacción es inmediata pero el controlador tiene que ser alcanzable desde el servidor Git, lo que en una instalación on-premise detrás del firewall no siempre es trivial. Es un problema que las herramientas SaaS integradas simplemente no tienen.

  1. El ecosistema de plugins: la mayor fortaleza y el mayor riesgo

Jenkins tiene más de mil ochocientos plugins. Prácticamente cualquier cosa que quieras integrar ya tiene uno: una consola serie para firmware, un sistema de tickets antiguo, un dispositivo de firma hardware, un mainframe. Ese es el argumento más sólido a favor de Jenkins en 2026, y no es pequeño: cuando tu integración rara no existe en ninguna otra herramienta, en Jenkins existe.

El precio, con la misma honestidad:

  • Calidad desigual. Junto a plugins mantenidos por empresas hay plugins con un solo mantenedor que no toca el código desde hace cinco años, y ninguna señal visible en la interfaz que te lo diga antes de instalarlo.
  • Dependencias entre plugins y con el núcleo. Actualizar un plugin puede exigir actualizar Jenkins, que puede romper otros tres. La actualización se convierte en un problema de resolución de dependencias hecho a mano.
  • Superficie de CVE. Los avisos de seguridad de Jenkins son frecuentes y muchos afectan a plugins, no al núcleo. Un plugin sin mantenimiento con una vulnerabilidad conocida es una decisión: lo quitas o convives con ella. Esto es gestión de dependencias de la 04-02 aplicada a tu propio sistema de CI.
  • Estado global compartido. Los plugins comparten la JVM del controlador: uno con una fuga de memoria degrada todo Jenkins.

Higiene mínima, que es trabajo real y recurrente: inventario de plugins instalados con justificación de por qué está cada uno, desinstalar lo que no se usa (la mayoría de instalaciones tienen decenas), suscribirse a los avisos de seguridad del proyecto Jenkins, y una ventana mensual de actualización con un entorno de pruebas donde ensayarla primero.

  1. Operación real: actualizaciones, copias de seguridad y JCasC

La pregunta que casi nunca se hace al elegir Jenkins es quién lo opera. No es una pregunta retórica: es una fracción de persona con nombre y apellidos.

Tarea Frecuencia Coste típico
Actualizar Jenkins y plugins Mensual Media jornada, más incidencias
Revisar avisos de seguridad Continuo Horas al mes
Copias de seguridad de JENKINS_HOME y prueba de restauración Diaria / trimestral Automatizable, pero la prueba hay que hacerla
Gestión de agentes (disco lleno, agente desconectado) Semanal Interrupciones impredecibles
Gestión de usuarios y permisos Continuo Depende del tamaño
Recuperar el servicio cuando el controlador cae Cuando toca Todo el mundo parado mientras dura

Sobre las copias: JENKINS_HOME lo es todo —configuración, credenciales, historial, plugins, artefactos archivados—. Y la regla de la 03-05 aplica igual aquí: una copia que no se ha restaurado nunca no es una copia, es una esperanza. Ensaya la restauración en una máquina limpia al menos cada trimestre.

La respuesta moderna a "la configuración vive en config.xml en un servidor" es JCasC (Jenkins Configuration as Code): describir la configuración del propio Jenkins en YAML versionado.

# jenkins.yaml — configuración del controlador como código
jenkins:
  systemMessage: "Jenkins de Reservalia · gestionado por JCasC · no editar por la interfaz"
  numExecutors: 0                       # 1 · cero ejecutores en el controlador
  authorizationStrategy:
    roleBased:
      roles:
        global:
          - name: "lectura"
            permissions: ["Overall/Read", "Job/Read"]
            entries: [{ group: "reservalia-todos" }]
  clouds:
    - kubernetes:                       # 2 · agentes efímeros declarados aquí
        name: "k8s"
        namespace: "jenkins-agentes"
        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 · desde variable de entorno o gestor de secretos
  1. La política del apartado 2, ahora escrita y revisable en un PR.
  2. Toda la nube de agentes descrita en el mismo fichero: reconstruir el controlador desde cero deja de ser arqueología.
  3. Los secretos no se escriben en el YAML: se interpolan desde el entorno, normalmente desde un gestor de secretos externo, tal como se estableció en la 04-03.

Con JCasC más un Jenkins desplegado por Helm en Kubernetes, el controlador se vuelve reemplazable, que es justo lo que la 03-03 pedía de cualquier infraestructura. Aviso realista: la migración de una instalación existente a JCasC no es automática ni rápida, y hay plugins que no exponen su configuración a JCasC. Se hace por incrementos, exactamente con el enfoque de la 05-04.

  1. Cuándo Jenkins es la elección correcta y cuándo es un lastre

Sin fanatismo en ninguna dirección. Sigue siendo la elección correcta cuando:

  • Tienes que ejecutar en hardware que controlas tú: laboratorio de dispositivos, bancos de pruebas de firmware, máquinas con licencias fijadas a un equipo concreto, GPUs propias, arquitecturas raras.
  • El código no puede salir de la red corporativa, por normativa o contrato, y un runner autoalojado de un SaaS no es aceptable porque el plano de control sigue estando fuera.
  • Necesitas una integración que solo existe como plugin de Jenkins, y escribirla desde cero en otra herramienta cuesta más que operar Jenkins.
  • Tienes varios sistemas de control de versiones a la vez —Git, y todavía Subversion o Perforce en alguna parte—: Jenkins es agnóstico y las herramientas nativas de plataforma no.
  • Ya tienes una instalación sana, con Jenkinsfile, JCasC y agentes efímeros, y el equipo sabe operarla. Migrar cuesta meses y el beneficio puede no compensar (la 06-07 pone números a esto).

Es un lastre cuando:

  • Nadie lo opera de verdad. Un Jenkins sin dueño se degrada solo: plugins sin actualizar, disco lleno, agentes muertos, jobs freestyle que nadie sabe qué hacen.
  • Todo tu código está en GitHub o GitLab y no tienes ninguna restricción especial: estás pagando el coste de operar un servidor y sincronizar identidades para obtener lo que la plataforma ya te da integrado.
  • El equipo es pequeño. La fracción de persona que consume la operación es un porcentaje enorme de un equipo de cinco, y ese tiempo sale directamente del producto. Es el argumento que Diego plantearía en Reservalia, y tendría razón.
  • La instalación es de la era freestyle y nadie ha empezado la migración: cada cambio da miedo y el pipeline es exactamente lo contrario de la documentación ejecutable que la 04-05 defiende.

Errores Comunes y Consejos

Ejecutar builds en el controlador. Es el error número uno y tiene consecuencias de seguridad y de disponibilidad a la vez. Pon numExecutors: 0 y no lo revises nunca más.

No poner buildDiscarder. El disco del controlador se llena con artefactos e historial, y un Jenkins con el disco lleno no arranca ni deja restaurar nada. Política de retención en todos los pipelines, desde el primero.

Confiar en el enmascarado de secretos. Enmascara coincidencias literales. Transformado, troceado o codificado, el secreto sale en claro. Trata cualquier secreto que haya podido tocar un log como comprometido y rótalo.

Un input dentro de un stage con agente. Retiene un ejecutor durante horas o días. Ponlo en un stage sin agent y siempre con timeout.

Abusar de script { }. Cada bloque convierte un trozo de pipeline declarativo en Groovy que solo entiende quien lo escribió, y te expone a los errores de serialización CPS. Si un pipeline tiene cinco bloques script, la lógica pertenece a un script del repositorio (scripts/*.sh, npm run) invocado con sh —la lección de portabilidad que la 06-07 desarrollará—.

Stash de directorios enormes. El stash pasa por el controlador. node_modules en un stash es una forma eficaz de tumbar Jenkins para todos.

Apuntar @Library a main. Un merge en la librería cambia el pipeline de toda la organización sin que nadie lo haya pedido. Ancla a tag o SHA.

Workspaces sucios. Sin agentes efímeros, usa cleanWs() en post y desconfía de cualquier fallo que "solo pasa en el agente 3".

Consejo de migración desde freestyle: no conviertas los cuarenta jobs a la vez. Elige el más ejecutado, escribe su Jenkinsfile, hazlo correr en paralelo con el freestyle durante dos semanas comparando resultados, y solo entonces apaga el viejo. Es el pipeline paralelo de la 05-04 aplicado aquí.

Ejercicios

Ejercicio 1. El pipeline de Reservalia en Jenkins tarda 22 minutos, pero la ejecución real suma 9. Investigando encuentras: agent { label 'linux' } a nivel de pipeline, el stage de aprobación con input dentro de ese ámbito, stash de 640 MB (incluye node_modules), y cuatro stages que hacen su propio checkout scm porque no está skipDefaultCheckout(). Diagnostica cada causa y escribe el Jenkinsfile corregido en sus partes relevantes.

Ejercicio 2. Marta quiere que el equipo deje de repetir el bloque de preparación y de publicación en los seis repositorios de Reservalia. Diseña la Shared Library: estructura de ficheros, el contenido de vars/publicarImagen.groovy con parámetros, cómo se versiona, cómo se referencia desde los Jenkinsfile, quién puede modificarla y cómo se prueba un cambio sin romper los seis repositorios a la vez.

Ejercicio 3. Diego pregunta: "Ya tenemos GitHub Actions funcionando. ¿Por qué íbamos a montar Jenkins?". A la vez, la empresa acaba de firmar con un cliente sanitario que exige que el código fuente no salga de su red y que las pruebas de integración corran contra un dispositivo físico conectado por USB en su centro de datos. Escribe la respuesta técnica: qué opciones hay, qué cuesta cada una y qué recomiendas.

Soluciones

Solución 1.

Síntoma Causa Corrección
El pipeline retiene un ejecutor 13 min de más agent global + input dentro agent none global, agent por stage, input en stage sin agente y con timeout
Transferencias lentas entre stages stash de 640 MB con node_modules a través del controlador Stash solo de fuentes y artefactos de build; node_modules se reconstruye con npm ci sobre caché, o se monta como volumen
Cuatro clonaciones redundantes Checkout implícito por stage con agent options { skipDefaultCheckout() } y checkout scm explícito donde hace falta
Stage de publicación reservando agente en main y en ramas Falta condición temprana 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: 'fuentes', includes: 'apps/**,packages/**,package*.json,tsconfig*.json'
                // node_modules NO va al stash: cada agente hace npm ci con caché montada
            }
        }
        stage('Verificación') {
            parallel {
                stage('Calidad') {
                    agent { docker { image 'node:22-bookworm'; args '-v $HOME/.npm:/root/.npm' } }
                    steps { unstash 'fuentes'; 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 'fuentes'; sh 'npm ci && npm test' }
                    post { always { junit 'informes/junit-*.xml' } }
                }
            }
        }
        stage('Publicar') {
            when { beforeAgent true; branch 'main' }
            agent { label 'linux && docker' }
            steps { unstash 'fuentes'; sh './scripts/publicar.sh' }
        }
        stage('Aprobación') {
            when { beforeAgent true; branch 'main' }
            steps {
                timeout(time: 4, unit: 'HOURS') {
                    input message: 'Desplegar en producción', submitter: 'equipo-plataforma'
                }
            }
        }
    }
    post { always { node('linux') { cleanWs() } } }
}

El resultado esperado no es "9 minutos" —siempre hay cola y arranque de contenedores—, pero sí eliminar los ~13 minutos de espera y transferencia que no eran trabajo. Y una observación de método que vale más que la corrección concreta: el diagnóstico salió de separar tiempo de cola, tiempo de transferencia y tiempo de ejecución, exactamente la instrumentación de la 04-04. Sin esa separación, la conclusión habría sido "Jenkins es lento".

Solución 2. Estructura:

reservalia-jenkins-lib/
├── vars/
│   ├── prepararNode.groovy
│   ├── publicarImagen.groovy
│   └── pipelineServicioNode.groovy   # pipeline entero parametrizado
├── src/com/reservalia/Ecr.groovy
├── test/                              # JenkinsPipelineUnit
└── CODEOWNERS                         # @reservalia/plataforma
// vars/publicarImagen.groovy
def call(Map cfg) {
    def repositorio = cfg.repositorio ?: error('Falta el parámetro repositorio')
    def dockerfile  = cfg.get('dockerfile', 'Dockerfile')
    def publicar    = cfg.get('publicar', false)
    def registro    = cfg.get('registro', env.ECR_REGISTRY)

    sh """
        docker buildx build --file ${dockerfile} \
          --cache-from type=registry,ref=${registro}/${repositorio}:cache \
          --cache-to   type=registry,ref=${registro}/${repositorio}:cache,mode=max \
          --tag ${registro}/${repositorio}:${env.GIT_COMMIT} \
          ${publicar ? '--push' : '--load'} .
    """
    if (publicar) {
        def digest = sh(returnStdout: true, script:
            "docker buildx imagetools inspect ${registro}/${repositorio}:${env.GIT_COMMIT} " +
            "--format '{{.Manifest.Digest}}'").trim()
        currentBuild.description = "digest ${digest}"
        return digest                    // el llamador promociona por digest (02-06)
    }
}

Versionado: tags semánticos (v1, v1.3.0) y los Jenkinsfile referencian @Library('reservalia@v1'), nunca @main. Un cambio incompatible sube a v2 y los repositorios migran cuando pueden, que es el mismo contrato de la 04-05.

Gobierno: CODEOWNERS con el equipo de plataforma como revisor obligatorio; la librería marcada como trusted ejecuta código sin sandbox en el controlador, así que el listón de revisión es el de código de producción con acceso privilegiado.

Cómo se prueba un cambio sin romper los seis repositorios: tres capas. (a) Tests unitarios con JenkinsPipelineUnit para la lógica pura —qué comando se genera con qué parámetros—, que se ejecutan en el CI de la propia librería. (b) Un repositorio de juguete, reservalia-lib-sandbox, cuyo Jenkinsfile apunta a @main y ejerce todos los vars en cada commit de la librería: es el canario. (c) Despliegue progresivo del tag nuevo: se sube v2, un repositorio real lo adopta durante una semana, y solo entonces se propaga. Es la migración progresiva de la 04-05 y explica también por qué anclar a main es tan peligroso: elimina las capas (b) y (c) de golpe.

Solución 3. La pregunta de Diego es la correcta y la respuesta no es "monta Jenkins".

Opciones sobre la mesa:

Opción Qué implica Coste Riesgos
A. Runners autoalojados de GitHub Actions en la red del cliente Se instala un runner con acceso al dispositivo USB, con etiqueta usb-lab. El código se clona dentro de la red; el plano de control (logs, secretos, orquestación) sigue en GitHub Bajo: una máquina y un agente El cliente debe aceptar que los metadatos y logs salen a un SaaS. Si su exigencia es literal sobre el código fuente, puede valer; si cubre todo el flujo, no
B. Jenkins on-premise solo para ese cliente Instalación pequeña en su red, con Jenkinsfile y agente con el dispositivo Alto: nueva instalación que operar, actualizar, respaldar y vigilar Segunda herramienta que mantener; dos pipelines que divergen
C. GitHub Enterprise Server on-premise Toda la plataforma dentro de su red Muy alto: licencias y operación de una plataforma completa Solo tiene sentido si son muchos clientes así, no uno
D. Pipeline híbrido CI normal en GitHub Actions; solo la prueba contra hardware se dispara en un Jenkins mínimo del cliente, que devuelve el resultado como check Medio Dos sistemas, pero cada uno pequeño y con una frontera clara

Recomendación: empezar por A, porque la mayoría de las exigencias de "el código no sale de nuestra red" se satisfacen con un runner autoalojado, y hay que leer el contrato antes de diseñar nada —esa es la primera acción, no una consideración posterior—. Si al leerlo resulta que la restricción cubre el plano de control, entonces D: un Jenkins mínimo, desplegado con JCasC, cero ejecutores en el controlador, agente con el dispositivo, alcance limitado a la prueba de hardware, y el resto del pipeline sin tocar.

La respuesta directa a Diego, que es la que importa: no se monta Jenkins porque sea mejor, se monta porque hay una restricción concreta —el dispositivo físico y la red del cliente— que la herramienta actual no puede satisfacer, y se monta lo mínimo que resuelve esa restricción. Añadir Jenkins "por si acaso" o "para tenerlo todo en un sitio" añadiría media jornada mensual de operación al equipo de plataforma sin resolver ningún problema que hoy exista. Y hay que ponerle números al contrato: si el cliente sanitario factura menos que el coste anual de operar la instalación, la conversación que toca es la de la 05-04 con negocio, no una decisión técnica.

Conclusión

Jenkins es un motor de automatización de propósito general de 2005 al que se le fueron añadiendo, uno a uno, todos los conceptos que este curso da por evidentes: pipeline as code llegó en 2016 con el Jenkinsfile, los agentes efímeros con los plugins de Docker y Kubernetes, la configuración reproducible con JCasC. Cada injerto funciona, y ninguno se siente nativo; esa es la diferencia entre un diseño que lo tenía desde el principio y uno que lo incorporó después. A cambio ofrece dos cosas que ninguna otra herramienta del módulo iguala: control total sobre dónde y cómo se ejecuta, y un ecosistema de plugins donde tu integración rara probablemente ya existe.

Lo que hay que llevarse: el controlador nunca ejecuta builds; el declarativo cubre el 90 % de los casos y el scripted es la excepción, no el punto de partida; los agentes efímeros resuelven el problema del entorno contaminado y son hoy la forma sensata de operarlo; withCredentials acota el secreto y el enmascarado del log no es una garantía; las Shared Libraries son los workflows reutilizables de la 04-05 con el mismo riesgo de radio si se anclan a una rama móvil; y operar Jenkins es una fracción de persona real que hay que nombrar antes de elegirlo, no descubrir después.

La herramienta siguiente resuelve el problema opuesto. Si Jenkins es un motor que integra todo pero no trae nada, GitLab CI/CD es una plataforma que lo trae todo integrado: repositorio, CI, registro de contenedores, entornos, análisis de seguridad e issues en un mismo producto, con el pipeline definido en un .gitlab-ci.yml que nació siendo pipeline as code y con un modelo de ejecución que evolucionó de las etapas fijas al grafo. Veremos qué se gana con esa integración, qué se paga por ella, y el pipeline de Reservalia traducido por segunda vez.

Curso de CI/CD: Integración y Despliegue Continuo

Módulo 1: Introducción a CI/CD

Módulo 2: Integración Continua (CI)

Módulo 3: Despliegue Continuo (CD)

Módulo 4: Prácticas Avanzadas de CI/CD

Módulo 5: Implementación de CI/CD en Proyectos Reales

Módulo 6: Herramientas y Tecnologías

Módulo 7: Ejercicios Prácticos

Módulo 8: Recursos Adicionales

© Copyright 2026. Todos los derechos reservados