Automatización con Jenkins
En esta página
- Objetivo de la práctica
- Proyecto seleccionado
- Tecnologías utilizadas
- Estructura general de la solución
- Ruta local de trabajo
- Instalación de herramientas necesarias en Arch Linux
- Instalación y arranque de Jenkins
- Clonado del repositorio
- Permisos para Jenkins
- Corrección del bloqueo de repositorios locales en Jenkins
- Creación de ficheros de automatización
- Fichero Docker Compose
- Fichero Kubernetes Deployment
- Fichero Kubernetes Service
- Jenkinsfile
- Explicación del pipeline
- Commit de los ficheros CI/CD
- Preparación de Minikube
- Configuración del job en Jenkins
- Primera ejecución manual del pipeline
- Comprobación del despliegue básico con Docker local
- Comprobación del despliegue en Kubernetes
- Acceso a la aplicación desde Kubernetes
- Demostración de balanceo y autorrecuperación
- Prueba de cambio automático mediante commit
- Comprobación final del cambio desplegado automáticamente
- Problemas encontrados y soluciones aplicadas
- Pruebas finales realizadas
- Ficheros entregados
- Conclusión
- Anexo: comandos principales usados
- Anexo: relación de capturas necesarias
- Anexo: ficheros de configuración
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:
- Clonar el repositorio del proyecto.
- Construir una imagen Docker de la aplicación.
- Levantar el servicio web mediante Docker Compose.
- Desplegar la aplicación en Kubernetes.
- Ejecutar 3 réplicas de la aplicación.
- Balancear carga mediante un Service de Kubernetes.
- Demostrar que, si se elimina un Pod, Kubernetes lo vuelve a crear automáticamente.
- 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/virtualtabletopSe 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ía | Uso |
|---|---|
| Arch Linux | Sistema operativo local |
| Git | Clonado del repositorio y control de commits |
| Jenkins | Automatización CI/CD |
| Docker | Construcción y ejecución de contenedores |
| Docker Compose | Despliegue local del servicio web |
| Minikube | Cluster Kubernetes local |
| kubectl | Administración del cluster Kubernetes |
| Kubernetes Deployment | Ejecución de 3 réplicas |
| Kubernetes Service | Exposición y balanceo de la aplicación |
| curl | Pruebas HTTP desde terminal |
Estructura general de la solución
La solución funciona de la siguiente forma:
- El repositorio de VirtualTableTop se clona en local.
- Jenkins lee el repositorio local usando Git.
- Jenkins ejecuta el fichero
Jenkinsfile. - El pipeline construye una imagen Docker nueva.
- Docker Compose detiene el contenedor anterior.
- Docker Compose levanta el nuevo contenedor.
- Jenkins comprueba que la web responde en el puerto
8272. - Jenkins carga la imagen Docker en Minikube.
- Jenkins despliega la aplicación en Kubernetes.
- Kubernetes ejecuta 3 réplicas de la aplicación.
- Kubernetes expone la aplicación con un Service de tipo
NodePort. - 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/recuPara 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 kubectlTambién se configuró Java 21 como versión por defecto:
sudo archlinux-java set java-21-openjdk
java -versionSe activó Docker:
sudo systemctl enable --now docker
sudo systemctl status dockerSe añadieron los usuarios inaki y jenkins al grupo docker:
sudo usermod -aG docker inaki
sudo usermod -aG docker jenkinsDespué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-worldEsta 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 jenkinsSe comprobó el estado:
sudo systemctl status jenkins --no-pagerEn esta práctica, Jenkins quedó accesible en:
http://localhost:8090La contraseña inicial se obtuvo con:
sudo cat /var/lib/jenkins/secrets/initialAdminPasswordDurante la configuración inicial se instalaron los plugins recomendados.
También se comprobó que estuvieran disponibles estos plugins:
- Git
- Pipeline
- Pipeline: Stage View
Captura:

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-devSe comprobó que el repositorio estuviera correctamente clonado:
git status
git branchTambién se comprobó la existencia de ficheros importantes del proyecto:
ls Dockerfile package.json server.mjsCaptura:

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-devPor 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/recuTambién se dieron permisos de lectura al repositorio:
sudo chmod -R a+rX /home/inaki/Documents/2ASIR3/optativa/recu/virtualtabletop-devSe 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-devSe comprobó que Jenkins pudiera leer el repositorio:
sudo -u jenkins -H git -C /home/inaki/Documents/2ASIR3/optativa/recu/virtualtabletop-dev statusCorrecció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=trueEn Arch Linux se editó el fichero de configuración de Jenkins:
sudo nano /etc/conf.d/jenkinsSe 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 jenkinsCon 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 k8sLa estructura creada fue:
virtualtabletop-dev/
├── Jenkinsfile
├── ci/
│ └── docker-compose.local.yml
└── k8s/
├── deployment.yaml
└── service.yamlFichero Docker Compose
Se creó el fichero:
ci/docker-compose.local.ymlContenido:
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.yamlContenido:
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: 3Este fichero implementa el apartado avanzado de la práctica.
La línea principal es:
replicas: 3Esto indica que Kubernetes debe mantener siempre tres réplicas de la aplicación.
También se han añadido dos comprobaciones:
readinessProbe
livenessProbeLa 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.yamlContenido:
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: 30080Este Service expone la aplicación desplegada en Kubernetes.
La aplicación queda accesible usando el puerto:
30080La URL de acceso tiene esta forma:
http://IP_DE_MINIKUBE:30080El Service balancea las peticiones entre los Pods que tienen esta etiqueta:
app: virtualtabletopJenkinsfile
Se creó el fichero:
JenkinsfileContenido 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-devTambién comprueba que existan los ficheros necesarios:
Dockerfile
package.json
server.mjsEsta 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:3Fase 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.mjsFase 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-orphansDespués levanta la nueva versión:
APP_IMAGE=virtualtabletop-local:${BUILD_NUMBER} docker compose -f ci/docker-compose.local.yml up -dFinalmente 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 statusDespué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.yamlFinalmente aplica el Deployment y el Service:
kubectl --context vtt-ci apply -f k8s/deployment.rendered.yaml
kubectl --context vtt-ci apply -f k8s/service.yamlY espera a que el despliegue termine correctamente:
kubectl --context vtt-ci rollout status deployment/vtt-deployment --timeout=180sFase 6: prueba de acceso Kubernetes
stage('6. Prueba de acceso Kubernetes')Obtiene la IP de Minikube:
minikube -p vtt-ci ipDespués prueba la URL:
http://IP_DE_MINIKUBE:30080La 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 -5Este 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=dockerComprobación del estado:
sudo -u jenkins -H minikube -p vtt-ci statusComprobación del nodo:
sudo -u jenkins -H kubectl --context vtt-ci get nodesSalida obtenida:
NAME STATUS ROLES AGE VERSION
vtt-ci Ready control-plane 7m v1.35.1Captura:

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: PipelineConfiguració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: JenkinsfileCaptura:

