29 Oct

Industrializar el entorno CI/CD (1): Jenkins

Industrializar software significa, entre otras cosas, que éste debe poder ser reconstruible a partir de un script que automatiza todo el proceso. En el caso de un entorno de CI/CD, también es interesante que esté industrializado: permite verificar la configuración de cada sistema, introducir cambios en un entorno controlado, volver a una versión que se sabe que funciona, etc. La alternativa a esto es tener lo que Martin Fowler denomina un «Snowflake Server«.

En el caso concreto de Jenkins, no disponer de un mecanismo que permita reconstruir el sistema tal cuál estaba puede ser peliagudo, especialmente si se trabaja con la weekly release: la versión actualizada puede tener algún problema, o los plugins ser incompatibles. En estos casos, es complicado volver a la versión anterior, y probar a instalar de nuevo la versión, con los plugins, los jobs, los usuarios, los nodos, etc. y dejarlo todo tal como estaba puede ser realmente complicado; por ejemplo, no es descabellado que al actualizar un plugin, éste decida migrar su configuración en todos los jobs. Si, por lo que sea, es necesario dar marcha atrás al cambio en una situación así, va a haber dolor. Mucho dolor.

Además, no ayuda que no haya un proceso más o menos estándar para poder realizar esta tarea. A día de hoy, la documentación oficial al respecto es básicamente nula. Es cierto que hay algunos plugins de Jenkins que intentan ayudar en esta tarea: Backup Plugin, thinBackup y SCM Sync Backup, pero o están descontinuados, o no se puede programar el backup, o guardan también los workspaces, o no guardan toda la configuración (p.ej., si usas el plugin de promotions ninguno de estos plugins te guardará la configuración de las promociones).

En el trabajo la manera que hemos encontrado de solucionarlo es con dos scripts, el primero de ellos para guardar toda la configuración en el control de versiones (en nuestro caso, svn):

#!/bin/bash
echo '*****************************'
echo '0.- Change into $JENKINS_HOME'
echo '*****************************'
cd /path/to/your/jenkins_home


echo '************************'
echo '1.- update plugins list '
echo '************************'
curl -sSL "http://JENKINS_HOST:JENKINS_PORT/jenkins/pluginManager/api/xml?depth=1&xpath=/*/*/shortName|/*/*/version&wrapper=plugins" | perl -pe 's/.*?<shortName>([\w-]+).*?<version>([^<]+)()(<\/\w+>)+/\1 \2\n/g'|sed 's/ /:/' > plugins.txt


echo '*****************************************************************************'
echo '2.- Template saves wipe out promotions folder, so restore it with local data '
echo '*****************************************************************************'
# IFS weirdness to cd into directories whose names contain spaces, ref. http://www.cyberciti.biz/tips/handling-filenames-with-spaces-in-bash.html
cd jobs
SAVEIFS=$IFS
IFS=$(echo -en "\n\b")
for dir in `find ./* -type d -prune`;
do
  echo "svn update promotions on $dir" && cd $dir && svn update promotions --force --accept=mine-full && cd ..
done
IFS=$SAVEIFS
cd ..


