
Creando contenedores Docker como agentes de construcción para Jenkins usando SSH
Introducción
El objetivo de este documento es servir de guía para configurar Jenkins para usar contenedores Docker como agentes de construcción.
Es muy común considerar los nodos y agentes de Jenkins como sinónimos, pero estrictamente hablando no lo son.
Un nodo Jenkins, también conocido como servidor Jenkins, es cualquier máquina (física o virtual) conectada a la red Jenkins o al entorno Jenkins. Los nodos proporcionan recursos computacionales y entornos para ejecutar trabajos de construcción de Jenkins. Tanto los controladores como los agentes se consideran nodos.
En resumen, podemos decir que los nodos de Jenkins son las máquinas en las que se ejecutan los agentes.
Usando contenedores Docker como agentes, puedes reducir y simplificar el proceso de creación de agentes: cada compilación inicia un nuevo contenedor, construye el proyecto y se destruye.
Una práctica común es crear contenedores Docker para ejecutar el proceso de construcción de la aplicación. En este documento, veremos cómo crear esos contenedores.
Configuración de la API REST de Docker
En primer lugar, necesitas un host Docker. El servidor Jenkins (master) se conectará a este host Docker para iniciar agentes de contenedores.
Esta conexión se realiza a través de la API REST de Docker, por lo que necesitamos habilitar la API remota en el host Docker. Usaré un servidor Ubuntu como mi host Docker, pero siéntete libre de usar cualquier SO que admita Docker.
Podemos habilitar el acceso remoto al demonio de docker configurando el archivo de unidad systemd docker.service
para distribuciones de Linux:
- Usar el comando
sudo systemctl edit docker.service
para abrir y anular el archivo para docker.service en un editor de texto, o abrir directamente con tu editor de texto preferido el archivo de servicio de docker/lib/systemd/system/docker.service
- En nuestro caso, definiremos 4243 como el puerto para exponer la API REST de Docker. Este será el puerto donde el maestro Jenkins se conectará para hacer que Docker ejecute los agentes de contenedores. Por lo tanto, en cualquiera de los casos anteriores, busca ExecStart y reemplaza esa línea por la siguiente:
[Service]
ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:4243 -H unix:///var/run/docker.sock
- Después de guardar el archivo, recarga la configuración del sistema y reinicia Docker emitiendo los siguientes comandos
$ sudo systemctl daemon-reload
$ sudo systemctl restart docker.service
- Verifica que la API REST de Docker esté habilitada y escuchando en el puerto 4243
$ curl http://localhost:4243/version
{"Platform":{"Name":"Docker Engine - Community"},"Components":[{"Name":"Engine","Version":"24.0.2","Details":{"ApiVersion":"1.43","Arch":"amd64","BuildTime":"2023-05-25T21:51:00.000000000+00:00","Experimental":"false","GitCommit":"659604f","GoVersion":"go1.20.4","KernelVersion":"6.5.0-17-generic","MinAPIVersion":"1.12","Os":"linux"}},{"Name":"containerd","Version":"1.6.21","Details":{"GitCommit":"3dce8eb055cbb6872793272b4f20ed16117344f8"}},{"Name":"runc","Version":"1.1.7","Details":{"GitCommit":"v1.1.7-0-g860f061"}},{"Name":"docker-init","Version":"0.19.0","Details":{"GitCommit":"de40ad0"}}],"Version":"24.0.2","ApiVersion":"1.43","MinAPIVersion":"1.12","GitCommit":"659604f","GoVersion":"go1.20.4","Os":"linux","Arch":"amd64","KernelVersion":"6.5.0-17-generic","BuildTime":"2023-05-25T21:51:00.000000000+00:00"}
$ sudo netstat -lntp | grep dockerd
tcp6 0 0 :::4243 :::* LISTEN 3551202/dockerd
Make sure the following ports are enabled in your server firewall to accept connections from Jenkins master: Asegúrate de que los siguientes puertos estén habilitados en el firewall del servidor para aceptar conexiones desde el maestro Jenkins:
- Docker Remote API port 4243
- Docker Hostport Range 32768 to 60999
32768 a 60999 es utilizado por Docker para asignar un puerto de host para que Jenkins se conecte al contenedor. Sin esta conexión, el nodo de construcción quedará en estado pendiente.
Una vez que hayas habilitado y probado la API, ahora puedes comenzar a construir la imagen del agente Jenkins Docker.
Plugin Docker para Jenkins
Antes de comenzar a crear la imagen Docker, algunas palabras sobre el plugin Docker para Jenkins (https://plugins.jenkins.io/docker-plugin/)
Según la documentación oficial del plugin Docker:
This plugin allows containers to be dynamically provisioned as Jenkins nodes using Docker. It is a Jenkins Cloud plugin for Docker.
Además, la documentación establece:
The aim of this docker plugin is to be able to use a Docker host to dynamically provision a docker container as a Jenkins agent node, let that run a single build, then tear-down that node, without the build process (or Jenkins job definition) requiring any awareness of docker.
Notas:
- Este plugin no utiliza el cliente nativo de docker del sistema operativo; utiliza docker-java. No necesitas instalar un cliente de docker en Jenkins o en tus agentes para usar este plugin.
- Este plugin no proporciona un demonio de Docker; permite que Jenkins use un demonio de Docker. Una vez que hayas instalado Docker en algún lugar, este plugin permitirá que Jenkins lo utilice.
Instalación del plugin Docker para Jenkins
- Ve al Administrador de Plugins (Panel de control de Jenkins –> Administrar Jenkins –> Administrar Plugins)
- Selecciona la pestaña Disponible, busca “Docker”, instala el plugin Docker (https://plugins.jenkins.io/docker-plugin/) y reinicia Jenkins.
Configuración del plugin Docker para Jenkins y creación de agentes de contenedores
Una vez instalado, ve al Panel de control de Jenkins » Administrar Jenkins » Administrar nodos y nubes.
El plugin Docker es una implementación “Cloud”, por lo que necesitamos seleccionar Configurar nubes.
Haz clic en Agregar una nueva nube y selecciona “Docker”
Bajo docker, necesitamos completar los detalles como se muestra en la imagen a continuación:
En primer lugar, necesitamos nombrar nuestra nube (“docker” en este ejemplo, pero puedes elegir cualquier otro nombre descriptivo). También podemos ver un par de paneles de configuración: Detalles de la nube Docker y Plantillas de agentes Docker.
Detalles de la nube Docker
Al abrir Detalles de la nube Docker, el campo más importante es URI del host de Docker. Después de completar, podemos hacer clic en Probar conexión para ver si funciona. En caso de error, revisa la configuración de la API REST remota de Docker.
Plantillas de agentes Docker
Bajo la sección de plantillas de agentes Docker, podemos definir tantos agentes de contenedores como necesitemos. Es importante tener en cuenta que todos los agentes de contenedores que definamos aquí se ejecutarán bajo el host de docker definido anteriormente. De esta manera, podemos definir diferentes contenedores que actuarán como agentes de Jenkins.
Como se muestra en la imagen a continuación, al hacer clic en Agregar plantilla de Docker, puedes crear un agente de contenedor específico.
Al hacer clic en Agregar plantilla de Docker, verás un formulario como este:
Campos importantes:
- Etiquetas: Etiqueta(s) de identificación para el agente. Dentro de un pipeline, los agentes se identifican no por nombre sino por sus etiquetas. Por lo tanto, dentro de un pipeline, la sección de agentes especifica la etiqueta donde se ejecutará todo el pipeline (o una etapa específica) en el entorno de Jenkins
- Nombre: Solo un prefijo para identificar los nodos creados a partir de esta plantilla (opcional)
- Imagen de Docker: Este es el campo más importante porque aquí indicaremos la imagen de Docker que se ejecutará.
Antes de especificar nuestra imagen, debemos tener en cuenta que el plugin Docker nos permite crear agentes de contenedores de tres formas diferentes dependiendo del método de inicio del agente. Estas son tres formas diferentes en las que el maestro Jenkins y los agentes de contenedores pueden comunicarse. Y, dependiendo del método elegido, la imagen tendrá diferentes requisitos.
Vamos a omitir campos menos relevantes y concentrarnos en uno muy importante: el Método de conexión.
Según la documentación del plugin Docker:
For all connection methods, Jenkins will start by triggering a docker run. Then, after this step, there will optionally be more steps to establish the connection. There are currently three alternative ways to connect your Jenkins master to the dynamically provisioned Docker agents.
Y los tres métodos de conexión son:
There are different pros and cons for each connection method. Depending on your environment, choose the one matching your needs. More detailed prerequisites are provided once you select a given method.
- Attach Docker container: este método ejecuta un contenedor, luego se conecta a él usando docker exec, todo mediante la API de Docker. El agente no necesita poder llegar al maestro a través de las capas de red para comunicarse; todo irá a través de la API de Docker.
- Conectar con JNLP: el contenedor solo recibirá un comando inicial de docker run con el secreto correcto. Y el agente de remoting establecerá la conexión con el maestro a través de la red. Por lo tanto, el agente debe poder acceder al maestro a través de su dirección y puerto.
- Conectar con SSH: se espera que la imagen especificada ejecute un servidor SSH. Luego, tratará esa computadora como lo hace normalmente para cualquier agente conectado por SSH: el maestro se conectará a ella, copiará el agente de remoting y luego lo iniciará.
Pros y contras de cada método de conexión
- Método #1 (Adjuntar contenedor Docker): es el más fácil de implementar. Como leerás más adelante en este documento, todo lo que se necesita dentro de la imagen del agente es un JDK y el agente.jar de Jenkins. Además, la conexión es unidireccional (maestro -> agente). La única desventaja es que necesitas actualizar manualmente el agente.jar si cambia la versión del maestro. Pero, hay una imagen de agente lista para usar que contiene tanto el JDK como el agente.jar, por lo que puedes usarla.
- Método #2 (Conectar con JNLP): se basa en JNLP, una tecnología que permite descargar el código del agente desde el maestro. Se basa en TCP o WebSockets para establecer una conexión de entrada al controlador de Jenkins, por lo que el agente debe poder acceder al maestro a través de su dirección y puerto, un requisito difícil de cumplir dependiendo de la segmentación de la red. Finalmente, la tecnología JNLP es “bastante” antigua, con consideraciones de desaprobación y eliminación. Todo esto hace que esta opción sea la menos recomendable.
- Método #3 (Conectar con SSH): es ligeramente más difícil de implementar pero la opción más segura y mantenible. Con este método de conexión, el agente.jar se copia de forma segura al contenedor (por lo que siempre se actualiza) y la conexión es unidireccional (maestro -> agente) por lo que no es necesario abrir la conectividad desde el host de docker de vuelta al maestro.
En resumen, podríamos decir que la opción #3 (Conectar con SSH) es la preferida, seguida por la #1 (Adjuntar contenedor Docker) y desaconsejaríamos la opción #2 (Conectar con JNLP) por las razones anteriores (aunque podría ser válida en algunos escenarios específicos).
El proceso de creación de la imagen depende del método de conexión elegido. Discutiremos las tres opciones disponibles en detalle.
Método de conexión #1: Adjuntar contenedor Docker
Esta es la forma más simple de conexión. Como se dijo anteriormente, con este método, el agente no necesita poder llegar al maestro a través de las capas de red para comunicarse; todo irá a través de la API de Docker. Esto reduce los requisitos de configuración de red.
Si seleccionas este método de conexión, la imagen de Docker a utilizar es bastante simple. Solo necesita tener un JDK instalado y el ejecutable del agente Jenkins (agent.jar). Puedes crear tu imagen desde cero o puedes usar la imagen lista para usar: jenkins/agent
En caso de que quieras construir tu propia imagen, puedes usar jenkins/agent como base para una imagen personalizada.
FROM jenkins/agent
RUN apt-get update && apt-get install XXX
... your dockerfile instructions here …
Método de conexión #2: Conectar con JNLP
Como ya comenté anteriormente, este método es el menos recomendado. Pero, si decides usarlo, la imagen de Docker debe tener un JDK instalado y el agente.jar de Jenkins. La conexión es unidireccional (maestro -> agente), por lo que el maestro debe poder acceder al contenedor a través de la red.
La imagen necesita tener un JDK instalado. Puedes usar jenkins/inbound-agent como base para una imagen personalizada.
FROM jenkins/inbound-agent
RUN apt-get update && apt-get install XXX
... your dockerfile instructions here …
Método de conexión #3: Conectar con SSH
Este método de conexión es el más seguro, confiable y mantenible. No es tan simple de implementar como los anteriores, pero nada realmente difícil ;)
En los siguientes ejemplos, usaremos un par de claves para autenticar. Por lo tanto, vale la pena hacer un breve resumen sobre cómo crear un par de claves.
El proceso de generación de claves SSH crea dos claves: una privada y una pública.
- Clave pública: Esta clave debe instalarse en el servidor al que queremos conectarnos. De esta forma, el servidor podrá reconocer y autenticar al cliente.
- Clave privada: Esta es la clave más importante porque te identifica de forma única como cliente. Por lo tanto, debe mantenerse segura. Si se compromete (y no se ha asegurado aún más con una frase de contraseña), cualquiera podría usarla y autenticarse como tú.
Puedes generar un par de claves SSH ingresando el siguiente comando:
$ ssh-keygen -t rsa -b 4096
Básicamente, -t especifica el tipo de clave a crear (los valores posibles son “dsa”, “ecdsa”, “ecd sa-sk”, “ed25519”, “ed25519-sk” o “rsa”) y -b especifica el número de bits en la clave a crear (para las claves RSA, el tamaño mínimo es de 1024 bits y el valor predeterminado es de 3072 bits).
Nota IMPORTANTE:
Incluiremos la clave pública en nuestro contenedor. Y usaremos la clave privada para autenticarnos desde Jenkins.
Hasta donde hemos probado:
Si usas claves RSA, ¡¡el plugin Docker solo funciona con claves de 4096 bits!! En el momento de escribir este documento, no estoy seguro si es un error o alguna limitación conocida del plugin, pero vale la pena mencionarlo porque si usas el tamaño predeterminado (3072), la autenticación no funcionará.
Una vez que generes el par de claves, necesitamos hacer lo siguiente:
- Guardar esa clave privada en el Administrador de Credenciales de Jenkins, para que pueda ser utilizada por los pipelines
- Instalar la clave pública en el contenedor (para que el contenedor pueda reconocer al cliente)
Para guardar la clave privada en el Administrador de Credenciales de Jenkins, crea una nueva credencial (tipo: SSH Username with private key)
Para hacer que la clave pública esté disponible para nuestro contenedor, la guardaremos en el archivo $HOME/.ssh/authorized_keys de la imagen (siendo $HOME el directorio de inicio del usuario que usaremos para conectarnos al contenedor).
$ cat my_public_key >> authorized_keys
Nuestro propósito es ejecutar un contenedor Docker que sirva como agente Jenkins. Para este propósito, primero debemos construir una imagen de Docker que acepte conexiones ssh desde Jenkins y funcione como agente.
En los métodos anteriores, las imágenes de alguna manera incluían algún código de tarro de Jenkins. Este caso no es diferente, pero con una diferencia: es completamente transparente para nosotros, el código del agente de Jenkins se copia detrás de las escenas y no necesitamos preocuparnos por ello.
Entonces, básicamente, solo tenemos que incluir el software adecuado en nuestras imágenes para hacerlo un servidor SSH.
La estructura básica del Dockerfile es:
- Instalar un servidor SSH
- Instalar JDK
- Agregar un usuario (jenkins en nuestro caso)
- Agregar la clave pública al directorio de inicio del usuario jenkins
- Exponer el puerto SSH y arrancar el servidor SSH
Este Dockerfile depende de la imagen base que usemos, por lo que veremos algunos ejemplos a continuación.
Ubuntu
Puedes usar el Dockerfile a continuación como plantilla para tus necesidades.
FROM ubuntu:18.04
# Update the repository
RUN apt-get update && \
apt-get -qy full-upgrade && \
# Install git
apt-get install -qy git && \
# Install a basic SSH server
apt-get install -qy openssh-server && \
mkdir -p /var/run/sshd && \
# Install JDK 11
apt-get install -qy openjdk-11-jdk && \
# Cleanup old packages
apt-get -qy autoremove && \
# Add user jenkins to the image
adduser --quiet jenkins && \
# Set password for the jenkins user
echo "jenkins:jenkins" | chpasswd
# Copy authorized keys
COPY --chown=jenkins ./authorized_keys /home/jenkins/.ssh/authorized_keys
RUN chown -R jenkins:jenkins /home/jenkins/.ssh/
RUN chmod 700 /home/jenkins/.ssh/
RUN chmod 600 /home/jenkins/.ssh/authorized_keys
# Standard SSH port
EXPOSE 22
CMD ["/usr/sbin/sshd", "-D"]
Cuando configures la plantilla del agente Docker, asegúrate de usar el método “Conectar con SSH” y “Usar credenciales SSH configuradas” como clave SSH. Bajo Credenciales SSH, elige la credencial ya creada con la clave privada.
Alpine
En el caso de Alpine, el Dockerfile es algo diferente debido a las diferencias en el sistema operativo subyacente.
FROM alpine:latest
RUN apk add --no-cache openssh openssh-server openssh-keygen
RUN addgroup -S jenkins
RUN adduser -D -G jenkins jenkins
RUN echo "jenkins:jenkins" | chpasswd
# Copy authorized keys
COPY --chown=jenkins ./authorized_keys /home/jenkins/.ssh/authorized_keys
RUN chown -R jenkins:jenkins /home/jenkins/.ssh/
RUN chmod 700 /home/jenkins/.ssh/
RUN chmod 600 /home/jenkins/.ssh/authorized_keys
EXPOSE 22
RUN ssh-keygen -A
CMD ["/usr/sbin/sshd", "-D"]
From jenkins/ssh-agent
Jenkins también proporciona una imagen base para construir tu contenedor. Por lo tanto, es de alguna manera más fácil usarla que construir desde cero.
FROM jenkins/ssh-agent:latest
RUN apt-get update
RUN apt-get install -qy zip && \
apt-get install -qy curl
COPY --chown=jenkins ./authorized_keys "${JENKINS_AGENT_HOME}"/.ssh/authorized_keys
Resumen y conclusiones
En este documento, hemos visto cómo configurar Jenkins para usar contenedores Docker como agentes. Hemos visto los tres métodos de conexión diferentes y los requisitos para cada uno.
También hemos visto cómo construir una imagen de Docker que funcionará como agente Jenkins, y cómo configurar Jenkins para usarla.
La opción más segura y mantenible es usar el método de conexión SSH. Este método requiere un poco más de trabajo para configurar, pero vale la pena.
El plugin Docker para Jenkins es una herramienta poderosa que te permite aprovisionar dinámicamente contenedores Docker como agentes de Jenkins. Es una excelente manera de escalar tu infraestructura de Jenkins y hacer que tus compilaciones sean más eficientes.