Automatización CI/CD con Docker y Jenkins: Construyendo un Pipeline Robusto
Este tutorial te guiará paso a paso en la creación de un pipeline de Integración Continua y Entrega Continua (CI/CD) utilizando Docker y Jenkins. Aprenderás a automatizar el proceso de construcción, prueba y despliegue de tus aplicaciones, garantizando consistencia y eficiencia en tus flujos de trabajo de desarrollo.
La integración y entrega continua (CI/CD) se ha convertido en una piedra angular del desarrollo de software moderno. Permite a los equipos entregar software de forma más rápida, segura y fiable. Combinar el poder de Jenkins, un servidor de automatización líder, con la portabilidad y el aislamiento de Docker, nos permite construir pipelines CI/CD extremadamente robustos y reproducibles.
En este tutorial, exploraremos cómo configurar un pipeline CI/CD completo utilizando estas dos herramientas, desde la construcción de imágenes Docker hasta el despliegue de contenedores.
🎯 ¿Por qué Docker y Jenkins para CI/CD?
La combinación de Docker y Jenkins ofrece ventajas significativas para un pipeline CI/CD:
- Consistencia de Entornos: Docker asegura que el entorno de construcción, prueba y despliegue sea idéntico en todas las etapas, eliminando problemas de "funciona en mi máquina".
- Aislamiento: Cada etapa del pipeline puede ejecutarse en su propio contenedor aislado, evitando conflictos de dependencias entre diferentes proyectos o pasos.
- Portabilidad: Las imágenes Docker son portátiles, lo que facilita el despliegue en diferentes entornos, ya sean servidores locales, la nube o clusters de orquestación.
- Escalabilidad: Jenkins puede ejecutar múltiples trabajos en paralelo utilizando agentes Docker, lo que mejora la eficiencia y el rendimiento del pipeline.
- Reproducibilidad: Los Dockerfiles y las configuraciones de Jenkins se versionan junto con tu código, garantizando la reproducibilidad del proceso de construcción y despliegue.
🛠️ Prerequisitos
Antes de empezar, asegúrate de tener lo siguiente instalado y configurado:
- Docker: Versión 19.03 o superior.
sudo apt update
sudo apt install docker.io -y
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker $USER # Esto permite ejecutar docker sin sudo, reinicia tu sesión
- Docker Compose: Versión 1.25.0 o superior (para configurar Jenkins).
sudo apt install docker-compose -y
- Git: Para clonar el repositorio de ejemplo.
sudo apt install git -y
- Un editor de texto de tu elección (VS Code, Sublime Text, Vim, etc.).
⚙️ Configuración Inicial de Jenkins con Docker
Vamos a desplegar Jenkins como un contenedor Docker. Esto nos permite tener un entorno de Jenkins limpio y fácil de gestionar. Utilizaremos Docker Compose para definir nuestra configuración.
1. Crear el Directorio de Trabajo
Crea un directorio para tu proyecto y entra en él:
mkdir jenkins-docker-cicd
cd jenkins-docker-cicd
2. Definir docker-compose.yml para Jenkins
Crea un archivo llamado docker-compose.yml en la raíz de tu proyecto con el siguiente contenido:
version: '3.8'
services:
jenkins:
image: jenkins/jenkins:lts
privileged: true
user: root
ports:
- 8080:8080
- 50000:50000
container_name: jenkins
volumes:
- ./jenkins_home:/var/jenkins_home
- /var/run/docker.sock:/var/run/docker.sock
- /usr/bin/docker:/usr/bin/docker
environment:
- DOCKER_HOST=tcp://docker:2375
networks:
- jenkins-network
docker-daemon:
image: docker:dind
privileged: true
container_name: docker-daemon
environment:
- DOCKER_TLS_CERTDIR=
networks:
- jenkins-network
networks:
jenkins-network:
driver: bridge
3. Iniciar Jenkins
Ejecuta Docker Compose para iniciar Jenkins:
docker-compose up -d
Espera unos minutos a que Jenkins se inicie. Puedes monitorear los logs:
docker-compose logs -f jenkins
4. Acceder a Jenkins
Una vez que Jenkins esté listo, abre tu navegador y ve a http://localhost:8080. Se te pedirá la contraseña inicial de administrador. Puedes obtenerla de los logs de Jenkins o del archivo en tu sistema de ficheros:
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
Copia y pega la contraseña en el campo correspondiente. A continuación, selecciona "Install suggested plugins" para instalar los plugins recomendados. Crea un usuario administrador cuando se te pida.
🌐 Preparando la Aplicación de Ejemplo
Para este tutorial, utilizaremos una aplicación web simple escrita en Python con Flask. El objetivo es construir una imagen Docker de esta aplicación, testearla y luego simular su despliegue.
1. Clonar el Repositorio de Ejemplo
Para simplificar, usaremos un repositorio de ejemplo:
git clone https://github.com/your-username/flask-sample-app.git # Reemplaza con tu propio fork o crea uno similar
cd flask-sample-app
Si no tienes un repositorio, puedes crear uno simple:
app.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello, CI/CD with Docker and Jenkins!'
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
requirements.txt
Flask==2.0.2
Dockerfile
FROM python:3.9-slim-buster
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]
2. Crear un Archivo de Test (Opcional pero recomendado)
Para demostrar una etapa de testing, crea un archivo test_app.py:
import unittest
from app import app
class MyTest(unittest.TestCase):
def setUp(self):
app.testing = True
self.app = app.test_client()
def test_hello_world(self):
response = self.app.get('/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.data.decode('utf-8'), 'Hello, CI/CD with Docker and Jenkins!')
if __name__ == '__main__':
unittest.main()
🚀 Creando el Pipeline CI/CD en Jenkins
Ahora es el momento de construir nuestro pipeline. Utilizaremos un Jenkinsfile, que permite definir el pipeline como código, versionándolo junto con la aplicación.
1. Entender la Estructura del Pipeline
Nuestro pipeline tendrá las siguientes etapas:
- Checkout: Obtener el código fuente del repositorio.
- Build Docker Image: Construir la imagen Docker de la aplicación.
- Test: Ejecutar pruebas unitarias o de integración dentro de un contenedor Docker.
- Push Docker Image (Opcional): Subir la imagen a un registro Docker (ej. Docker Hub).
- Deploy (Simulado): Desplegar la aplicación (en este caso, ejecutar el contenedor).
2. Crear el Jenkinsfile
En la raíz de tu repositorio flask-sample-app (o el que estés usando), crea un archivo llamado Jenkinsfile con el siguiente contenido:
// Jenkinsfile
pipeline {
agent { docker { image 'docker:dind' label 'docker' } }
environment {
IMAGE_NAME = 'my-flask-app'
DOCKER_REGISTRY = 'your-dockerhub-username' // Opcional: Reemplaza con tu usuario de Docker Hub
}
stages {
stage('Checkout') {
steps {
git 'https://github.com/your-username/flask-sample-app.git' // Reemplaza con la URL de tu repositorio
}
}
stage('Build Docker Image') {
steps {
script {
sh "docker build -t ${env.DOCKER_REGISTRY}/${env.IMAGE_NAME}:${env.BUILD_NUMBER} ."
sh "docker tag ${env.DOCKER_REGISTRY}/${env.IMAGE_NAME}:${env.BUILD_NUMBER} ${env.DOCKER_REGISTRY}/${env.IMAGE_NAME}:latest"
}
}
}
stage('Test Docker Image') {
steps {
script {
// Ejecutar los tests dentro de un nuevo contenedor a partir de la imagen construida
// Montamos el volumen actual para que los tests se encuentren
sh "docker run --rm -v $(pwd):/app ${env.DOCKER_REGISTRY}/${env.IMAGE_NAME}:${env.BUILD_NUMBER} python -m unittest test_app.py"
}
}
}
stage('Push Docker Image') {
when {
expression { env.BRANCH_NAME == 'main' }
}
steps {
script {
// Asegúrate de haber configurado tus credenciales de Docker Hub en Jenkins
// 'dockerhub-creds' es el ID de las credenciales en Jenkins
withCredentials([usernamePassword(credentialsId: 'dockerhub-creds', passwordVariable: 'DOCKER_PASSWORD', usernameVariable: 'DOCKER_USERNAME')]) {
sh "echo \"${DOCKER_PASSWORD}\" | docker login -u \"${DOCKER_USERNAME}\" --password-stdin"
sh "docker push ${env.DOCKER_REGISTRY}/${env.IMAGE_NAME}:${env.BUILD_NUMBER}"
sh "docker push ${env.DOCKER_REGISTRY}/${env.IMAGE_NAME}:latest"
sh "docker logout"
}
}
}
}
stage('Deploy') {
steps {
script {
// Simulación de despliegue: detener contenedor anterior y ejecutar el nuevo
sh "docker stop my-flask-app-container || true"
sh "docker rm my-flask-app-container || true"
sh "docker run -d --name my-flask-app-container -p 80:5000 ${env.DOCKER_REGISTRY}/${env.IMAGE_NAME}:${env.BUILD_NUMBER}"
echo "Aplicación desplegada en http://localhost:80"
}
}
}
}
post {
always {
echo 'Pipeline finalizado.'
script {
// Limpiar imágenes intermedias si es necesario
sh 'docker system prune -f --volumes'
}
}
success {
echo '¡El pipeline se ejecutó con éxito!'
}
failure {
echo '¡El pipeline falló!'
}
}
}
3. Configurar Credenciales en Jenkins (para Docker Hub, si aplica)
Si vas a usar la etapa Push Docker Image, necesitarás configurar tus credenciales de Docker Hub en Jenkins:
- En el dashboard de Jenkins, ve a "Manage Jenkins" > "Manage Credentials".
- En la columna izquierda, haz clic en "(global)".
- Haz clic en "Add Credentials".
- Selecciona el tipo: "Username with password".
- Rellena:
- Username: Tu nombre de usuario de Docker Hub.
- Password: Tu token de acceso o contraseña de Docker Hub (es preferible un token).
- ID:
dockerhub-creds(debe coincidir con elcredentialsIden elJenkinsfile). - Description: Una descripción clara.
- Haz clic en "Create".
4. Crear un Nuevo Job de Pipeline en Jenkins
- En el dashboard de Jenkins, haz clic en "New Item".
- Introduce un nombre para tu pipeline (ej.
Flask-App-CI-CD). - Selecciona "Pipeline" como tipo de proyecto.
- Haz clic en "OK".
5. Configurar el Job de Pipeline
En la página de configuración del job:
-
Marca "GitHub hook trigger for GITScm polling" si quieres que Jenkins se dispare automáticamente con cada push a tu repositorio.
-
En la sección "Pipeline":
- Selecciona "Pipeline script from SCM".
- SCM:
Git. - Repository URL: Introduce la URL de tu repositorio Git (ej.
https://github.com/your-username/flask-sample-app.git). - Credentials: Si tu repositorio es privado, añade tus credenciales Git aquí. Para repositorios públicos, no es necesario.
- Branches to build:
*/main(o la rama que uses, ej.*/master). - Script Path:
Jenkinsfile(asegúrate de que este es el nombre de tu archivo).
-
Haz clic en "Save".
6. Ejecutar el Pipeline
Ahora, haz clic en "Build Now" en la barra lateral izquierda del job de Jenkins. Observa cómo el pipeline avanza a través de las diferentes etapas. Puedes hacer clic en el número de la construcción (ej. #1) y luego en "Console Output" para ver los logs detallados.
🔍 Análisis de las Etapas del Pipeline
Repasemos cada etapa para entender su función y cómo Docker facilita el proceso.
Checkout
git 'https://github.com/your-username/flask-sample-app.git'
Esta etapa clona el repositorio Git que contiene el Jenkinsfile y el código fuente de tu aplicación. Es el punto de partida para cualquier pipeline CI/CD.
Build Docker Image
sh "docker build -t ${env.DOCKER_REGISTRY}/${env.IMAGE_NAME}:${env.BUILD_NUMBER} ."
sh "docker tag ${env.DOCKER_REGISTRY}/${env.IMAGE_NAME}:${env.BUILD_NUMBER} ${env.DOCKER_REGISTRY}/${env.IMAGE_NAME}:latest"
Aquí, Jenkins ejecuta el comando docker build para crear una imagen Docker de tu aplicación. Utiliza el Dockerfile que definimos previamente. La imagen se etiqueta con el número de construcción de Jenkins (BUILD_NUMBER) para tener versiones únicas y con latest para la versión más reciente.
Test Docker Image
sh "docker run --rm -v $(pwd):/app ${env.DOCKER_REGISTRY}/${env.IMAGE_NAME}:${env.BUILD_NUMBER} python -m unittest test_app.py"
Esta es una parte crítica. En lugar de ejecutar tests directamente en el agente de Jenkins (que podría tener dependencias incompatibles), lanzamos un nuevo contenedor a partir de la imagen que acabamos de construir. Esto asegura que los tests se ejecuten en el mismo entorno que se desplegará. Usamos --rm para que el contenedor se elimine automáticamente después de su ejecución y -v $(pwd):/app para montar el código fuente local (donde se encuentran los tests) dentro del contenedor, si los tests no están dentro de la imagen ya.
Push Docker Image
withCredentials([...]) {
sh "echo \"${DOCKER_PASSWORD}\" | docker login -u \"${DOCKER_USERNAME}\" --password-stdin"
sh "docker push ..."
sh "docker logout"
}
Esta etapa, que solo se ejecuta para la rama main, sube la imagen Docker construida a un registro de contenedores (como Docker Hub). El bloque withCredentials inyecta las credenciales de forma segura, evitando exponerlas en los logs. Es esencial para que otros servicios o entornos puedan acceder a tu imagen.
Deploy
sh "docker stop my-flask-app-container || true"
sh "docker rm my-flask-app-container || true"
sh "docker run -d --name my-flask-app-container -p 80:5000 ${env.DOCKER_REGISTRY}/${env.IMAGE_NAME}:${env.BUILD_NUMBER}"
Aquí, simulamos un despliegue. Detenemos y eliminamos cualquier contenedor anterior de la aplicación y luego lanzamos un nuevo contenedor utilizando la imagen recién construida y probada. En un entorno de producción real, esta etapa interactuaría con un orquestador de contenedores como Kubernetes o Docker Swarm para un despliegue más robusto y escalable.
Después de que el pipeline se complete con éxito, deberías poder acceder a tu aplicación en http://localhost:80.
✨ Mejoras y Próximos Pasos
Este pipeline es un excelente punto de partida, pero hay muchas formas de mejorarlo y expandirlo:
- Integración de escaneo de seguridad: Añade herramientas como Trivy o Clair para escanear tus imágenes Docker en busca de vulnerabilidades antes del despliegue.
- Notificaciones: Configura notificaciones de Jenkins (correo electrónico, Slack) para informar al equipo sobre el estado de las construcciones.
- Despliegues avanzados: Integra con Kubernetes, Helm o Docker Swarm para despliegues más complejos, Canary deployments, Blue/Green deployments, etc.
- Testing más robusto: Añade tests de integración, tests de carga, tests de UI (usando herramientas como Selenium en contenedores).
- Optimización de Jenkinsfile: Utiliza plantillas, funciones compartidas y otras características avanzadas de Jenkins para hacer tus pipelines más DRY (Don't Repeat Yourself).
- Agentes Jenkins dinámicos: Configura Jenkins para que lance agentes Docker bajo demanda, ahorrando recursos.
¿Por qué usamos `docker:dind` como agente en Jenkins?
Usar `docker:dind` (Docker-in-Docker) como agente significa que cada etapa del pipeline se ejecuta dentro de un contenedor Docker que tiene su propio daemon Docker. Esto proporciona un aislamiento excelente entre los trabajos de Jenkins y el sistema host, garantizando que las dependencias de Docker para la construcción de imágenes no contaminen el host ni otros trabajos. Además, simplifica la configuración del agente, ya que no se necesita preinstalar Docker en la máquina del agente.
¿Cómo manejar diferentes entornos (desarrollo, staging, producción)?
Puedes extender este pipeline para manejar múltiples entornos usando:
- Parámetros de Jenkins: Permite seleccionar el entorno de despliegue al iniciar el trabajo.
- Branches específicas: Configura ramas como `develop`, `staging` y `main` (o `master`), y que el pipeline despliegue automáticamente a un entorno particular cuando se haga un push a esa rama.
- Configuraciones de entorno: Utiliza archivos de configuración específicos por entorno inyectados en los contenedores, o variables de entorno gestionadas por Jenkins.
✅ Conclusión
Has construido un pipeline CI/CD fundamental utilizando Jenkins y Docker. Esta combinación te permite automatizar el ciclo de vida de tu aplicación, desde la integración de código hasta el despliegue, de una manera consistente, reproducible y escalable. Dominar estas herramientas es un paso crucial hacia la implementación de prácticas DevOps eficientes en tus proyectos.
Ahora tienes las bases para explorar configuraciones más avanzadas y adaptar este enfoque a las necesidades específicas de tus proyectos.
Tutoriales relacionados
- Despliegue de Aplicaciones Multi-Contenedor con Docker Compose: Guía Completaintermediate18 min
- Asegurando Contenedores Docker: Mejores Prácticas y Herramientas Esencialesintermediate15 min
- Orquestación de Contenedores Docker con Docker Swarm: Escalabilidad y Alta Disponibilidadintermediate25 min
- Configurando un Registro Privado de Docker con Docker Registry y Nginx: Almacena tus Imágenes de Forma Seguraintermediate20 min
- Aislamiento y Gestión de Redes en Docker: Conectando Contenedores de Forma Seguraintermediate15 min
Comentarios (0)
Aún no hay comentarios. ¡Sé el primero!