echo '********************************************************************************************************************'
echo '3.- Add any new conf files, jobs with promotions, users, userContent, secrets, nodes and list of installed plugins. '
echo '********************************************************************************************************************'
svn add --force --parents *.xml jobs/*/config.xml jobs/*/promotions/*/config.xml users/*/config.xml userContent/* nodes/* secret.key secret.key.not-so-secret secrets/* plugins.txt


echo '***************************************************'
echo "4.- Ignore things in the root we don't care about. "
echo '***************************************************'
echo "warnlog\n*.log\n*.tmpn*.old\n*.bak\n*.jar\n*.json" > jenkins_ignores && svn propset svn:ignore -F jenkins_ignores . && rm jenkins_ignores 


echo '***************************************************'
echo "5.- Ignore things inside jobs we don't care about. "
echo '***************************************************'
echo "builds\nlast*\nnext*\n*.txt\n*.log\nworkspace*\ncobertura\njavadoc\nhtmlreports\nncover\ndoclinks" > jenkins_ignores && svn propset svn:ignore -F jenkins_ignores jobs/* && rm jenkins_ignores 


echo '***************************************************************'
echo '6.- Remove anything from SVN that no longer exists in Jenkins. '
echo '***************************************************************'
svn status | grep '!' | awk '{print $2;}' | xargs -r svn rm 


echo '**********************************************************************************'
echo '7.- And finally, check in of course, showing status before and after for logging. '
echo '**********************************************************************************'
svn st && svn ci --non-interactive --username=SVN_USER --password=SVN_PASSWORD -m "automated commit of Jenkins configuration" && svn st

exit 0

El punto 2 es bastante específico, dado que su objeto es tener en cuenta las interacciones entre los plugins EZ Templates y promotions; si no tienes instalados ambos, no es problema, este paso no hará nada.

Usamos el propio Jenkins para ejecutar este script con cadencia semanal y almacenar la configuración en el control de versiones. Para recuperar la instalación de Jenkins a partir del control de versiones realizamos estos pasos:

  • check out de la configuración del control de versiones en un directorio, que será el nuevo JENKINS_HOME.
  • descargar, mediante wget o cómo se prefiera, el war apropiado de Jenkins
  • ejecutar el siguiente script, adaptado de la imagen Docker oficial de Jenkins:
#! /bin/bash

# Parse a support-core plugin -style txt file as specification for jenkins plugins to be installed
# in the reference directory, so user can define a derived Docker image with just :
#
# FROM jenkins
# COPY plugins.txt /plugins.txt
# RUN /usr/local/bin/plugins.sh /plugins.txt
#
# Note: Plugins already installed are skipped
#

set -e

JENKINS_WAR=../jenkins.war
JENKINS_HOME=.
JENKINS_PLUGINS_DIR=$JENKINS_HOME/plugins
REF=$JENKINS_HOME/ref
# jenkins update center
JENKINS_UC=${JENKINS_UC:-https://updates.jenkins-ci.org}

if [ -z "$1" ]
then
    echo "
USAGE:
  Parse a support-core plugin -style txt file as specification for jenkins plugins to be installed
  in the reference directory, so user can define a derived Docker image with just :

  FROM jenkins
  COPY plugins.txt /plugins.txt
  RUN /usr/local/bin/plugins.sh /plugins.txt

  Note: Plugins already installed are skipped

"
    exit 1
else
    JENKINS_INPUT_JOB_LIST=$1
    if [ ! -f $JENKINS_INPUT_JOB_LIST ]
    then
        echo "ERROR File not found: $JENKINS_INPUT_JOB_LIST"
        exit 1
    fi
fi

# the war includes a # of plugins, to make the build efficient filter out
# the plugins so we dont install 2x - there about 17!
if [ -d $JENKINS_HOME ]
then
    TEMP_ALREADY_INSTALLED=$JENKINS_HOME/preinstalled.plugins.$$.txt
else
    echo "ERROR $JENKINS_HOME not found"
    exit 1
fi

if [ -d $JENKINS_PLUGINS_DIR ]
then
    echo "Analyzing: $JENKINS_PLUGINS_DIR"
    for i in `ls -pd1 $JENKINS_PLUGINS_DIR/*|egrep '\/$'`
    do
        JENKINS_PLUGIN=`basename $i`
        JENKINS_PLUGIN_VER=`egrep -i Plugin-Version "$i/META-INF/MANIFEST.MF"|cut -d\: -f2|sed 's/ //'`
        echo "$JENKINS_PLUGIN:$JENKINS_PLUGIN_VER"
    done > $TEMP_ALREADY_INSTALLED
else
    #JENKINS_WAR=/usr/share/jenkins/jenkins.war
    if [ -f $JENKINS_WAR ]
    then
        echo "Analyzing war: $JENKINS_WAR"
        TEMP_PLUGIN_DIR=/tmp/plugintemp.$$
        echo "TEMP_PLUGIN_DIR: $TEMP_PLUGIN_DIR"
        for i in `jar tf $JENKINS_WAR|egrep 'plugins'|egrep -v '\/$'|sort`
        do
            echo "TEMP_PLUGIN_DIR: $TEMP_PLUGIN_DIR"
            rm -fr $TEMP_PLUGIN_DIR
            mkdir -p $TEMP_PLUGIN_DIR
            PLUGIN=`basename $i|cut -f1 -d'.'`
            echo "$PLUGIN"
            (cd $TEMP_PLUGIN_DIR;jar xf $JENKINS_WAR "$i";jar xvf $TEMP_PLUGIN_DIR/$i META-INF/MANIFEST.MF >/dev/null 2>&1)
            VER=`egrep -i Plugin-Version "$TEMP_PLUGIN_DIR/META-INF/MANIFEST.MF"|cut -d\: -f2|sed 's/ //'`
            echo "$PLUGIN:$VER"
        done > $TEMP_ALREADY_INSTALLED
        rm -fr $TEMP_PLUGIN_DIR
    else
        rm -f $TEMP_ALREADY_INSTALLED
        echo "ERROR file not found: $JENKINS_WAR"
        exit 1
    fi
fi

#REF=/usr/share/jenkins/ref/plugins
mkdir -p $REF
COUNT_PLUGINS_INSTALLED=0
while read spec || [ -n "$spec" ]; do

    plugin=(${spec//:/ });
    [[ ${plugin[0]} =~ ^# ]] && continue
    [[ ${plugin[0]} =~ ^\s*$ ]] && continue
    [[ -z ${plugin[1]} ]] && plugin[1]="latest"

    if [ -z "$JENKINS_UC_DOWNLOAD" ]; then
      JENKINS_UC_DOWNLOAD=$JENKINS_UC/download
    fi

    if ! grep -q "${plugin[0]}:${plugin[1]}" $TEMP_ALREADY_INSTALLED
    then
        echo "Downloading ${plugin[0]}:${plugin[1]}"
        curl --retry 3 --retry-delay 5 -sSL -f ${JENKINS_UC_DOWNLOAD}/plugins/${plugin[0]}/${plugin[1]}/${plugin[0]}.hpi -o $REF/${plugin[0]}.jpi
        unzip -qqt $REF/${plugin[0]}.jpi
        COUNT_PLUGINS_INSTALLED=`expr $COUNT_PLUGINS_INSTALLED + 1`
    else
        echo "  ... skipping already installed:  ${plugin[0]}:${plugin[1]}"
    fi
done  < $JENKINS_INPUT_JOB_LIST

echo "---------------------------------------------------"
if [ $COUNT_PLUGINS_INSTALLED -gt 0 ]
then
    echo "INFO: Successfully installed $COUNT_PLUGINS_INSTALLED plugins."

    if [ -d $JENKINS_PLUGINS_DIR ]
    then
        echo "INFO: Please restart the container for changes to take effect!"
    fi
else
    echo "INFO: No changes, all plugins previously installed."

fi
echo "---------------------------------------------------"

#cleanup
rm $TEMP_ALREADY_INSTALLED
exit 0

¡Profit!

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *