Building Maven Projects in Jenkins Docker workers

This is a second post in a series, describing the problems my team has faced during implementation of Jenkins pipelines in Kubernetes.

  1. Jenkins Java Centric Pipelines in Kubernetes
  2. Building Maven Projects in Jenkins Docker workers (This article)
  3. Using Maven and Jenkins to perform modular Java builds
  4. Building Docker Images in Jenkins on Kubernetes

Building Maven projects in Docker or Kubernetes can be trickier than it first appears. In this post I’ll show you the steps required to make it work and a few tricks and workarounds.


Maven Plugin

Maven plugin saves the build engineer many of the tedious tasks, like: installing required version of Maven on the build machine, providing Maven configuration (setting.xml), uploading build artifacts and unit test reports. While useful, it wasn’t well adapted to run in Docker environment, much less in Kubernetes.

Another issue is that the Maven / Java path broken: it doesn’t injects the settings correctly.

Tool Configuration -> Maven
Name: mvn (Or similar)
MAVEN_HOME: /usr/share/maven (Must match the path inside the container)

Code example:

    withMaven(maven:'mvn',
            options: [artifactsPublisher(disabled: true), openTasksPublisher(disabled: true), junitPublisher(disabled: true)]) {
        sh "export PATH=/usr/local/openjdk-8/bin:$MVN_CMD_DIR:$PATH " +
            "&& mvn -s settings.xml -B install -Drevision=${params.CURRENT_VERSION}"
    }

Explanation:
maven:'mvn'
Here you set the maven installation you have have defined in the Tool Configuraion page.

options: [artifactsPublisher(disabled: true), openTasksPublisher(disabled: true), junitPublisher(disabled: true)]
The publishers are enabled by default. Unless you wish saving artifacts in the Jenkins master, you need to mark them as disabled.

export PATH=/usr/local/openjdk-8/bin:$MVN_CMD_DIR:$PATH
This is a workaround for the broken path when using a Maven image.

mvn -s settings.xml
Here you define the Maven configuration file (usially residing in ${HOME}/.m2 folder)

In case of Kubernetes, you are most likely to use a Maven Docker image in your pod, rather than having Maven plugin installing it for you. I went one step further and preemptively caching the packages in .m2/repository folder to avoid costly downloading of rarely changing 3rd party dependencies during every build.

Caching Jenkins workspace

And finally you’ll have to decide on the best approach to handle the stateless nature of building in Docker images / Kubernetes.
It basically runs down to:

  • Do you want to cache Jenkins workspace?
  • Do you want to want to cache the artifacts?

Benefits
Caching the Jenkins workspace, will make the build as close as it possibly get to your development environment on your desktop.
Meaning that you get artifact caching between builds, and even incremental builds if you choose to avoid the maven “clean” stage.

Drawbacks
The biggest strength of building in a container is the isolation between projects, branches and previous build runs. Caching the workspace negates this isolation, possibly introducing various state related issues and errors.

Caching Maven artifacts

Maven Docker image comes with an empty repository causing a costly download of every required dependency on every build. Due to a great performance tradeoff, we would choose to avoiding it, hence we must resort to caching with two options at hand:

  • Using a shared volume for Maven repository:
    Benefits:

    • Simple to setup.
    • Most efficient with regards to downloading artifacts.

    Drawbacks:

    • It can cause artifact collision if a single volume used across projects or different branches.
    • It adds a maintenance overhead to clean the repository, multiplied if we use separate volumes.

    Code Example:
    Creating persistent volume:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: jenkins-maven-m2
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Gi

Assigning persistent volume to a worker:

apiVersion: v1
kind: Pod
metadata:
  labels:
    buildType: maven
    docker: true
spec:
  containers:
  - name: maven361
    image: maven:3.6.1-jdk-8-slim
        resources:
      requests:
        memory: "2Gi"
        cpu: "2"
      limits:
        memory: "4Gi"
        cpu: "4"
    imagePullPolicy: Always
    command:
    - cat
    tty: true
    volumeMounts:
    - name: pvol
      mountPath: /root/.m2
  volumes:
  - name: pvol
    persistentVolumeClaim:
        claimName: "jenkins-maven-m2"
  • Using a Maven image with a cached .m2 repository: Benefits:

    • Provides a trully isolated environment.
    • Doesn’t require artifact maintenance.

    Drawbacks:

    • Requires an extra job to create cache images.
    • Doesn’t completely eliminate download of artifacts during build.
    • Requies cleaning up of old images.

    Code Example:

    Jenkinsfile:

pipeline {
    agent {
        kubernetes {
            defaultContainer 'docker'
            yaml """
apiVersion: v1
kind: Pod
metadata:
  labels:
    buildType: maven
spec:
  containers:
  - name: docker
    image: docker:stable
    imagePullPolicy: Always
    command:
    - cat
    tty: true
    volumeMounts:
    - name: docker-sock-volume
      mountPath: /var/run/docker.sock
  volumes:
  - name: docker-sock-volume
    hostPath:
      path: /var/run/docker.sock
"""
        }
    }

    stages {
        stage('Build Docker Image') {
            steps {
                echo 'Creating Docker images'
                preBuild(settings)
                script {
                    dockerImage = docker.build("docker-registy/path/maven:3.6.1-master",
                    "--build-arg MAVEN_VER=3.6.1-jdk-8-slim -f base-image/Dockerfile .")
                }
            }
        }
        stage('Deploy Docker Image') {
            steps {
                echo 'Pushing the artifacts'
                script {
                    docker.withRegistry('https://docker-registy', 'registry-key') {
                        dockerImage.push()
                    }
                }
            }
        }
    }
}

Dockerfile:

ARG MAVEN_VER
FROM maven:${MAVEN_VER} as builder
ENV TERM xterm
WORKDIR /root
COPY ./ ./
RUN export PATH=/usr/local/openjdk-8/bin:$MVN_CMD_DIR:$PATH && mvn -s settings.xml -B dependency:go-offline
ARG MAVEN_VER
FROM maven:${MAVEN_VER}
WORKDIR /root
COPY --from=builder /root/.m2 /root/.m2