Primera ejecución manual del pipeline
La primera ejecución se lanzó manualmente desde Jenkins usando:
Build NowLa ejecución terminó correctamente.
Captura:

También se revisó la consola del build.
Captura:

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 psLa salida debe mostrar el contenedor:
virtualtabletop-localTambién debe verse el puerto:
8272:8272Captura:

Después se comprobó el acceso HTTP:
curl -I http://localhost:8272La salida correcta debe ser similar a:
HTTP/1.1 200 OKSe abrió la aplicación en el navegador:
http://localhost:8272Captura:

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 deploymentSalida esperada:
NAME READY UP-TO-DATE AVAILABLE
vtt-deployment 3/3 3 3Se comprobaron los Pods:
sudo -u jenkins -H kubectl --context vtt-ci get pods -o wideSalida 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 0Se comprobó el Service:
sudo -u jenkins -H kubectl --context vtt-ci get svcSalida esperada:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
vtt-service NodePort ... <none> 8272:30080/TCPCaptura:

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 ipSe 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:30080Se comprobó con curl:
curl -I "$URL"Salida esperada:
HTTP/1.1 200 OKDespués se abrió en el navegador.
Captura:

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 podsDespué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:

Después se comprobó que Kubernetes creaba otro Pod automáticamente:
sudo -u jenkins -H kubectl --context vtt-ci get pods -o wideCaptura:

En esta captura se ve que vuelven a existir 3 Pods.
Esto demuestra que Kubernetes mantiene el estado deseado indicado en el Deployment:
replicas: 3Si 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.htmlSe revisó el cambio:
git diff client/room.htmlDespués se hizo commit:
git add client/room.html
git commit -m "Cambiar interfaz para probar despliegue automático"Captura:

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

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:8272Acceso 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 ASIRCaptura:

