En Mi Local Funciona

Technical thoughts, stories and ideas

Pipeline Jenkins sobre contenedores Docker creados al vuelo

Publicado por Jorge Bau Gracia el

Entrega ContinuaDockerJenkins

El propósito del artículo es proponer una solución sencilla para ejecutar en Jenkins pipelines de construcción sobre contenedores instanciados al vuelo mediante Docker.

Se utiliza Docker y los plugins de Jenkins Build Flow, Swarm, NodeLabel y Publish Over SSH.

Imagen Docker

Configuramos una toolchain que nos permita construir en el esclavo. Montaremos cliente Git, Java, Groovy y Maven, y un cliente Swarm.

FROM debian:jessie

RUN apt-get update  
RUN apt-get -y install wget  
RUN apt-get -y install openjdk-7-jdk  
RUN apt-get -y install groovy2  
RUN apt-get -y install git  
RUN apt-get -y install maven2

RUN mkdir /jenkins

RUN mkdir -p /opt/sw/swarm  
RUN cd /opt/sw/swarm  
RUN wget -O /opt/sw/swarm/swarm-client-2.0-jar-with-dependencies.jar http://repo.jenkins-ci.org/releases/org/jenkins-ci/plugins/swarm-client/2.0/swarm-client-2.0-jar-with-dependencies.jar

ENV JAVA_HOME /usr/lib/jvm/java-7-openjdk-amd64  
ENV GROOVY_HOME /usr/share/groovy2  
ENV GIT_PATH /usr/bin/git

CMD java -jar /opt/sw/swarm/swarm-client-2.0-jar-with-dependencies.jar -name ${NODE_NAME} -master http://master_jenkins:9080 -labels esclavo_swarm -t Groovy-1.8.7=$GROOVY_HOME -t default-git=$GIT_PATH

El esclavo se conecta a Jenkins como NODE_NAME mediante Swarm.

Configuración de Jenkins

En nuestro ejemplo, Jenkins se comunica por SSH con la máquina que usaremos para levantar contenedores Docker:

Configuración de SSH

Pasos de construcción

Se debe admitir un parámetro 'node':

nodeParam

Gestión de contenedores desde Jenkins

El Build Flow utiliza dos jobs que arrancan o detienen un contenedor, identificado por un alias handle. Entre ambas llamadas, invoca los pasos sobre el nodo recién levantado (también identificado por handle).

runDockerContainer -> docker run -e NODE_NAME='${handle}' --name=${handle} swarm-slave &
runDockerContainer stopDockerContainer -> docker stop ${handle} && docker rm -v=true ${handle}
stopDockerContainer

Build Flow

params

// Nodo levantado en el contenedor Docker
def getNode(String handle) {  
   // Detección de timeout de esclavo: se puede sacar a parámetros globales
   //    en la configuración de Jenkins
   long msecWait = 500;
   int triesLeft = 10;
   def lookForNode = { String pattern ->
      def ret = null;
      for (slave in hudson.model.Hudson.instance.slaves) {
         if (slave.getNodeName().startsWith(pattern)){
            ret = slave;
         }
      }
      return ret;
   }
   def ret = null;
   // lo intenta $triesLeft veces, separadas cada una por periodos de
   //    de $msecWait milisegundos.  Pasado este tiempo, asume que el
   //    esclavo swarm no ha conseguido levantarse.
   while (ret == null && triesLeft > 0) {
      ret = lookForNode(handle);
      if (ret == null) {
         triesLeft--;
         Thread.sleep(msecWait);
      }
   }
   return ret;
}

long timestamp = new Date().getTime();  
String handle = "swarm_slave_$timestamp";  
String parentWorkspace = "/jenkins/BuildWorkflow$timestamp";  
ignore(FAILURE) {  
   build("runDockerContainer", handle: "$handle" )
   def swarmNode = getNode(handle);
   if (swarmNode == null) throw new Exception("El esclavo no ha arrancado");
   println "Nodo levantado en contenedor Docker: $swarmNode";
   // Lanzar los jobs en la lista contra el nodo indicado
   def jobs = params["jobsList"].split("\n");

   jobs.each { String job ->
      println "********** EJECUTANDO $job";
      // Este ejemplo no gestiona errores ni parámetros
      Map jobParams = [:]
      jobParams.put("parentWorkspace", parentWorkspace);
      jobParams.put("node", swarmNode.getNodeName());
      build(jobParams, job);
   }
}

build("stopDockerContainer", handle: "$handle" )

Para un workflow que baja un proyecto de Git y construye, la ejecución queda así:

buildTreeDocker

(Se marca en azul aquellos jobs ejecutados sobre un esclavo)

Mejoras posibles...

Se puede reducir la imagen Docker siguiendo consejos como el de Henn Idan en su blog, teniendo en cuenta las advertencias que hace al respecto.

Se podría crear un pool de máquinas, esclavas de Jenkins, sobre las que lanzar contenedores, cambiando el lanzamiento de forma que mantenga ocupado el ejecutor. Así, con el plugin Least Load, el propio Jenkins nos ayudaría a escalar en horizontal.

El script groovy del Build Flow admite varias mejoras: parametrización, lanzamientos en paralelo, control de errores, etc.

Si te ha gustado y quieres estar al día de nuevos posts ¡Sigue a “En mi local funciona” en Twitter!

Jorge Bau Gracia
Autor

Jorge Bau Gracia

Pertenezco al Centro Experto de Entrega Continua de atSistemas. Vengo del desarrollo web y de escritorio y me dedico a la automatización de la construcción y el despliegue de aplicaciones.