sábado, 27 de octubre de 2018

Docker swarm - Enrutamiento de servicios

Hola de nuevo, en el post anterior construimos un pequeño swarm de 4 nodos y desplegamos un servicio web muy simple. En dicho ejemplo veíamos como el manager creaba copias idénticas de nuestro servicio y las repartía entre los nodos que forman el swarm. Comprobamos que el puerto de nuestro servicio se publicaba en cada uno de los nodos y que, independientemente del nodo al que accediésemos, podíamos conectarnos con nuestro servicio.

En este post vamos a explicar, de manera muy resumida, como funciona la routing mesh en la que participan todos los nodos de un swarm y que nos permite acceder a un servicio independientemente del nodo en el que se encuentre corriendo. Para una explicación mucho mejor os recomiendo, como siempre, consultar la documentación de Docker y en concreto, el siguiente enlace.

Para comenzar vamos a desplegar de nuevo el servicio web simple que usamos especificando que queremos que el número de réplicas sea 1, con lo que el nodo manager desplegará el servicio en uno de los cuatro nodos del swarm:

Desplegamos el servicio web en el swarm.
Comprobamos en que nodo está corriendo el servicio.
Como vemos el contenedor está corriendo en el nodo 3 del swarm y si comprobamos los puertos en los que están escuchando los 4 nodos veremos que, en todos ellos, el puerto 443 está disponible en la red de servicio. Por tanto, si nos conectamos a cualquier nodo del swarm salvo el 3, comprobamos que efectivamente, nuestro servicio web está disponible:

Acceso al servicio web desde un nodo sin réplica.
En definitiva la pregunta que nos hacemos es, ¿como puedo acceder al servicio desde cualquier nodo del swarm si este solo está corriendo en uno de los nodos? La respuesta a esta pregunta es el sistema de enrutamiento del swarm.

Cuando creamos un swarm, todos los nodos del mismo participan en lo que Docker denomina una malla de enrutamieno de entrada (ingress routing mesh) mediante la cual, todos los miembros del swarm son capaces de enrutar las peticiones de conexión entrantes a los nodos donde realmente se encuentran los contenedores.

Esto quiere decir que, cuando desplegamos un servicio y publicamos un puerto, este puerto pasa a estar enlazado en los interfaces que hayamos definido como interfaces de datos en todos los hosts del swarm. El motor de Docker crea la malla de enrutamiento, que escucha en los puertos publicados en todos los hosts, y enruta cualquier petición entrante al swarm a dicho puerto hacia un contenedor activo, aunque este se encuentre en un host diferente.

De forma muy simplificada, podemos verlo del siguiente modo:

Malla de enrutamiento de entrada en un Docker swarm.
Por tanto, cada vez que llegue una petición a un host de nuestro swarm en un puerto publicado para un servicio, el nodo que reciba dicha petición lo enrutará a un contenedor activo aunque este se encuentre en otro host.

Como es lógico, necesitaremos balanceadores de carga para tener un único punto de acceso a nuestro swarm, lo cual veremos más adelante.

domingo, 21 de octubre de 2018

Como crear un cluster de Docker.

Hasta ahora, en todas las pruebas realizadas, hemos usado un solo dockerhost con el que hemos podido explorar algunas de las características que nos proprociona Docker.

Como sabemos, en un entorno productivo real, necesitaremos un cluster que nos asegure la alta disponibilidad de nuestros servicios. Por tanto hoy vamos a crear nuestro primer cluster de nodos dockerhost, con el cual podremos realizar pruebas más parecidas a un entorno real.

En un entorno Docker un grupo de dockerhosts trabajando conjuntamente, se denomina swarm. Algunas de sus características, de forma muy resumida, son:
  • En un swarm tenemos dos tipos de nodos, los nodos manager y los nodos worker. Los nodos manager se encargan del reparto de tareas entre todos los nodos worker, así como del control del estado del swarm, lo cual no quiere decir que en los nodos manager no corra ningún contenedor.
  • Veremos que podemos desplegar servicios o stacks de servicios, los cuales podremos definir mediante ficheros YAML.
  • Cada nodo enrutará el tráfico entre ellos para todas aquellas conexiones de entrada que se correspondan con servicios ejecutándose en otros nodos.
