tutoriales.com

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.

Intermedio20 min de lectura29 views
Reportar error

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.
💡 Consejo: Piensa en Docker como el contenedor estandarizado para tus aplicaciones y Jenkins como el director de orquesta que coordina todas las acciones sobre esos contenedores.

🛠️ 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
📌 Nota: Aquí estamos ejecutando Jenkins con `privileged: true` y montando `/var/run/docker.sock` y `/usr/bin/docker`. Esto permite que el contenedor de Jenkins interactúe con el *daemon* de Docker del host, y a su vez con el contenedor `docker-daemon` que actúa como un Docker-in-Docker (DinD). Esto es crucial para que Jenkins pueda construir y gestionar otros contenedores. La configuración de `docker-daemon` en el mismo `docker-compose.yml` proporciona un *daemon* de Docker dedicado para Jenkins si el host no es ideal, o si quieres un aislamiento total.

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:

  1. Checkout: Obtener el código fuente del repositorio.
  2. Build Docker Image: Construir la imagen Docker de la aplicación.
  3. Test: Ejecutar pruebas unitarias o de integración dentro de un contenedor Docker.
  4. Push Docker Image (Opcional): Subir la imagen a un registro Docker (ej. Docker Hub).
  5. Deploy (Simulado): Desplegar la aplicación (en este caso, ejecutar el contenedor).
Código Fuente (Git) Checkout Build Docker Image Test Docker Image Push Docker Image Deploy

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ó!'
        }
    }
}
⚠️ Advertencia: Reemplaza `https://github.com/your-username/flask-sample-app.git` con la URL real de tu repositorio y `your-dockerhub-username` con tu usuario de Docker Hub si vas a usar el paso de `Push Docker Image`. Asegúrate de hacer un `git push` de tu `Jenkinsfile` al repositorio.

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:

  1. En el dashboard de Jenkins, ve a "Manage Jenkins" > "Manage Credentials".
  2. En la columna izquierda, haz clic en "(global)".
  3. Haz clic en "Add Credentials".
  4. Selecciona el tipo: "Username with password".
  5. 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 el credentialsId en el Jenkinsfile).
    • Description: Una descripción clara.
  6. Haz clic en "Create".

4. Crear un Nuevo Job de Pipeline en Jenkins

  1. En el dashboard de Jenkins, haz clic en "New Item".
  2. Introduce un nombre para tu pipeline (ej. Flask-App-CI-CD).
  3. Selecciona "Pipeline" como tipo de proyecto.
  4. Haz clic en "OK".

5. Configurar el Job de Pipeline

En la página de configuración del job:

  1. Marca "GitHub hook trigger for GITScm polling" si quieres que Jenkins se dispare automáticamente con cada push a tu repositorio.

  2. 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).
  3. 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.

Pipeline completado (idealmente)

🔍 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.

🔥 Importante: El `agent { docker { image 'docker:dind' label 'docker' } }` en la parte superior del `Jenkinsfile` significa que todo el pipeline se ejecuta *dentro* de un contenedor `docker:dind`. Esto nos permite usar el `docker` CLI dentro del agente Jenkins sin tenerlo preinstalado en el agente, y el `docker-daemon` que montamos en el `docker-compose.yml` de Jenkins es el que realmente gestiona la construcción y ejecución de imágenes.

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

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!