How to cook Jenkins in 2017

Since Jenkins was born in another IT age, pre-Cloud and pre-DevOps (and was called Hudson at this time), it appears not that lightweight and straightforward manageable as we might expect of today's tools. Still it's the most prominent CI tool and deserve attention. Furthermore with recent features (see below) Jenkins seems to catch up with CI tools 2.0

Let's name some challenging moment with Jenkins

  • Jenkins Plugin hell dependencies management - that are tending to make the system fragile e.g. on updates.
  • XML based configuration, that is not documented well and differs from plugin to plugin
  • Dependency on used tools. I mean Jenkins depends on tools that are used in Jenkins Jobs. E.g. java, go, c++, maven, ant, python.. in different versions, that are tending to live their own lifes.

But meanwhile we have tools and methods to handle these Jenkins problems.
In 2017 we simply can run Jenkins in a Docker container and profit from better Isolation and reproducibility of Jenkins installation. Let's get's hands on it...

Jenkins Docker image

Of course there is Official Jenkins Container, so we don't need to start from scratch.

FROM jenkins:2.46.3  
...

This can be a beginning of your customized Dockerfile. Version 2.46.3 is current LTS Jenkins version. Next will be probably 2.60.0.

Let's improve Jenkins plugin management form begin on. The nice things about the official Docker image is, that it includes script that is best for pre-install desired plugins. And this with (automatic) dependency resolution. Here an example:

FROM jenkins:2.46.3  
#Install plugins with dependencies
RUN /usr/local/bin/install-plugins.sh workflow-aggregator:2.5 workflow-job:2.11 \  
    git \
    maven-plugin m2release checkstyle findbugs jacoco \
    dashboard-view disk-usage jobConfigHistory \ 
    scm-sync-configuration \
...

We see some well know plugins whereat workflow-aggregator and workflow-job are pinned to a specific version. I case of workflow-job it was needed since latest version (2.12) is transitively included by workflow-aggregator but requires Jenkins jenkins:2.60> which is not in LTS status now (bug from plugin maintainers i would say). However version pinnig and explicit declaration is there to help.

You need to decide what else you need to be included in your Docker image.

Jenkins Configuration

Another nice feature of the official Jenkins Image is the fact that everything that you place to /usr/share/jenkins/ref/ during build time will be placed to /var/jenkins_home on container start.

So just COPY/ADD need files like..