Hasta aquí la aburrida teoría la cual, como siempre, recomiendo ampliar visitando la documentación oficial de Docker y que intentaré ampliar en las próximas entradas.

Para construir un swarm básico necesitamos varios nodos con docker instalado, crear el swarm en uno de los nodos, lo que lo convertirá en un nodo manager y añadir el resto de nodos al swarm, que se añadirán como nodos worker.

En mi caso he utilizado cuatro máquinas virtuales CentOS 7 con dos tarjetas de red, una para formar la red de cluster entre los dockerhost y una red de servicio, para el acceso a los servicios proporcionados por los contenedores.

Para la configuración de los nodos, instalación de Docker y creación del swarm, he desarrollado un par de playbooks de Ansible, los cuales están disponibles en github para su consulta, modificación, uso y disfrute.

Así que tras instalar los nodos y ejecutar los playbooks correspondientes, tendremos un swarm de cuatro nodos. Como hemos dicho, hay un nodo manager en el swarm, que es desde el cual debemos lanzar todos nuestros comandos. Así, para comprobar el estado de nuestro swarm ejecutaremos el siguiente comando desde nuestro nodo manager:

Información sobre los nodos del swarm.
El comando docker node ls nos muestra el estado de todos los nodos que forman nuestro swarm y además, nos indica cuales son nodos manager, la versión del motor de docker de cada uno y su disponibilidad.

Como primera prueba sencilla, además de la cración del swarm, hoy vamos a desplegar un servicio simple web basado en una imagen con un servidor HTTP, para esto solo tenemos que lanzar el comando siguiente:
Creación de un servicio web simple.
De forma muy resumida, con el comando anterior hemos desplegado un servicio http basado en Apache que tiene tres réplicas distribuidas entre todos los nodos de nuestro swarm, usando como imagen la imagen disponible en el repositorio correspondiente de Docker Hub y publicando el puerto 443 de cada contenedor en cada uno de los hosts. Para comprobarlo solo tenemos que listar los servicios de nuestro swarm con el siguiente comando:

Lista de los servicios ejecutándose en el swarm.
Si inspeccionamos los contenedores que se están ejecutando en cada uno de los nodos del swarm, ejecutando el comando docker ps -a en cada uno de ellos, veremos lo siguiente:

Contenedores ejecutándose en los nodos del swarm.
Como vemos en la salida anterior, cada dos de los nodos worker del swarm está ejecutando un contenedor correspondiente a nuestro servicio, estando el tercero alojado en el nodo manager. Si ahora accedemos al servicio en nuestros nodos, podremos ver que la web se muestra correctamente y en cada caso muestra lo siguiente:
Web mostrada accediendo al nodo swarm-node1.
Web mostrada accediendo al nodo swarm-node2.
Para terminar, si quieremos eliminar el servicio, solo tendremos que ejecutar el siguiente comando en el manager de nuestro swarm:
Eliminando el servicio webserver del swarm.
Por tanto, como podemos ver, una vez que hemos creado un swarm tenemos que tener en cuenta los siguientes puntos:
  • Debemos interactuar con uno de los nodos manager del swarm para poder desplegar y controlar nuestros servicios.
  • Los contenedores se ejecutarán en cualquier nodo que forme parte del swarm, salvo que establezcamos algún tipo de limitación por nodo.
  • Al publicar un puerto de un servicio, dicho puerto estará disponible en la IP correspondiente del nodo físico que hayamos especificado como data-path en el momento de la creación del swarm.
  • Al crear un servicio simple, como el de este ejemplo, podremos especificar el número de réplicas que formarán dicho servicio y estas se distribuirán por todos los nodos que formen el swarm.
Con este ejemplo básico hemos visto como podemos comenzar a trabajar con swarms de Docker y, en las proximas entradas, exploraremos más detenidamente los tipos de despliegues y sus características, la comunicación entre los nodos que forman el swarm y como acceder a volúmenes para conseguir la persistencia de nuestros datos.

sábado, 20 de octubre de 2018

Recuperación de directorios y ndmpcopy.

Hoy volvemos con una nueva entrada relacionada con la protección de datos en nuestros sistemas ONTAP, con lo que vamos a recordar dos herramientas fundamentales, snap restore y ndmpcopy las cuales nos permitirán recuperar información cuando sea necesario.

Mediante snap restore podemos recuperar información de los snapshots existentes en un volumen, mientras que ndmpcopy nos permite copiar datos entre SVMs e incluso con la configuración adecuada, entre diferentes clusters.

Por ejemplo, para los casos en los que queremos recuperar un fichero, podemos usar snap restore-file del siguiente modo:

Recuperación de un fichero desde un snapshot.

En caso de que necesitemos restaurar todo un volumen, el comando que usaremos será snap restore, el cual recuperará todos los datos del volumen al estado en el que se encontraba en el momento de creación del snapshot correspondiente. Por ejemplo:

Recuperación de un volumen completo a partir de un snapshot.
Es muy importante tener en cuenta los siguientes puntos en el caso de recuperación de un volumen completo:
  1. Al restaurar un volumen completo de un snapshot determinado, si el snapshot elegido no es el último, cualquier snapshot posterior se borrará.
  2. Las cuotas establecidas en qtrees pueden ser diferentes entre el volumen y el snapshot, con lo que será necesario revisarlas tras la recuperación y reinicializar las cuotas en dicho volumen en caso de ser necesario. 
  3. Es necesario revisar las políticas de exportación NFS por si son diferentes entre el volumen y el momento del snapshot usado para la recuperación.

En determinadas ocasiones necesitaremos recuperar  un directorio completo y en ese caso, nos encontramos con el siguiente problema al usar snap restore-file:

Error al intentar recuperar un directorio mendiante snap restore-file.
Por tanto, además de copiar y pegar directamente el directorio desde el snapshot correspondiente accediendo al mismo desde el directorio .snapshot, ¿que otra opción tenemos? Podemos usar el comando ndmpcopy como vamos a ver a continuación.


Para poder usar este comando correctamente, primero es necesario añadir el protocolo ndmp a la SVM, para lo cual solo necesitamos modificar los protocolos de la misma añadiendo el protocolo ndmp:

Modificamos los protocolos del vserver.
A continuación iniciamos el servicio ndmp en la SVM y generamos una password para el usuario que utilizaremos para el proceso de copia:

Iniciamos el servicio ndmp y generamos la password.
Ya solo nos queda ejecutar el comando ndmpcopy para restaurar el directorio, con todo su contenido, desde el snapshot deseado. Para este comando es muy importante tener en cuenta que debe ejecutarse en un nodo, por tanto el comando será:

Comando ndmpcopy para restaurar un directorio completo (no se muestra toda la salida).
Es muy importante definir correctamente las rutas fuente y destino que, como vemos, deben incluir los nombres de las SVMs involucradas en la operación de recuperación, así como el usuario y contraseña de los usuarios utilizados en origen y destino. Como referencia, el comando ndmpcopy usado en este ejemplo tiene la siguiente sintaxis:

Sintaxis simple comando ndmpcopy.
En próximas entradas veremos como proteger nuestra información a largo plazo utilizando snapvault para la realización de backups a disco.

viernes, 12 de octubre de 2018

Actualizando imágenes

Hola de nuevo, hoy vamos a ver un punto importante en el ciclo de vida de nuestras imágenes. Supongamos que tenemos una imagen de un servicio que dispone de actualizaciones automáticas o bien que queremos actualizar manualmente una imagen, pero no queremos bajar la última disponible de Docker Hub o reconstruir manualmente la imagen completa.

Para este ejemplo vamos a utilizar una imagen oficial de Jenkins descargada de Docker Hub. Para este caso, estableciendo el volumen donde vamos a mantener la configuración de Jenkins, al arrancar y configurar nuestro servicio Jenkins, nos encontramos con el siguiente mensaje al acceder al mismo:

Actualizaciones disponibles para Jenkins.
Como es lógico si nuestro contenedor es efímero, podemos descargar la actualización y trabajar con la versión actualizada pero, en cuanto paremos el contenedor habremos perdido la actualización y tendremos que realizarla de nuevo la próxima vez que arranquemos.

Ahora podríamos o bien bajar una imagen más moderna y usarla en todos nuestros contenedores o bien, podemos hacer uso del comando commit de docker, con el cual podemos crear una nueva imagen a partir del contenido de un contenedor. Veamos como hacerlo con nuestro Jenkins del ejemplo.

Realizamos la actualización de nuestro servicio Jenkins comenzando por los plugins que estemos utilizando:

Actualización de los plugins del servicio Jenkins.
En el caso de Jenkins hay que tener en cuenta que los plugins se almacenan en la carpeta de configuración, que deberíamos montar en un volumen para asegurar la persistencia de la configuración de nuestros jobs, usuarios, etc... Como ya vimos en otra entrada, podemos comprobar donde debemos montar nuestro volumen con solo usar el comando inspect sobre la imagen de jenkins:

Definición de volumen para servicio Jenkins.
Una vez actualizados los plugins vamos a pasar a actualizar el servicio Jenkins en si, para lo cual lo más sencillo es descargar el fichero de actualización y pasarlo a nuestro contenedor para sutituir el actual. De forma resumida sería algo como lo siguiente:

Paramos el servicio Jenkins dentro del contenedor.
Ahora copiamos el fichero de actualización de la nueva versión usando el comando docker cp:

Copiamos el fichero de actualización dentro del contenedor.
Ahora solo tenemos que copiar el fichero a la ruta correcta dentro del contenedor y pararlo.

Conectamos al contenedor y copiamos el fichero a la ruta correcta.
En este punto tenemos un contenedor parado a partir del cual podemos construir nuestra nueva imagen usando el comando commit de docker:

Creamos la nueva imagen a partir del contenedor parado.
Ahora solo tenemos que crear un contenedor, pero usando esta nueva imagen ya actualizada y conectarnos para comprobar que, efectivamente nuestro servicio está actualizado:

Creamos un contenedor usando nuestra nueva imagen de jenkins.

Nuestro servicio Jenkins ya actualizado.
En resumen, de una forma muy sencilla podremos crear imágenes a partir de contenedores en los que introduzcamos cambios de configuración o software. Esto nos permite mantener nuestros servicios actualizados y en el estado que nosotros queramos, ya que podemos controlar totalmente las imágenes que tengamos en nuestros repositorios.

El ejemplo que hemos visto es para un servicio Jenkins pero, como es evidente, es aplicable a practicamente cualquier otro y, como último punto a tener en cuenta, podemos usar la opción --pause con el comando commit si no queremos o no podemos parar el contenedor.

sábado, 15 de septiembre de 2018

Ejecución de múltiples servicios en contenedores

Hola de nuevo, hoy vamos a estudiar un punto importante a la hora de crear nuestras imágenes relacionado con el control de los procesos que corren en nuestros contenedores.

En general la recomendación a la hora de diseñar servicios complejos basados en Docker, es que cada contenedor sea responsable de un único servicio, estableciendo la comunicación entre ellos mediante las definiciones de red y volúmenes que ya hemos visto.

El proceso principal de un contenedor será el ENTRYPOINT o CMD que habremos definido en nuestro dockerfile el cual en ocasiones, puede lanzar múltiples procesos que correrán dentro de nuestro contenedor.

En esos casos, o en casos en los que necesariamente tengamos que ejecutar más de un servicio dentro del mismo contenedor, es muy recomendable utilizar un gestor de procesos que controle el estado de los mismos, asegurándonos así que se gestionan las señales correctamente, parando los proceos de forma correcta cuando sea necesario y que se controlan procesos que se puedan quedar en estado zombie o que no respondan adecuadamente.

Una forma de lograr esto sería incluir un gestor de procesos, como supervisord, en nuestra imagen base para luego utilizarlo en todas y cada una de las imágenes siguientes. El problema de esta aproximación es que hará nuestras imágenes más pesadas debido a que tendremos que incluir supervisord y todas sus dependencias.

