Automatización con Jenkins


Alumno: Iñaki Spinardi
Curso: 2º ASIR
Asignatura: Optativa
Sistema utilizado: Arch Linux
Ruta de trabajo: /home/inaki/Documents/2ASIR3/optativa/recu
Proyecto utilizado: VirtualTableTop
Repositorio: https://github.com/ArnoldSmith86/virtualtabletop


Objetivo de la práctica

El objetivo de esta práctica es crear una automatización de integración continua para nuevos desarrolladores de la aplicación web VirtualTableTop.

La solución implementada permite que Jenkins automatice el proceso completo de construcción y despliegue de la aplicación.

La automatización realizada cumple los siguientes puntos:

  1. Clonar el repositorio del proyecto.
  2. Construir una imagen Docker de la aplicación.
  3. Levantar el servicio web mediante Docker Compose.
  4. Desplegar la aplicación en Kubernetes.
  5. Ejecutar 3 réplicas de la aplicación.
  6. Balancear carga mediante un Service de Kubernetes.
  7. Demostrar que, si se elimina un Pod, Kubernetes lo vuelve a crear automáticamente.
  8. Automatizar el redespliegue cuando se realiza un commit nuevo en el repositorio.

Proyecto seleccionado

El proyecto utilizado es el propuesto en el enunciado:

https://github.com/ArnoldSmith86/virtualtabletop

Se ha seleccionado este proyecto porque cumple los requisitos de la práctica:

  • Es una aplicación web.
  • Es un proyecto real alojado en GitHub.
  • Dispone de Dockerfile.
  • Puede ejecutarse en contenedor Docker.
  • Expone un servicio web en el puerto 8272.
  • Puede ser desplegado mediante Docker Compose.
  • Puede ser desplegado en Kubernetes.
  • No corresponde al proyecto 5etools, prohibido por el enunciado.

No se ha utilizado un proyecto alternativo, por lo que no es necesaria una justificación adicional.

Tecnologías utilizadas

Para realizar la práctica se han usado las siguientes tecnologías:

TecnologíaUso
Arch LinuxSistema operativo local
GitClonado del repositorio y control de commits
JenkinsAutomatización CI/CD
DockerConstrucción y ejecución de contenedores
Docker ComposeDespliegue local del servicio web
MinikubeCluster Kubernetes local
kubectlAdministración del cluster Kubernetes
Kubernetes DeploymentEjecución de 3 réplicas
Kubernetes ServiceExposición y balanceo de la aplicación
curlPruebas HTTP desde terminal

Estructura general de la solución

La solución funciona de la siguiente forma:

  1. El repositorio de VirtualTableTop se clona en local.
  2. Jenkins lee el repositorio local usando Git.
  3. Jenkins ejecuta el fichero Jenkinsfile.
  4. El pipeline construye una imagen Docker nueva.
  5. Docker Compose detiene el contenedor anterior.
  6. Docker Compose levanta el nuevo contenedor.
  7. Jenkins comprueba que la web responde en el puerto 8272.
  8. Jenkins carga la imagen Docker en Minikube.
  9. Jenkins despliega la aplicación en Kubernetes.
  10. Kubernetes ejecuta 3 réplicas de la aplicación.
  11. Kubernetes expone la aplicación con un Service de tipo NodePort.
  12. Al hacer un nuevo commit, Jenkins detecta el cambio y ejecuta de nuevo el pipeline.

Ruta local de trabajo

La práctica se ha realizado en la siguiente ruta:

/home/inaki/Documents/2ASIR3/optativa/recu

Para trabajar de forma más cómoda se definieron estas variables:

export VTT_BASE="/home/inaki/Documents/2ASIR3/optativa/recu"
export VTT_REPO="$VTT_BASE/virtualtabletop-dev"

Se creó la carpeta principal:

mkdir -p "$VTT_BASE"
cd "$VTT_BASE"

Instalación de herramientas necesarias en Arch Linux

Se instalaron las herramientas necesarias usando pacman:

sudo pacman -Syu
sudo pacman -S --needed git docker docker-compose jenkins jdk21-openjdk curl minikube kubectl

También se configuró Java 21 como versión por defecto:

sudo archlinux-java set java-21-openjdk
java -version

Se activó Docker:

sudo systemctl enable --now docker
sudo systemctl status docker

Se añadieron los usuarios inaki y jenkins al grupo docker:

sudo usermod -aG docker inaki
sudo usermod -aG docker jenkins

Después fue necesario reiniciar sesión o reiniciar los servicios para que los permisos del grupo docker se aplicasen correctamente.

Comprobación de Docker:

docker run hello-world

Esta comprobación confirma que Docker funciona correctamente en el equipo.

Instalación y arranque de Jenkins

Jenkins se instaló como servicio local en Arch Linux.

Se activó el servicio:

sudo systemctl enable --now jenkins

Se comprobó el estado:

sudo systemctl status jenkins --no-pager

En esta práctica, Jenkins quedó accesible en:

http://localhost:8090

La contraseña inicial se obtuvo con:

sudo cat /var/lib/jenkins/secrets/initialAdminPassword

Durante la configuración inicial se instalaron los plugins recomendados.

También se comprobó que estuvieran disponibles estos plugins:

  • Git
  • Pipeline
  • Pipeline: Stage View

Captura:

Jenkins funcionando

Clonado del repositorio

Se clonó el repositorio de VirtualTableTop en la ruta de trabajo:

cd /home/inaki/Documents/2ASIR3/optativa/recu
git clone https://github.com/ArnoldSmith86/virtualtabletop.git virtualtabletop-dev
cd /home/inaki/Documents/2ASIR3/optativa/recu/virtualtabletop-dev

Se comprobó que el repositorio estuviera correctamente clonado:

git status
git branch

También se comprobó la existencia de ficheros importantes del proyecto:

ls Dockerfile package.json server.mjs

Captura:

Repositorio clonado

Permisos para Jenkins

Como Jenkins se ejecuta con el usuario jenkins, fue necesario permitirle acceder al repositorio local.

El repositorio se encuentra dentro del directorio personal del usuario inaki:

/home/inaki/Documents/2ASIR3/optativa/recu/virtualtabletop-dev

Por eso se dieron permisos de recorrido a las carpetas padre:

sudo chmod o+x /home/inaki
sudo chmod o+x /home/inaki/Documents
sudo chmod o+x /home/inaki/Documents/2ASIR3
sudo chmod o+x /home/inaki/Documents/2ASIR3/optativa
sudo chmod o+x /home/inaki/Documents/2ASIR3/optativa/recu

También se dieron permisos de lectura al repositorio:

sudo chmod -R a+rX /home/inaki/Documents/2ASIR3/optativa/recu/virtualtabletop-dev

Se marcó el repositorio como seguro para Git cuando lo usa el usuario jenkins:

sudo -u jenkins git config --global --add safe.directory /home/inaki/Documents/2ASIR3/optativa/recu/virtualtabletop-dev

Se comprobó que Jenkins pudiera leer el repositorio:

sudo -u jenkins -H git -C /home/inaki/Documents/2ASIR3/optativa/recu/virtualtabletop-dev status

Corrección del bloqueo de repositorios locales en Jenkins

Al configurar Jenkins para leer un repositorio local usando file:///, apareció el siguiente error:

Checkout of Git remote 'file:///home/inaki/Documents/2ASIR3/optativa/recu/virtualtabletop-dev' aborted because it references a local directory, which may be insecure.
You can allow local checkouts anyway by setting the system property 'hudson.plugins.git.GitSCM.ALLOW_LOCAL_CHECKOUT' to true.

Jenkins bloquea por seguridad los checkouts desde rutas locales. Para permitirlo se añadió la siguiente propiedad:

-Dhudson.plugins.git.GitSCM.ALLOW_LOCAL_CHECKOUT=true

En Arch Linux se editó el fichero de configuración de Jenkins:

sudo nano /etc/conf.d/jenkins

Se configuró la variable JAVA_ARGS así:

JAVA_ARGS="-Xmx512m -Dhudson.plugins.git.GitSCM.ALLOW_LOCAL_CHECKOUT=true"

Después se reinició Jenkins:

sudo systemctl daemon-reload
sudo systemctl restart jenkins

Con esto Jenkins pudo hacer checkout desde el repositorio local.

Creación de ficheros de automatización

Dentro del repositorio se crearon los ficheros necesarios para CI/CD:

cd /home/inaki/Documents/2ASIR3/optativa/recu/virtualtabletop-dev
mkdir -p ci k8s

La estructura creada fue:

virtualtabletop-dev/
├── Jenkinsfile
├── ci/
│   └── docker-compose.local.yml
└── k8s/
    ├── deployment.yaml
    └── service.yaml

Fichero Docker Compose

Se creó el fichero:

ci/docker-compose.local.yml

Contenido:

services:
  virtualtabletop:
    image: ${APP_IMAGE:-virtualtabletop-local:latest}
    container_name: virtualtabletop-local
    ports:
      - "8272:8272"
    volumes:
      - vtt-save:/app/save
    restart: unless-stopped

volumes:
  vtt-save:

Este fichero corresponde al apartado básico de la práctica.

Su función es:

  • Levantar el contenedor virtualtabletop-local.
  • Publicar el puerto 8272.
  • Usar la imagen generada por Jenkins.
  • Mantener datos en un volumen Docker.
  • Reiniciar el contenedor automáticamente salvo que se detenga manualmente.

Fichero Kubernetes Deployment

Se creó el fichero:

k8s/deployment.yaml

Contenido:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vtt-deployment
  labels:
    app: virtualtabletop
spec:
  replicas: 3
  selector:
    matchLabels:
      app: virtualtabletop
  template:
    metadata:
      labels:
        app: virtualtabletop
    spec:
      containers:
        - name: virtualtabletop
          image: IMAGE_PLACEHOLDER
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8272
          readinessProbe:
            httpGet:
              path: /
              port: 8272
            initialDelaySeconds: 10
            periodSeconds: 5
            failureThreshold: 6
          livenessProbe:
            httpGet:
              path: /
              port: 8272
            initialDelaySeconds: 30
            periodSeconds: 10
            failureThreshold: 3

Este fichero implementa el apartado avanzado de la práctica.

La línea principal es:

replicas: 3

Esto indica que Kubernetes debe mantener siempre tres réplicas de la aplicación.

También se han añadido dos comprobaciones:

readinessProbe
livenessProbe

La readinessProbe permite saber cuándo el Pod está preparado para recibir tráfico.

La livenessProbe permite detectar si el contenedor deja de responder.

Fichero Kubernetes Service

Se creó el fichero:

k8s/service.yaml

Contenido:

apiVersion: v1
kind: Service
metadata:
  name: vtt-service
  labels:
    app: virtualtabletop
spec:
  type: NodePort
  selector:
    app: virtualtabletop
  ports:
    - name: http
      port: 8272
      targetPort: 8272
      nodePort: 30080

Este Service expone la aplicación desplegada en Kubernetes.

La aplicación queda accesible usando el puerto:

30080

La URL de acceso tiene esta forma:

http://IP_DE_MINIKUBE:30080

El Service balancea las peticiones entre los Pods que tienen esta etiqueta:

app: virtualtabletop

Jenkinsfile

Se creó el fichero:

Jenkinsfile

Contenido completo:

pipeline {
    agent any

    options {
        skipDefaultCheckout(true)
        timestamps()
        disableConcurrentBuilds()
    }

    triggers {
        pollSCM('* * * * *')
    }

    environment {
        IMAGE_NAME = 'virtualtabletop-local'
        IMAGE_TAG = "${BUILD_NUMBER}"
        CONTAINER_NAME = 'virtualtabletop-local'
        HOST_PORT = '8272'

        MINIKUBE_PROFILE = 'vtt-ci'
        KUBECONFIG = '/var/lib/jenkins/.kube/config'
        HOME = '/var/lib/jenkins'
    }

    stages {
        stage('1. Clonar repositorio') {
            steps {
                checkout scm
                sh '''
                    echo "Repositorio clonado por Jenkins:"
                    pwd
                    git remote -v
                    git log -1 --oneline
                    test -f Dockerfile
                    test -f package.json
                    test -f server.mjs
                '''
            }
        }

        stage('2. Construir imagen Docker') {
            steps {
                sh '''
                    docker build --pull \
                      -t ${IMAGE_NAME}:${IMAGE_TAG} \
                      -t ${IMAGE_NAME}:latest \
                      .
                '''
            }
        }

        stage('3. Validar imagen') {
            steps {
                sh '''
                    docker image inspect ${IMAGE_NAME}:${IMAGE_TAG} >/dev/null
                    docker run --rm --entrypoint node ${IMAGE_NAME}:${IMAGE_TAG} --check server.mjs
                '''
            }
        }

        stage('4. Despliegue básico con Docker Compose') {
            steps {
                sh '''
                    docker compose -f ci/docker-compose.local.yml down --remove-orphans || true
                    APP_IMAGE=${IMAGE_NAME}:${IMAGE_TAG} docker compose -f ci/docker-compose.local.yml up -d

                    echo "Esperando a VirtualTableTop en Docker local..."
                    for i in $(seq 1 30); do
                        if curl -fsS http://127.0.0.1:${HOST_PORT}/ >/dev/null; then
                            echo "Servicio Docker local OK: http://localhost:${HOST_PORT}"
                            exit 0
                        fi
                        sleep 2
                    done

                    echo "Fallo arrancando servicio local"
                    docker logs ${CONTAINER_NAME} --tail 100 || true
                    exit 1
                '''
            }
        }

        stage('5. Despliegue avanzado en Kubernetes') {
            steps {
                sh '''
                    minikube -p ${MINIKUBE_PROFILE} status

                    minikube -p ${MINIKUBE_PROFILE} image load ${IMAGE_NAME}:${IMAGE_TAG}

                    sed "s#IMAGE_PLACEHOLDER#${IMAGE_NAME}:${IMAGE_TAG}#g" \
                      k8s/deployment.yaml > k8s/deployment.rendered.yaml

                    kubectl --context ${MINIKUBE_PROFILE} apply -f k8s/deployment.rendered.yaml
                    kubectl --context ${MINIKUBE_PROFILE} apply -f k8s/service.yaml

                    kubectl --context ${MINIKUBE_PROFILE} rollout status deployment/vtt-deployment --timeout=180s

                    echo "Pods:"
                    kubectl --context ${MINIKUBE_PROFILE} get pods -l app=virtualtabletop -o wide

                    echo "Servicio:"
                    kubectl --context ${MINIKUBE_PROFILE} get svc vtt-service
                '''
            }
        }

        stage('6. Prueba de acceso Kubernetes') {
            steps {
                sh '''
                    NODE_IP=$(minikube -p ${MINIKUBE_PROFILE} ip)
                    URL="http://${NODE_IP}:30080"

                    echo "Probando $URL"
                    for i in $(seq 1 30); do
                        if curl -fsS "$URL/" >/dev/null; then
                            echo "Servicio Kubernetes OK: $URL"
                            exit 0
                        fi
                        sleep 2
                    done

                    echo "Fallo accediendo al servicio Kubernetes"
                    kubectl --context ${MINIKUBE_PROFILE} get all
                    exit 1
                '''
            }
        }

        stage('7. Demostración de autorrecuperación') {
            steps {
                sh '''
                    echo "Eliminando un pod para demostrar que Kubernetes lo recrea:"
                    POD=$(kubectl --context ${MINIKUBE_PROFILE} get pods -l app=virtualtabletop -o jsonpath='{.items[0].metadata.name}')
                    kubectl --context ${MINIKUBE_PROFILE} delete pod "$POD"

                    sleep 10

                    echo "Estado posterior:"
                    kubectl --context ${MINIKUBE_PROFILE} get pods -l app=virtualtabletop -o wide
                '''
            }
        }
    }

    post {
        always {
            sh '''
                echo "Resumen Docker:"
                docker ps --filter name=${CONTAINER_NAME} || true

                echo "Resumen Kubernetes:"
                kubectl --context ${MINIKUBE_PROFILE} get deployment,svc,pods -l app=virtualtabletop || true
            '''
        }

        success {
            echo 'Pipeline completado correctamente.'
        }

        failure {
            echo 'Pipeline fallido. Revisar consola Jenkins.'
        }
    }
}

Explicación del pipeline

El pipeline está dividido en siete fases principales.

Fase 1: clonar repositorio

stage('1. Clonar repositorio')

Jenkins obtiene el código desde el repositorio configurado en el job:

file:///home/inaki/Documents/2ASIR3/optativa/recu/virtualtabletop-dev

También comprueba que existan los ficheros necesarios:

Dockerfile
package.json
server.mjs

Esta fase demuestra que Jenkins es capaz de clonar el proyecto automáticamente.

Fase 2: construir imagen Docker

stage('2. Construir imagen Docker')

Construye una imagen Docker nueva:

docker build --pull \
  -t virtualtabletop-local:${BUILD_NUMBER} \
  -t virtualtabletop-local:latest \
  .

Cada ejecución de Jenkins genera una etiqueta distinta usando el número de build.

Ejemplo:

virtualtabletop-local:1
virtualtabletop-local:2
virtualtabletop-local:3

Fase 3: validar imagen

stage('3. Validar imagen')

Comprueba que la imagen existe:

docker image inspect virtualtabletop-local:${BUILD_NUMBER}

También valida que server.mjs no tenga errores de sintaxis:

docker run --rm --entrypoint node virtualtabletop-local:${BUILD_NUMBER} --check server.mjs

Fase 4: despliegue básico con Docker Compose

stage('4. Despliegue básico con Docker Compose')

Primero detiene contenedores anteriores:

docker compose -f ci/docker-compose.local.yml down --remove-orphans

Después levanta la nueva versión:

APP_IMAGE=virtualtabletop-local:${BUILD_NUMBER} docker compose -f ci/docker-compose.local.yml up -d

Finalmente comprueba que el servicio web responde:

curl -fsS http://127.0.0.1:8272/

Con esto se cumple el apartado básico de la práctica.

Fase 5: despliegue avanzado en Kubernetes

stage('5. Despliegue avanzado en Kubernetes')

Primero comprueba que Minikube está funcionando:

minikube -p vtt-ci status

Después carga la imagen generada por Jenkins dentro de Minikube:

minikube -p vtt-ci image load virtualtabletop-local:${BUILD_NUMBER}

Luego genera el manifiesto final de Kubernetes sustituyendo IMAGE_PLACEHOLDER por la imagen real:

sed "s#IMAGE_PLACEHOLDER#virtualtabletop-local:${BUILD_NUMBER}#g" \
  k8s/deployment.yaml > k8s/deployment.rendered.yaml

Finalmente aplica el Deployment y el Service:

kubectl --context vtt-ci apply -f k8s/deployment.rendered.yaml
kubectl --context vtt-ci apply -f k8s/service.yaml

Y espera a que el despliegue termine correctamente:

kubectl --context vtt-ci rollout status deployment/vtt-deployment --timeout=180s

Fase 6: prueba de acceso Kubernetes

stage('6. Prueba de acceso Kubernetes')

Obtiene la IP de Minikube:

minikube -p vtt-ci ip

Después prueba la URL:

http://IP_DE_MINIKUBE:30080

La prueba se realiza con:

curl -fsS "$URL/"

Fase 7: demostración de autorrecuperación

stage('7. Demostración de autorrecuperación')

Esta fase obtiene el nombre de uno de los Pods:

kubectl --context vtt-ci get pods -l app=virtualtabletop -o jsonpath='{.items[0].metadata.name}'

Después lo elimina:

kubectl --context vtt-ci delete pod "$POD"

Kubernetes debe crear automáticamente otro Pod para mantener las 3 réplicas indicadas en el Deployment.

Commit de los ficheros CI/CD

Una vez creados los ficheros, se añadieron al repositorio:

cd /home/inaki/Documents/2ASIR3/optativa/recu/virtualtabletop-dev

git add Jenkinsfile ci/docker-compose.local.yml k8s/deployment.yaml k8s/service.yaml
git commit -m "Añadir CI/CD local con Jenkins Docker y Kubernetes"

Se comprobó el historial:

git log --oneline -5

Este commit contiene los ficheros necesarios para que Jenkins pueda automatizar el despliegue.

Preparación de Minikube

El cluster Kubernetes local se levantó con Minikube.

Es importante que Minikube se ejecute con el usuario jenkins, porque Jenkins será quien ejecute los comandos de Kubernetes durante el pipeline.

Comando usado:

sudo -u jenkins -H minikube start --profile vtt-ci --driver=docker

Comprobación del estado:

sudo -u jenkins -H minikube -p vtt-ci status

Comprobación del nodo:

sudo -u jenkins -H kubectl --context vtt-ci get nodes

Salida obtenida:

NAME     STATUS   ROLES           AGE   VERSION
vtt-ci   Ready    control-plane   7m    v1.35.1

Captura:

Minikube Ready

Esta captura demuestra que el cluster Kubernetes local está funcionando.

Configuración del job en Jenkins

Se creó un nuevo trabajo en Jenkins.

Configuración:

New Item
Nombre: SpinardiInaki_Virtualtabletop
Tipo: Pipeline

Configuración del pipeline:

Definition: Pipeline script from SCM
SCM: Git
Repository URL: file:///home/inaki/Documents/2ASIR3/optativa/recu/virtualtabletop-dev
Branch Specifier: */main
Script Path: Jenkinsfile

Captura:

Configuración del job Jenkins

Primera ejecución manual del pipeline

La primera ejecución se lanzó manualmente desde Jenkins usando:

Build Now

La ejecución terminó correctamente.

Captura:

Build manual correcto

También se revisó la consola del build.

Captura:

Consola checkout y build

Esto demuestra que Jenkins ejecuta todo el proceso de forma automatizada.

Comprobación del despliegue básico con Docker local

Después de ejecutar el pipeline, se comprobó que el contenedor estuviera levantado:

docker ps

La salida debe mostrar el contenedor:

virtualtabletop-local

También debe verse el puerto:

8272:8272

Captura:

Docker ps

Después se comprobó el acceso HTTP:

curl -I http://localhost:8272

La salida correcta debe ser similar a:

HTTP/1.1 200 OK

Se abrió la aplicación en el navegador:

http://localhost:8272

Captura:

VirtualTableTop en Docker local

Con esto queda demostrado el apartado básico:

  • Jenkins clona el repositorio.
  • Jenkins construye la imagen.
  • Jenkins levanta el contenedor.
  • La aplicación web queda accesible desde el navegador.

Comprobación del despliegue en Kubernetes

Se comprobó el estado del Deployment:

sudo -u jenkins -H kubectl --context vtt-ci get deployment

Salida esperada:

NAME             READY   UP-TO-DATE   AVAILABLE
vtt-deployment   3/3     3            3

Se comprobaron los Pods:

sudo -u jenkins -H kubectl --context vtt-ci get pods -o wide

Salida esperada:

NAME                              READY   STATUS    RESTARTS
vtt-deployment-xxxxxxxxxx-xxxxx   1/1     Running   0
vtt-deployment-xxxxxxxxxx-xxxxx   1/1     Running   0
vtt-deployment-xxxxxxxxxx-xxxxx   1/1     Running   0

Se comprobó el Service:

sudo -u jenkins -H kubectl --context vtt-ci get svc

Salida esperada:

NAME          TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)
vtt-service   NodePort   ...             <none>        8272:30080/TCP

Captura:

Kubernetes con tres Pods

Esta captura es una de las más importantes de la entrega, porque demuestra que Kubernetes está ejecutando 3 réplicas.

Acceso a la aplicación desde Kubernetes

Se obtuvo la IP de Minikube:

sudo -u jenkins -H minikube -p vtt-ci ip

Se construyó la URL de acceso:

URL="http://$(sudo -u jenkins -H minikube -p vtt-ci ip):30080"
echo "$URL"

Ejemplo:

http://192.168.49.2:30080

Se comprobó con curl:

curl -I "$URL"

Salida esperada:

HTTP/1.1 200 OK

Después se abrió en el navegador.

Captura:

VirtualTableTop en Kubernetes

Esta captura demuestra que el Service de Kubernetes está exponiendo la aplicación correctamente.

Demostración de balanceo y autorrecuperación

Para demostrar que Kubernetes mantiene las réplicas, primero se listaron los Pods:

sudo -u jenkins -H kubectl --context vtt-ci get pods

Después se eliminó uno de los Pods:

POD=$(sudo -u jenkins -H kubectl --context vtt-ci get pods -l app=virtualtabletop -o jsonpath='{.items[0].metadata.name}')
sudo -u jenkins -H kubectl --context vtt-ci delete pod "$POD"

Captura:

Eliminación de un Pod

Después se comprobó que Kubernetes creaba otro Pod automáticamente:

sudo -u jenkins -H kubectl --context vtt-ci get pods -o wide

Captura:

Pod recreado automáticamente

En esta captura se ve que vuelven a existir 3 Pods.

Esto demuestra que Kubernetes mantiene el estado deseado indicado en el Deployment:

replicas: 3

Si un Pod falla o se elimina, Kubernetes crea otro para recuperar el número de réplicas configurado.

Prueba de cambio automático mediante commit

Para demostrar la automatización, se modificó la interfaz web añadiendo un banner visible.

Se ejecutó:

cd /home/inaki/Documents/2ASIR3/optativa/recu/virtualtabletop-dev

sed -i '/<body class="loading">/a\ <div id="ciBanner" style="position:fixed;z-index:99999;top:0;left:0;right:0;background:#fff3cd;color:#000;padding:8px;text-align:center;font-weight:bold">Despliegue automático Jenkins ASIR</div>' client/room.html

Se revisó el cambio:

git diff client/room.html

Después se hizo commit:

git add client/room.html
git commit -m "Cambiar interfaz para probar despliegue automático"

Captura:

Commit de cambio

Después del commit, Jenkins lanzó automáticamente una nueva ejecución del pipeline.

Captura:

Build automático Jenkins

En la consola de Jenkins se ve que el build se ejecutó tras detectar un cambio SCM.

Esto demuestra que Jenkins no depende únicamente del botón Build Now.

Comprobación final del cambio desplegado automáticamente

Después del build automático, se volvió a abrir la aplicación.

Acceso por Docker local:

http://localhost:8272

Acceso por Kubernetes:

URL="http://$(sudo -u jenkins -H minikube -p vtt-ci ip):30080"
echo "$URL"

En la web debe verse el banner añadido:

Despliegue automático Jenkins ASIR

Captura:

Web con cambio automático

Esta captura demuestra el ciclo completo:

  1. Se realiza un cambio en el código.
  2. Se hace commit.
  3. Jenkins detecta el commit.
  4. Jenkins reconstruye la imagen Docker.
  5. Jenkins detiene el contenedor anterior.
  6. Jenkins levanta la nueva versión.
  7. Jenkins actualiza Kubernetes.
  8. La web muestra el cambio desplegado.

Problemas encontrados y soluciones aplicadas

Durante la realización de la práctica aparecieron varios problemas.

Minikube descargado incorrectamente

Inicialmente el comando minikube fallaba con un error similar a:

/usr/local/bin/minikube: rivi 1: lauseoppivirhe lähellä odottamatonta avainsanaa "<"
<?xml version='1.0' encoding='UTF-8'?><Error><Code>NoSuchKey</Code><Message>The specified key does not exist.</Message>

La causa era que /usr/local/bin/minikube no era el binario correcto, sino un XML descargado incorrectamente.

Se comprobó con:

which minikube
file /usr/local/bin/minikube
head -n 3 /usr/local/bin/minikube

Solución aplicada:

sudo rm -f /usr/local/bin/minikube
sudo pacman -S --needed minikube kubectl

Después se comprobó:

which minikube
minikube version

El resultado correcto fue que Minikube se ejecutaba desde:

/usr/bin/minikube

Jenkins no podía acceder a Docker

Jenkins mostraba este error:

permission denied while trying to connect to the docker API at unix:///var/run/docker.sock

La causa era que el usuario jenkins no tenía permisos efectivos sobre el socket de Docker.

Solución aplicada:

sudo usermod -aG docker jenkins
sudo systemctl restart docker
sudo systemctl restart jenkins

Comprobación:

sudo -u jenkins -H docker ps

La salida correcta mostró la tabla de contenedores, por lo que Jenkins ya tenía acceso a Docker.

Jenkins bloqueaba el checkout local

Jenkins mostraba este error:

Checkout of Git remote 'file:///home/inaki/Documents/2ASIR3/optativa/recu/virtualtabletop-dev' aborted because it references a local directory, which may be insecure.

La causa era que Jenkins bloquea repositorios locales por seguridad.

Solución aplicada en Arch Linux:

sudo nano /etc/conf.d/jenkins

Se configuró:

JAVA_ARGS="-Xmx512m -Dhudson.plugins.git.GitSCM.ALLOW_LOCAL_CHECKOUT=true"

Después se reinició Jenkins:

sudo systemctl daemon-reload
sudo systemctl restart jenkins

Se comprobó:

ps aux | grep '[j]enkins.war'

Debía aparecer:

-Dhudson.plugins.git.GitSCM.ALLOW_LOCAL_CHECKOUT=true

Tras esto, Jenkins pudo clonar el repositorio local correctamente.

Minikube debía ejecutarse con el usuario Jenkins

El pipeline ejecuta comandos como:

minikube -p vtt-ci status
kubectl --context vtt-ci get nodes

Por eso Minikube se inició con el usuario jenkins:

sudo -u jenkins -H minikube start --profile vtt-ci --driver=docker

De esta forma, Jenkins tiene acceso directo al contexto Kubernetes correcto.

Pruebas finales realizadas

Se realizaron las siguientes comprobaciones finales.

Docker local

docker ps
curl -I http://localhost:8272

Resultado esperado:

HTTP/1.1 200 OK

Kubernetes

sudo -u jenkins -H kubectl --context vtt-ci get nodes
sudo -u jenkins -H kubectl --context vtt-ci get deployment
sudo -u jenkins -H kubectl --context vtt-ci get pods -o wide
sudo -u jenkins -H kubectl --context vtt-ci get svc

Resultado esperado:

vtt-deployment   3/3

Acceso web desde Kubernetes

URL="http://$(sudo -u jenkins -H minikube -p vtt-ci ip):30080"
echo "$URL"
curl -I "$URL"

Resultado esperado:

HTTP/1.1 200 OK

Automatización por commit

git log --oneline -5

Después del commit, Jenkins ejecutó un nuevo build automáticamente.

La web mostró el cambio:

Despliegue automático Jenkins ASIR

Ficheros entregados

Junto a esta memoria se entregan los ficheros necesarios para reproducir el ejercicio:

Jenkinsfile
ci/docker-compose.local.yml
k8s/deployment.yaml
k8s/service.yaml

No se entrega el código fuente completo de VirtualTableTop, ya que el enunciado indica que deben entregarse los ficheros Jenkins y demás necesarios, excepto el código de VirtualTableTop.

La estructura de entrega queda así:

entrega/
├── memoria.pdf
├── Jenkinsfile
├── ci/
│   └── docker-compose.local.yml
└── k8s/
    ├── deployment.yaml
    └── service.yaml

Conclusión

La práctica se ha completado correctamente.

Se ha implementado una solución CI/CD local usando Jenkins en Arch Linux. Jenkins clona el repositorio, construye la imagen Docker, valida la imagen, detiene el contenedor anterior y levanta la nueva versión de VirtualTableTop mediante Docker Compose.

También se ha implementado el despliegue avanzado en Kubernetes usando Minikube. La aplicación se ejecuta mediante un Deployment con 3 réplicas y queda expuesta mediante un Service de tipo NodePort. Además, se ha demostrado que Kubernetes recrea automáticamente los Pods eliminados para mantener el estado deseado.

Finalmente, se ha implementado la automatización por commit. Al realizar un cambio en el repositorio y hacer commit, Jenkins detecta el cambio y ejecuta de nuevo el pipeline, desplegando la nueva versión sin intervención manual.

Por tanto, se cumplen los tres niveles solicitados:

NivelRequisitoEstado
BásicoJenkins clona el repositorio, construye y levanta el servicio webCompletado
AvanzadoKubernetes con 3 réplicas y balanceo mediante ServiceCompletado
Avanzado++Redespliegue automático al realizar commitCompletado

Anexo: comandos principales usados

Instalar herramientas