# Jenkins config
COPY configuration/*.groovy /usr/share/jenkins/ref/init.groovy.d/  
COPY configuration/log.properties /usr/share/jenkins/ref/log.properties.override  

By the way you can do all pre-configurations you need via groovy scrips. Here e.g. we declare default java temp directory and disable Jenkins installation Wizard.

import jenkins.*  
import hudson.*

Thread.start {  
    System.setProperty('jenkins.install.runSetupWizard', "false")
    println "--> Startup Wizard is disabled"

    System.setProperty('java.io.tmpdir','/var/jenkins_home')
    println "--> Setting java.io.tmpdir"
}

E.g. you may want include or better generate some ssh-keys that can be used for identification into the Docker image but maybe it's also better to configure such thing later in the UI by placing it in the secure store of Jenkins - decisions you do on yourself.

More likely you want't to trust your organization and commiting public keys is Ok. So here is example hot to teach java that is of course included in to Jenkins container to trust your root certificates.

COPY ssl/*.pem /etc/pki/java/

#Jenkins container runs with user Jenkins so we need switch to root tempory.
USER root

# SSL Trust Certs: Importing to java & to system
RUN bash -c 'echo "Yes" | /usr/lib/jvm/java-8-openjdk-amd64/bin/keytool -import -alias my Root -file /etc/pki/java/myrootca.cert.pem -keystore /usr/lib/jvm/java-8-openjdk-amd64/jre/lib/security/cacerts -storepass secretpass'

#And placing it in the system so e.g. subversion can access your server via SSL
RUN cp /etc/pki/java/myrootca.cert.pem /usr/local/share/ca-certificates/ && update-ca-certificates  
#back to user Jenkins
USER jenkins  

And here is an example of AWS CLI installation

# AWS ClI 
RUN curl "https://s3.amazonaws.com/aws-cli/awscli-bundle.zip" -o "awscli-bundle.zip" && unzip awscli-bundle.zip && ./awscli-bundle/install -i /usr/local/aws -b /usr/local/bin/aws  

Also you might want to install some system packages. At this point you should think twice, because you might run into the problem that you need to rebuild your Jenkins image on changing packet version or you need that packages in different version for different jobs and everything get messy. But if it's not an issue here is an example for installation of Ansible

#Ansible source list
RUN echo "deb http://ppa.launchpad.net/ansible/ansible/ubuntu trusty main" >> /etc/apt/sources.list && \  
    apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 93C4A3FD7BB9C367

# Install additional packages.
RUN apt-get update && apt-get install -y ansible python-dnspython && rm -rf /var/lib/apt/lists/*  

if you're trusting your job builders you can include sudo package as well and define rules

# Allow Jenkins user to execute  /path/to/my/program as root
RUN bash -c 'echo "jenkins ALL=(ALL) NOPASSWD: /path/to/my/program" >> /etc/sudoers'  

Please consider the risks, do not user keyword ALL insetead of specific program if possible.

Docker client

Next important thing is to have a possibility to build docker images on your Jenkins. Obviosly it can be a good idea to use the same docker host where out Jenkins container runs. First we need to install docker client inside of the container.

USER root  
RUN curl https://get.docker.com/builds/Linux/x86_64/docker-latest.tgz | tar xvz -C /tmp/ && mv /tmp/docker/docker /usr/bin/docker  
USER jenkins  

But this requires further actions that we need to do if container starts. For this we change entrypoint to our custom script, that is capable to execute needed actions before starting Jenkins.

ENTRYPOINT ["/bin/tini", "--", "/usr/local/bin/init.sh"]  

to wire docker client from inside of the container to the hosts docker-engine we need to start the Jenkins with volume that is docker-engine's socket (file representation on linux)

docker run -p 8080:8080 -v /var/run/docker.sock:/var/run/docker.sock my-jenkins  

and in the init.sh we put the following.

#!/usr/bin/env bash
echo "=== Docker INFO ======================================="  
if [ -S ${DOCKER_SOCKET} ]; then  
    echo "Mapping docker socket"
    sudo chmod 777 /var/run/docker.sock
fi  
...

of course we can do all other need run-time configuration here. To start Jenkins we just include these lines at the end.

echo "==== Finishing prestarting actions ===================="  
echo "Starting /usr/local/bin/jenkins.sh as $(whoami) - $(id)"

exec /usr/local/bin/jenkins.sh;  

Jenkins Declarative Pipeline

Now we nearly done, but there is more. Take a look on Jenkins Declarative pipelines. Here is one example to illustrate something important.

pipeline {  
    agent none 
    stages {
        stage('Example Build') {
            agent { docker 'maven:3-alpine' } 
            steps {
                echo 'Hello, Maven'
                sh 'mvn --version'
            }
        }
        stage('Example Test') {
            agent { docker 'openjdk:8-jre' } 
            steps {
                echo 'Hello, JDK'
                sh 'java -version'
            }
        }
    }
}

Most certainly an attentive reader has already noticed that compilation steps are executed in different docker containers and the workspace is prepared automatically. This is the answer to the complexity of "tool configuration" and is much readable and maintenable that native xml-based job configuration. You don't need to install maven or java. Docker images are predefined and downloaded automatically. So this pipeline is reusable after upgrades or disasters and on other Jenkins installation. You can put this declaration into s.c. Jenkinsfile and commit it to your scm.

Worth to mention, Jenkins team is working on Editor for pipelines
However i prefer to write it directly, and syntax reference might be helpful here.

Thank you for reading, and thank you for your comments.