Una opción mucho más ligera es usar el flag --init en el momento de construir nuestro contenedor, de este modo Docker inserta un pequeño gestor de procesos como proceso principal, el cual podrá realizar todas las acciones descritas de control de procesos dentro de nuestro contenedor sin necesidad de cambiar nuestras imágenes. En concreto, al usar el flag --init, docker inserta en nuestra imagen el gestor de procesos tini, del cual podéis obtener más información en https://github.com/krallin/tini.

Al construir un contenedor con la opción --init usando una de nuestras imágenes, el resultado es el siguiente:

Contenedor creado con opción --init a partir de imagen ldap.
Como podemos ver en la salida anterior, ahora el proceso con PID 1 del contenedor ya no es el correspondiente a nuestro servicio LDAP, sino el init insertado por Docker en el momento de la creación del contenedor y el proceso slapd es un proceso hijo de este nuevo proceso init.

Como está claro no hemos tenido que modificar la imagen de ninguna manera con lo que, de una forma increiblemente sencilla, podemos construir contenedores en los que se ejecuten más de un servicio o estar seguros de que, en caso de problemas con los procesos de nuestros contenedores, podremos usar un gestor de procesos que evitará problemas como procesos huerfanos o procesos zombies dentro de los mismos.

jueves, 13 de septiembre de 2018

Configurando el acceso a la API de Docker

El demonio dockerd que se ejecuta en nuestros hosts implementa una API RESTful a la que podemos acceder mediante un cliente HTTP. De esta manera podemos integrar los servicios proporcionados por Docker con otros servicios de nuestra infraestructura.

La configuración por defecto de Docker utiliza un socket Unix, con lo que no hay acceso desde la red a la API del motor de Docker. Como para determinados casos es necesario poder comunicarnos con el servicio Docker desde la red, vamos a ver como configurarlo para que sea accesible mediante conexiones TCP de forma segura. 

En principio, todo lo que tenemos que hacer es configurar el servicio Docker para que escuche en el puerto y direcciones IP del host que queramos y especificar las opciones de configuración tlsverify y tlscacert. De este modo, cuando el demonio dockerd actúa en modo servidor, solo aceptará conexiones de clientes autenticados con un certificado firmado por la CA en la que se confía y, en modo cliente, solo se conectará a servidores con un certificado firmado por la misma CA.

Por tanto, como es necesario disponer de una CA, crearemos una entidad certificadora de pruebas generando un certificado autofirmado usando openssl. Una vez creada nuestra CA, crearemos y firmaremos el resto de certificados que necesitemos. Como es lógico en sistemas en producción, deberíamos tener implementada una CA para todas estas tareas.

Con nuestros certificados ya listos, solo tenemos que configurar el servicio Docker de nuestra máquina usando el comando systemctl del siguiente modo:

Modificación del servicio docker. Añadimos socket de escucha y opciones TLS.
De este modo el servicio docker escuchará en el puerto TCP y dirección IP especificadas verificando las conexiones TLS de cualquier cliente y, adicionalmente creará el socket local que nos permitirá seguir usando los comandos localmente en el host. 

Si intentamos conectar ahora a nuestro host docker por el puerto especificado usando https, recibiremos un mensaje de error que nos indica que ha intentado validar nuestro certificado. Esto viene dado por la opción tlsverify y al no tener un certificado firmado por la CA configurada mediante la opción tlscacert, genera la denegación de nuestro acceso. Creando un certificado para nuestra máquina cliente con nuestra CA de pruebas podremos acceder sin problemas:

Acceso al servicio Docker remoto mediante HTTPS y verificación de certificado cliente.
Con esta configuración ya establecida, veremos más adelante como usarla para crear arquitecturas y servicios más complejos.

sábado, 8 de septiembre de 2018

Usando contenedores como build agents de Jenkins.

Siguiendo con Jenkins y su integración con Docker, vamos a estudiar como podemos usar contenedores para realizar builds de nuestros desarrollos, de tal modo que lancemos contenedores efímeros que solo se encargarán de realizar las builds y las pruebas que configuremos para cada caso.

Para poder integrar un servicio Jenkins con un host Docker es necesario configurar el demonio dockerd de nuestro dockerhost para que pueda recibir peticiones por red. Para esto, suponiendo que el servicio Jenkins es un contenedor dentro del propio host Docker, tenemos que editar la configuración del demonio dockerd y añadir una definición de URL para el arranque del demonio. Esto podemos hacerlo de manera simple con el comando systemctl edit docker.service y solo tenemos que añadir las siguientes líneas de configuración:

Modificación de configuración del servicio Docker para añadir un socket de escucha de red y verificación TLS.
Una vez realizado este cambio es necesario reiniciar el servicio Docker para que los nuevos cambios tengan efecto, para esto solo es necesario ejecutar el comando systemctl con la opción restart y si todo está correcto, el demonio Docker estará escuchando en el puerto definido en nuestra nueva configuración y requerirá validación TLS. Adicionalmente es muy posible que haya que añadir una regla en los cortafuegos implicados para permitir las conexiones a dicho puerto.

Tras realizar la configuración anterior, pasamos a configurar nuestro servicio Jenkins, con lo que desde el apartado Nube de la configuración del sistema, añadimos un proveedor específico de nube en Jenkins que será de tipo Docker. Para esto, al añadir Docker como proveedor de nube, tendremos que rellenar los campos siguientes:

Configuración de nube Docker en Jenkins.
Es muy importante crear unas credenciales basadas en un certificado para la validación del servicio Jenkins con el demonio dockerd del host remoto. Para esto tendremos que generar un certificado firmado por la misma CA que hemos utilizado para el certificado usado por el demonio dockerd y que hemos especificado en la configuración del servicio Docker del host remoto. 

Podemos comprobar que la configuración y el acceso al host Docker es correcto con solo utilizar el botón Test Connection, si todo es correcto la versión del host Docker aparecerá como puede verse en la imagen anterior. 

Ahora que hemos realizado la configuración correctamente, podemos ver que nuestro host Docker aparece en el apartado Docker dentro de la configuración de Jenkins:

Nuestro host Docker registrado en Jenkins.
El siguiente paso es configurar las imágenes que se usarán para realizar las builds, así como los parámetros que se usarán al lanzarlas. Para esto es importante tener en cuenta que la imagen a utilizar debe tener instalado java, además de las herramientas para realizar la build y pruebas necesarias. Para esto lo recomendable sería crear imágenes a medida, a partir de una imagen base que podemos crear como nodo slave básico de Jenkins. Luego podremos modificar dicha imagen base con las herramientas necesarias para cada uno de los casos que necesitemos.

Para esto lo mejor será, a partir de nuestra imagen base Linux, crear una nueva imagen en la que incluyamos java y git para, posteriormente, crear imágenes en las que incluyamos las herramientas que necesitemos en función de las herramientas de desarrollo que vayamos a utilizar.

Por tanto, para comenzar, creamos una nueva imagen cuyo dockerfile será el siguiente:

Dockerfile de nuestra imagen base para un nodo slave de Jenkins.
En este dockerfile hay unos cuantos puntos importantes a tener en cuenta:
  • Las instrucciones COPY y ADD nos permiten añadir archivos a nuestra imagen como ya sabemos, pero lo importante es que siempre los añadirán como root, aunque el fichero o directorio origen sean de otro usuario.
  • Debido al punto anterior, la segunda instrucción ejecuta el comando /usr/bin/chown dentro del contenedor temporal generado para crear la imagen. De este modo podemos cambiar la propiedad del directorio /JENKINS de la imagen. Esta instrucción RUN se ha ejecutado como root.
  • Especificamos que el directorio /JENKINS de la imagen es un punto de montaje de volúmenes. De este modo podremos mantener cualquier resultado generado en una build en caso de ser necesario.
  • Hasta este punto, todo se ha ejecutado como el usuario root en cualquiera de los contenedores temporales que se han ido generando para crear nuestra imagen. La siguiente instrucción define que el usuario a utilizar tiene el UID 10000, que es el usuario jenkins dentro de nuestra imagen, hará que cualquier otra instrucción RUN, CMD o ENTRYPOINT siguiente se ejecuten con dicho usuario. Por este motivo, la instrucción RUN que ha cambiado el propietario del directorio /JENKINS debe ir antes que la instrucción USER.
  • A continuación definimos variables de entorno que serán necesarias en los contenedores generados para las diferentes builds.
Con este dockerfile contruimos la imagen y comprobamos que, efectivamente, los nuevos programas añadidos están disponibles en la nueva imagen y que funcionan correctamente.

Con nuestra imagen ya creada y tras comprobar que el software que hemos incluido en la misma funciona adecuadamente, modificamos el dockerfile de la imagen del siguiente modo:

Dockerfile de nuestra imagen base para jenkins.
En este dockerfile solo hay una diferencia respecto al anterior, la definición de ENTRYPOINT es distinta y apunta a un script. Este script es muy simple, solo registra una entrada en el log del contenedor y luego lanza un comando sleep. La razón para esto es que, cuando Jenkins lanza un job en el que usa contenedores remotos como build agents, necesita un tiempo para poder engancharse con el contenedor y por tanto, si este se parase muy rápido, el build fallaría.

Ahora que tenemos nuestra imagen básica creada, la cual solo contiene de momento git y java, vamos a añadir la plantilla en la configuración de nuestro servidor Jenkins y crear nuestro job.

La configuración de la plantilla la realizaremos como podemos ver en la siguiente imagen:

Configuración de nuestra plantilla base en Jenkins.
En la configuración anterior, uno de los puntos más importantes, es la etiqueta que le asignemos a esta plantilla ya que será la que luego usaremos en la definición de nuestro job. También debemos asegurarnos de especificar el nombre correcto de nuestra imagen Docker a utilizar, en este caso jenkins_slave_base.

Adicionalmente, si no especificamos en el campo Pull strategy la opción Never pull, Jenkins siempre intentará obtener la imagen para construir el contenedor de un repositorio remoto, en vez del repositorio local de nuestro dockerhost.

Con esta configuración de plantilla, nuestro job podemos configurarlo del siguiente modo:

Creación del job en Jenkins - Especificamos el uso de un contenedor docker específico.
En el punto anterior vemos que, al especificar la misma etiqueta que hemos definido en el momento de crear la plantilla de docker, Jenkins indica que esta proporcionado por un proveedor de nube.

Realizamos cualquier configuración adicional que necesitemos para nuestro job, como obtener nuestra fuente de un repositorio git y lo lanzamos para comprobar que funciona correctamente:

Resultado del build usando un contenedor.
Además del resultado anterior indicado por Jenkins, podemos comprobar en la salida por consola del job como se han realizado todos los pasos adecuadamente.

Adicionalmente, mientras se está ejecutando el job, si comprobamos en nuestro dockerhost la lista de contenedores corriendo vemos que aparece un contenedor adicional que, al finalizar el job desaparece ya que se crea como un contenedor efímero:

Contenedor lanzado como build agent por Jenkins.
Ahora, si añadimos un compilador como por ejemplo gcc y creamos una nueva imagen a partir de esta, podremos configurar una nueva template en Jenkins que usaremos para compilar nuestro código C creando los jobs correspondientes.

Para crear una nueva template, tendremos que repetir la configuración de template anterior pero cambiando el nombre de la etiqueta, así como el nombre de la imagen a utilizar para construir los contenedores. Por ejemplo, para el caso de un slave agent para realizar builds de código C, podríamos hacer algo como lo siguiente:

Definimos una nueva template para usar la imagen que contiene el gcc.
Ahora definimos un nuevo job que haga uso de esta template y que realice la compilación del código fuente que obtiene de nuestro repositorio git, con lo que el job quedaría más o menos con esta configuración:

Configuración del job - Definición de la template Docker a utilizar.
Configuración del job - Definición de tareas simples de ejecución.
Con esta configuración de job, la salida por consola del job que obtenemos es la siguiente:

Resultado de la ejecución de nuestro job.
Por tanto, como podemos ver, es posible integrar Jenkin con nuestra infraestructura Docker para poder usar de una manera mucho más óptima nuestros recuursos. De este modo Jenkins es capaz de lanzar contenedores efímeros como agentes para nuestros entornos CI/CD permitiedo mucha más flexibilidad y aprovechamiento de los recursos.