sudo pacman -Syu
sudo pacman -S --needed git docker docker-compose jenkins jdk21-openjdk curl minikube kubectl

Activar Docker

sudo systemctl enable --now docker
sudo usermod -aG docker inaki
sudo usermod -aG docker jenkins

Activar Jenkins

sudo systemctl enable --now jenkins
sudo systemctl status jenkins --no-pager

Clonar repositorio

cd /home/inaki/Documents/2ASIR3/optativa/recu
git clone https://github.com/ArnoldSmith86/virtualtabletop.git virtualtabletop-dev

Permisos para Jenkins

sudo chmod o+x /home/inaki
sudo chmod o+x /home/inaki/Documents
sudo chmod o+x /home/inaki/Documents/2ASIR3
sudo chmod o+x /home/inaki/Documents/2ASIR3/optativa
sudo chmod o+x /home/inaki/Documents/2ASIR3/optativa/recu
sudo chmod -R a+rX /home/inaki/Documents/2ASIR3/optativa/recu/virtualtabletop-dev
sudo -u jenkins git config --global --add safe.directory /home/inaki/Documents/2ASIR3/optativa/recu/virtualtabletop-dev

Permitir checkout local en Jenkins

sudo nano /etc/conf.d/jenkins

Añadir o modificar:

JAVA_ARGS="-Xmx512m -Dhudson.plugins.git.GitSCM.ALLOW_LOCAL_CHECKOUT=true"

Reiniciar:

sudo systemctl daemon-reload
sudo systemctl restart jenkins

Arrancar Minikube como Jenkins

sudo -u jenkins -H minikube start --profile vtt-ci --driver=docker
sudo -u jenkins -H kubectl --context vtt-ci get nodes

Comprobar Docker local

docker ps
curl -I http://localhost:8272

Comprobar Kubernetes

sudo -u jenkins -H kubectl --context vtt-ci get deployment
sudo -u jenkins -H kubectl --context vtt-ci get pods -o wide
sudo -u jenkins -H kubectl --context vtt-ci get svc

Acceder a Kubernetes

URL="http://$(sudo -u jenkins -H minikube -p vtt-ci ip):30080"
echo "$URL"
curl -I "$URL"

Eliminar un Pod para probar autorrecuperación

POD=$(sudo -u jenkins -H kubectl --context vtt-ci get pods -l app=virtualtabletop -o jsonpath='{.items[0].metadata.name}')
sudo -u jenkins -H kubectl --context vtt-ci delete pod "$POD"
sudo -u jenkins -H kubectl --context vtt-ci get pods -o wide

Crear hook post-commit

nano /home/inaki/Documents/2ASIR3/optativa/recu/virtualtabletop-dev/.git/hooks/post-commit

Contenido:

#!/bin/sh
curl -s "http://localhost:8090/git/notifyCommit?url=file:///home/inaki/Documents/2ASIR3/optativa/recu/virtualtabletop-dev" >/dev/null || true

Permisos:

chmod +x /home/inaki/Documents/2ASIR3/optativa/recu/virtualtabletop-dev/.git/hooks/post-commit

Commit de prueba

cd /home/inaki/Documents/2ASIR3/optativa/recu/virtualtabletop-dev

sed -i '/<body class="loading">/a\ <div id="ciBanner" style="position:fixed;z-index:99999;top:0;left:0;right:0;background:#fff3cd;color:#000;padding:8px;text-align:center;font-weight:bold">Despliegue automático Jenkins ASIR</div>' client/room.html

git add client/room.html
git commit -m "Cambiar interfaz para probar despliegue automático"

Anexo: relación de capturas necesarias

Las capturas incluidas en la memoria son:

ImagenContenido
img/04-jenkins-servicio.pngJenkins funcionando
img/05-jenkins-job-config.pngConfiguración del job Jenkins
img/06-build-manual-ok.pngPrimera ejecución manual correcta
img/07-console-checkout-build.pngConsola del pipeline
img/08-docker-ps.pngContenedor Docker en ejecución
img/09-web-docker-local.pngWeb funcionando en Docker local
img/10-minikube-ready.pngMinikube y nodo Kubernetes Ready
img/11-kubernetes-3-pods.pngDeployment con 3 Pods
img/12-web-kubernetes.pngWeb funcionando desde Kubernetes
img/13-delete-pod.pngEliminación manual de un Pod
img/14-pod-recreado.pngPod recreado automáticamente
img/15-commit-cambio.pngCommit de cambio en la web
img/16-build-automatico.pngBuild automático en Jenkins
img/17-web-cambio-automatico.pngWeb mostrando el cambio desplegado

Anexo: ficheros de configuración

Jenkinsfile

pipeline {
    agent any

    options {
        skipDefaultCheckout(true)
        timestamps()
        disableConcurrentBuilds()
    }

    triggers {
        pollSCM('* * * * *')
    }

    environment {
        IMAGE_NAME = 'virtualtabletop-local'
        IMAGE_TAG = "${BUILD_NUMBER}"
        CONTAINER_NAME = 'virtualtabletop-local'
        HOST_PORT = '8272'

        MINIKUBE_PROFILE = 'vtt-ci'
        KUBECONFIG = '/var/lib/jenkins/.kube/config'
        HOME = '/var/lib/jenkins'
    }

    stages {
        stage('1. Clonar repositorio') {
            steps {
                checkout scm
                sh '''
                    echo "Repositorio clonado por Jenkins:"
                    pwd
                    git remote -v
                    git log -1 --oneline
                    test -f Dockerfile
                    test -f package.json
                    test -f server.mjs
                '''
            }
        }

        stage('2. Construir imagen Docker') {
            steps {
                sh '''
                    docker build --pull \
                      -t ${IMAGE_NAME}:${IMAGE_TAG} \
                      -t ${IMAGE_NAME}:latest \
                      .
                '''
            }
        }

        stage('3. Validar imagen') {
            steps {
                sh '''
                    docker image inspect ${IMAGE_NAME}:${IMAGE_TAG} >/dev/null
                    docker run --rm --entrypoint node ${IMAGE_NAME}:${IMAGE_TAG} --check server.mjs
                '''
            }
        }

        stage('4. Despliegue básico con Docker Compose') {
            steps {
                sh '''
                    docker compose -f ci/docker-compose.local.yml down --remove-orphans || true
                    APP_IMAGE=${IMAGE_NAME}:${IMAGE_TAG} docker compose -f ci/docker-compose.local.yml up -d

                    echo "Esperando a VirtualTableTop en Docker local..."
                    for i in $(seq 1 30); do
                        if curl -fsS http://127.0.0.1:${HOST_PORT}/ >/dev/null; then
                            echo "Servicio Docker local OK: http://localhost:${HOST_PORT}"
                            exit 0
                        fi
                        sleep 2
                    done

                    echo "Fallo arrancando servicio local"
                    docker logs ${CONTAINER_NAME} --tail 100 || true
                    exit 1
                '''
            }
        }

        stage('5. Despliegue avanzado en Kubernetes') {
            steps {
                sh '''
                    minikube -p ${MINIKUBE_PROFILE} status

                    minikube -p ${MINIKUBE_PROFILE} image load ${IMAGE_NAME}:${IMAGE_TAG}

                    sed "s#IMAGE_PLACEHOLDER#${IMAGE_NAME}:${IMAGE_TAG}#g" \
                      k8s/deployment.yaml > k8s/deployment.rendered.yaml

                    kubectl --context ${MINIKUBE_PROFILE} apply -f k8s/deployment.rendered.yaml
                    kubectl --context ${MINIKUBE_PROFILE} apply -f k8s/service.yaml

                    kubectl --context ${MINIKUBE_PROFILE} rollout status deployment/vtt-deployment --timeout=180s

                    echo "Pods:"
                    kubectl --context ${MINIKUBE_PROFILE} get pods -l app=virtualtabletop -o wide

                    echo "Servicio:"
                    kubectl --context ${MINIKUBE_PROFILE} get svc vtt-service
                '''
            }
        }

        stage('6. Prueba de acceso Kubernetes') {
            steps {
                sh '''
                    NODE_IP=$(minikube -p ${MINIKUBE_PROFILE} ip)
                    URL="http://${NODE_IP}:30080"

                    echo "Probando $URL"
                    for i in $(seq 1 30); do
                        if curl -fsS "$URL/" >/dev/null; then
                            echo "Servicio Kubernetes OK: $URL"
                            exit 0
                        fi
                        sleep 2
                    done

                    echo "Fallo accediendo al servicio Kubernetes"
                    kubectl --context ${MINIKUBE_PROFILE} get all
                    exit 1
                '''
            }
        }

        stage('7. Demostración de autorrecuperación') {
            steps {
                sh '''
                    echo "Eliminando un pod para demostrar que Kubernetes lo recrea:"
                    POD=$(kubectl --context ${MINIKUBE_PROFILE} get pods -l app=virtualtabletop -o jsonpath='{.items[0].metadata.name}')
                    kubectl --context ${MINIKUBE_PROFILE} delete pod "$POD"

                    sleep 10

                    echo "Estado posterior:"
                    kubectl --context ${MINIKUBE_PROFILE} get pods -l app=virtualtabletop -o wide
                '''
            }
        }
    }

    post {
        always {
            sh '''
                echo "Resumen Docker:"
                docker ps --filter name=${CONTAINER_NAME} || true

                echo "Resumen Kubernetes:"
                kubectl --context ${MINIKUBE_PROFILE} get deployment,svc,pods -l app=virtualtabletop || true
            '''
        }

        success {
            echo 'Pipeline completado correctamente.'
        }

        failure {
            echo 'Pipeline fallido. Revisar consola Jenkins.'
        }
    }
}

ci/docker-compose.local.yml

services:
  virtualtabletop:
    image: ${APP_IMAGE:-virtualtabletop-local:latest}
    container_name: virtualtabletop-local
    ports:
      - "8272:8272"
    volumes:
      - vtt-save:/app/save
    restart: unless-stopped

volumes:
  vtt-save:

k8s/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vtt-deployment
  labels:
    app: virtualtabletop
spec:
  replicas: 3
  selector:
    matchLabels:
      app: virtualtabletop
  template:
    metadata:
      labels:
        app: virtualtabletop
    spec:
      containers:
        - name: virtualtabletop
          image: IMAGE_PLACEHOLDER
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8272
          readinessProbe:
            httpGet:
              path: /
              port: 8272
            initialDelaySeconds: 10
            periodSeconds: 5
            failureThreshold: 6
          livenessProbe:
            httpGet:
              path: /
              port: 8272
            initialDelaySeconds: 30
            periodSeconds: 10
            failureThreshold: 3

k8s/service.yaml

apiVersion: v1
kind: Service
metadata:
  name: vtt-service
  labels:
    app: virtualtabletop
spec:
  type: NodePort
  selector:
    app: virtualtabletop
  ports:
    - name: http
      port: 8272
      targetPort: 8272
      nodePort: 30080