Esta captura demuestra el ciclo completo:
- Se realiza un cambio en el código.
- Se hace commit.
- Jenkins detecta el commit.
- Jenkins reconstruye la imagen Docker.
- Jenkins detiene el contenedor anterior.
- Jenkins levanta la nueva versión.
- Jenkins actualiza Kubernetes.
- 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/minikubeSolución aplicada:
sudo rm -f /usr/local/bin/minikube
sudo pacman -S --needed minikube kubectlDespués se comprobó:
which minikube
minikube versionEl resultado correcto fue que Minikube se ejecutaba desde:
/usr/bin/minikubeJenkins 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.sockLa 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 jenkinsComprobación:
sudo -u jenkins -H docker psLa 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/jenkinsSe configuró:
JAVA_ARGS="-Xmx512m -Dhudson.plugins.git.GitSCM.ALLOW_LOCAL_CHECKOUT=true"Después se reinició Jenkins:
sudo systemctl daemon-reload
sudo systemctl restart jenkinsSe comprobó:
ps aux | grep '[j]enkins.war'Debía aparecer:
-Dhudson.plugins.git.GitSCM.ALLOW_LOCAL_CHECKOUT=trueTras 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 nodesPor eso Minikube se inició con el usuario jenkins:
sudo -u jenkins -H minikube start --profile vtt-ci --driver=dockerDe 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:8272Resultado esperado:
HTTP/1.1 200 OKKubernetes
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 svcResultado esperado:
vtt-deployment 3/3Acceso 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 OKAutomatización por commit
git log --oneline -5Después del commit, Jenkins ejecutó un nuevo build automáticamente.
La web mostró el cambio:
Despliegue automático Jenkins ASIRFicheros 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.yamlNo 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.yamlConclusió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:
| Nivel | Requisito | Estado |
|---|---|---|
| Básico | Jenkins clona el repositorio, construye y levanta el servicio web | Completado |
| Avanzado | Kubernetes con 3 réplicas y balanceo mediante Service | Completado |
| Avanzado++ | Redespliegue automático al realizar commit | Completado |
Anexo: comandos principales usados
Instalar herramientas
sudo pacman -Syu
sudo pacman -S --needed git docker docker-compose jenkins jdk21-openjdk curl minikube kubectlActivar Docker
sudo systemctl enable --now docker
sudo usermod -aG docker inaki
sudo usermod -aG docker jenkinsActivar Jenkins
sudo systemctl enable --now jenkins
sudo systemctl status jenkins --no-pagerClonar repositorio
cd /home/inaki/Documents/2ASIR3/optativa/recu
git clone https://github.com/ArnoldSmith86/virtualtabletop.git virtualtabletop-devPermisos 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-devPermitir checkout local en Jenkins
sudo nano /etc/conf.d/jenkinsAñadir o modificar:
JAVA_ARGS="-Xmx512m -Dhudson.plugins.git.GitSCM.ALLOW_LOCAL_CHECKOUT=true"Reiniciar:
sudo systemctl daemon-reload
sudo systemctl restart jenkinsArrancar Minikube como Jenkins
sudo -u jenkins -H minikube start --profile vtt-ci --driver=docker
sudo -u jenkins -H kubectl --context vtt-ci get nodesComprobar Docker local
docker ps
curl -I http://localhost:8272Comprobar 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 svcAcceder 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 wideCrear hook post-commit
nano /home/inaki/Documents/2ASIR3/optativa/recu/virtualtabletop-dev/.git/hooks/post-commitContenido:
#!/bin/sh
curl -s "http://localhost:8090/git/notifyCommit?url=file:///home/inaki/Documents/2ASIR3/optativa/recu/virtualtabletop-dev" >/dev/null || truePermisos:
chmod +x /home/inaki/Documents/2ASIR3/optativa/recu/virtualtabletop-dev/.git/hooks/post-commitCommit 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:
| Imagen | Contenido |
|---|---|
img/04-jenkins-servicio.png | Jenkins funcionando |
img/05-jenkins-job-config.png | Configuración del job Jenkins |
img/06-build-manual-ok.png | Primera ejecución manual correcta |
img/07-console-checkout-build.png | Consola del pipeline |
img/08-docker-ps.png | Contenedor Docker en ejecución |
img/09-web-docker-local.png | Web funcionando en Docker local |
img/10-minikube-ready.png | Minikube y nodo Kubernetes Ready |
img/11-kubernetes-3-pods.png | Deployment con 3 Pods |
img/12-web-kubernetes.png | Web funcionando desde Kubernetes |
img/13-delete-pod.png | Eliminación manual de un Pod |
img/14-pod-recreado.png | Pod recreado automáticamente |
img/15-commit-cambio.png | Commit de cambio en la web |
img/16-build-automatico.png | Build automático en Jenkins |
img/17-web-cambio-automatico.png | Web 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: 3k8s/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