Pages

Wednesday, April 10, 2019

Full Build Automation For Java application Using Docker Containers

Share it Please
In this pipeline implementation, we will be using Dockers containers fully. We will be using docker for building our java application. We will run Jenkins inside a Docker container, start maven container from Jenkins container to build our code, Run test cases in another Maven Container, Generate the artifact ( jar in this case ), then Build a docker image inside the Jenkins Container itself and push that to the Docker Hub at the end from Jenkins Container.
For this Pipeline , we will be using 2 Github repositories.
1. Jenkins-complete - This is the main repository. This repo contains configuration files for starting Jenkins Container.

2. Simple-java-maven-app - This is our sample java application created using maven. 

We need to understand both repo before building this automation. 

Understanding Jenkins-complete
This is the core repository as this will contain all necessary files that build our jenkins image. Jenkins officially provides a Docker Image by which we can start the container. Once the container is started, we need to perform many things as installing plugins, creating a user etc. 


Once these are created, we need then create credentials for Github to check our sample java application, and a Docker credential for pushing the finally created docker image to dockerhub. Finally we have to create the pipeline job in Jenkins for building our application.

This is a long process our goal is to completely automate all these things. This repo contains files and configuration details which will be used while creating the image. Once the image is created and ran , we have

  1. User admin/admin created
  2. Plugins installed
  3. Credentials for Github and Docker are created
  4. Pipeline job with name sample-maven-job is created.
If we checkout the source code and do a tree, we can see the below structure,
jagadishmanchala@Jagadish-Local:/Volumes/Work$ tree jenkins-complete/
jenkins-complete/
├── Dockerfile
├── README.md
├── credentials.xml
├── default-user.groovy
├── executors.groovy
├── install-plugins.sh
├── sample-maven-job_config.xml
├── create-credential.groovy
└── trigger-job.sh


Lets see what each file talks about
default-user.groovy - this is the file that creates the default user admin/admin.
 

executors.groovy - this is the groovy script that sets the executors in the jenkins server with value 5. A Jenkins executor can be treated as single process which allow a Jenkins job to run on a respective slave/agent machine.

create-credential.groovy - Groovy script for creating credentials in the jenkins global store. This file can be used to create any credential in the jenkins global store. This file is used to create Docker hub credentials. We need to change the username and secret entries in the file by adding our docker hub username and password. This file will be copied to the image and ran when the server starts up

credentials.xml - this is the xml file which will contain  our credentials. This file contain credentials for both Github and Docker. The credential looks like below,
 <com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl>
          <scope>GLOBAL</scope>
          <id>github</id>
          <description>github</description>
          <username>jagadish***</username>
   <password>{AQAAABAAAAAQoj3DDFSH1******</password>      </com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl>

If you see the above snippet, we have the id as “github”, username and encrypted password. The id is very important as we will be using this in the pipeline job that we create.

How can i get the encrypted values for my password
In order to get encrypted content for your password, go to a running jenkins server -> Manage Jenkins -> Script console.In the text field it provides enter the code,
import hudson.util.Secret
def secret = Secret.fromString("password")
println(secret.getEncryptedValue())


In the place of password, enter your password which when you run gives you the encrypted password. You can paste the content in the credentials.xml file.
The same thing is used to generate the DockerHub password too. 


sample-maven-job_config.xml - this is the xml file which contains our pipeline job details. This will be used by the jenkins to create a job by the name “sample-maven-job” in the Jenkins console. This job will be configured with the details defined in this xml file.

The configuration is simple, Jenkins will read this file to create a job “sample-maven-job” pipeline job, sets the SCM pointing to the github location. This will also be configured with the credential set to “github” id. This looks something like this,



Once the scm is set, it also set the job with a Token for triggering the job remotely. For this we have to enable the “trigger builds remotely” and provide a token over there. This is available under the “Build Triggers” section in the pipeline job. I have given the token as “MY-TOKEN” which will be used in our shell script to trigger the job.

 

trigger-job.sh - This is simple shell script which contains a Curl command for running the job.

Though we create the entire jenkins Server inside a container, and create a job. We need a way to trigger that job in order to make that whole build automated. I preferred a way of

  1. Creating the jenkins docker container with all necessary things like job creation, credentials, users etc
  2. Trigger the job once the container is up and running.
I have written this simple shell script to trigger the job once the container is up and running. The shell script is simple curl command sending a post request to the jenkins server. The content looks something like this,

jagadishmanchala@Jagadish-Local:/Volumes/Work/jenkins-complete$cat trigger-job.sh
#!/bin/bash

HOSTNAME=`hostname -i`
curl --silent -I -u admin:admin http://$HOSTNAME:8080/job/sample-maven-job/build?token=MY_TOKEN

As said earlier, we will be using the token “MY_TOKEN” that we configured in the build triggers earlier. We use that in the curl command to trigger our job.

Install-plugins.sh - This is the script that we will use to install the necessary plugins. We will copy this script to the jenkins images passing the plugin names as arguments. Once the container is started, the script is ran taking the plugins as arguments and are installed.


Dockerfile - The most important file in this automation. We will use the Docker file to build the whole jenkins server with all configurations. Understanding this file is very important if you want to write your own build automations.

FROM jenkins/jenkins:lts - We will be using the jenkins image provided officially. 

ARG HOST_DOCKER_GROUP_ID  - One important things to keep in mind is that though we create Docker containers from the jenkins Docker container, we are not actually creating containers inside the jenkins rather we  are creating them on the host machine itself. This means we tell the docker tool installed inside the jenkins docker container to delegate the request of creating the maven container to the host machine. In order for the delegation to happen we need to have the same groups configured on the jenkins docker container and on host machine

To allows access for a non-privileged user like jenkins, we need to add jenkins user to the docker group. In order to make things works, we must assures that the docker group inside the container has the same GID as the group on the host machine. The groupid can be obtained using the command “getent group docker”

Now the HOST_DOCKER_GROUP_ID is set as build argument which means we need to send the groupid for the docker on host machine to the image file while building this. We will sending this value as an argument which building that.

# Installing the plugins we need using the in-built install-plugins.sh script
RUN install-plugins.sh pipeline-graph-analysis:1.9 \
    cloudbees-folder:6.7 \
    docker-commons:1.14 \

The next instruction is to run the “install-plugins.sh” script passing the plugins to be installed as arguments. The script is provided by default or we can copy from our host machine.

# Setting up environment variables for Jenkins admin user
ENV JENKINS_USER admin
ENV JENKINS_PASS admin

We set the JENKINS_USER and JENKINS_PASS environment variables. These variables are passed to the default-user.groovy script for creating user admin with password admin.

# Skip the initial setup wizard
ENV JAVA_OPTS -Djenkins.install.runSetupWizard=false

This lets jenkins to be installed in silent mode

# Start-up scripts to set number of executors and creating the admin user
COPY executors.groovy /usr/share/jenkins/ref/init.groovy.d/
COPY default-user.groovy /usr/share/jenkins/ref/init.groovy.d/
COPY create-credential.groovy /usr/share/jenkins/ref/init.groovy.d/

The above scripts as we discussed will set the executors to 5 and create a default user admin/admin.

One important thing to remember here is, if we check the jenkins official Docker image we will see a VOLUME set to /var/jenkins_home. This means this is the home directory for our jenkins server similar to /var/lib/jenkins when we install on physical machine.

But once a volume is attached, only root user has the capability to edit files, add files over there. In order to let non-privileged user “jenkins” to copy content to this location, jenkins provides us a way. Copy all the scripts, configuration files etc to the location /usr/share/jenkins/ref/ location. Once the container is started, the jenkins will take care of copying the content from this location to the /var/jenkins_home as jenkins users.

Similarly scripts copied to /usr/share/jenkins/ref/init.groovy.d/ will be executed when the server gets started up.

# Name the jobs 
ARG job_name_1="sample-maven-job"
RUN mkdir -p "$JENKINS_HOME"/jobs/${job_name_1}/latest/ 
RUN mkdir -p "$JENKINS_HOME"/jobs/${job_name_1}/builds/1/
COPY ${job_name_1}_config.xml /usr/share/jenkins/ref/jobs/${job_name_1}/config.xml
COPY credentials.xml /usr/share/jenkins/ref/
COPY trigger-job.sh /usr/share/jenkins/ref/

In the above case, iam setting my job name as “sample-maven-job” and creating the directories and copying the files. 


RUN mkdir -p "$JENKINS_HOME"/jobs/${job_name_1}/latest/ 
RUN mkdir -p "$JENKINS_HOME"/jobs/${job_name_1}/builds/1/

The above 2 instructions are very important, these create a jobs directory in the jenkins home where we need to copy the job configuration file. The latest and builds/1 are also need to be created in the job location for that specific job.

Once these are created, we are copying our “sample-maven-job_config.xml” file to the /var/share/jenkins/ref and asking jenkins to copy the file to the /var/jenkins_home/jobs/ as sample-maven-job.

Finally we are also copying the  credentials.xml and trigger-job.sh files to the /usr/share/jenkins/ref. Once the container is started, all content available in this location will be moved to /var/jenkins_home as jenkins user.

USER root
#RUN chown -R jenkins:jenkins "$JENKINS_HOME"/
RUN chmod -R 777 /usr/share/jenkins/ref/trigger-job.sh

# Create 'docker' group with provided group ID
# and add 'jenkins' user to it
RUN groupadd docker -g ${HOST_DOCKER_GROUP_ID} && \
    usermod -a -G docker jenkins

RUN apt-get update && apt-get install -y tree nano curl sudo
RUN curl https://get.docker.com/builds/Linux/x86_64/docker-latest.tgz | tar xvz -C /tmp/ && mv /tmp/docker/docker /usr/bin/docker
RUN curl -L "https://github.com/docker/compose/releases/download/1.23.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
RUN chmod 755 /usr/local/bin/docker-compose
RUN usermod -a -G sudo jenkins
RUN echo "jenkins ALL=(ALL:ALL) NOPASSWD:ALL" >> /etc/sudoers

RUN newgrp docker
USER jenkins

The next instructions are executed as root user.  Under the root instructions we are creating the docker group with the same groupid as host machine passed as argument. Then we are modifying the jenkins user by adding that to the docker group. By this we can create container from the jenkins user. This is very important as docker container can only be created by root user. In order to create them with jenkins, we need to add the jenkins user to the docker group which we are doing.

In the next instructions, we are installing the docker-ce and docker-compose tools. We are also setting the permissions on the docker-compose tool. Finally we are also adding the jenkins to the sudoers file to give certain permissions root user

RUN newgrp docker
This instruction is very important. When ever we modify the groups of a user, we need to logout and login to reflect the changes.  In order to bypass the logout and login we use this “newgrp docker” instruction to reflect the changes. Finally we change back to the jenkins user.

Build the Image - Once we are good with the Docker file, to create a image from this we need to run,
docker build --build-arg HOST_DOCKER_GROUP_ID="`getent group docker | cut -d':' -f3`" -t jenkins1 .

From the location where we have our Dockerfile, run the above docker build instruction. In the above command, we are passing the build-arg with the value of the groupid for the docker user. This value will be passed to the “HOST_DOCKER_GROUP_ID” which will be used to create the same groupid in jenkins Docker container. The Image build will take some time since it need to download and install the plugins for jenkins servers.

Running the Image - Once the Image is built, we need to run the container as
docker run -itd -v /var/run/docker.sock:/var/run/docker.sock  -v $(which docker):/usr/bin/docker -p 8880:8080 -p 50000:50000 jenkins1

Two important things in here are that volume that we are mounting. We are mounting the docker command line utility to the container, so that if another container need to be created from container, this can be used.

The most important one is the /var/run/docker.sock mount. Docker.sock is a UNIX socket that docker daemon is listening to. This is the main entrypoint for Docker API. this can also be a TCP socket but by default for security reasons it is UNIX socket.

Docker uses this socket to execute docker command by default. The reason why we mount this to a docker container is to launch new containers from container. This can also be used for auto service discovery and logging purpose. This increases attack surface so we need to be very careful mounting this.

Once the command is ran, we will get the jenkins container up and running. Use the URL “<ip address>:8880” to see the Jenkins console. Once the console is up, login using “admin/admin”. We will see our sample-maven-job created with SCM, Token and credentials But not ran yet

Running the JOB - In order to run the job, all we have to do is to take the containerID and run the trigger-job.sh job as below,

docker exec <Jenkins Container ID> /bin/sh -C /var/jenkins_home/trigger-job.sh

Once you run the command we can see the building the pipeline job get started.


Understanding simple-java-maven-app
As we already said this repo contains our java application. The application is created using maven artifacts. The repo contains a Dockerfile, Jenkinsfile and Source code. The source code is quite similar as other maven based applications.

Jenkinsfile - The Jenkins file is the core file that will be ran when the sample-maven-job is started. The pipeline job download the source code from the github location using the github credential.

The most important thing in the Jenkinsfile is the agent definition. We will be using “agent any” for building our java code from any available agents. But we will define agents when we go to specific stages to run the stage.

stage("build"){
        agent {
                docker {
                    image 'maven:3-alpine'
                    args '-v /root/.m2:/root/.m2'
                  }
             }
      
       steps {
              sh 'mvn -B -DskipTests clean package'
                stash includes: 'target/*.jar', name: 'targetfiles'
            } 
   

If you see the above stage, we are setting the agent as docker with image file “maven:3-alpine”. So jenkins will trigger a docker run maven:3-alpine container and run the command defined in the steps as “mvn -B -DskipTests clean package”

Similarly the test cases are also ran in the same way. It trigger a docker run with the maven image and then run the “mvn test” on the source code.

environment {
    registry = "docker.io/<user name>/<image Name>"
    registryCredential = 'dockerhub'
    dockerImage = ''
}


The other important this is the environment definition. I have defined the registry name as “docker.io/jagadesh1982/sample” which mean when we create a image with the final artifact (jar), the image name will be “docker.io/jagadesh1982/sample:<version>. This is very important if you want to push the images to the dockerhub. Dockerhub expects to have the image name with “docker.io/<user Name>/<Image Name>” in order to upload.

Once the building of the image is done, it is then uploaded to the DockerHub and then removed from the jenkins docker container.

Dockerfile - This repo also contains a Dockerfile which will be used to create a docker image with the final artifact. This means it copies the my-app-1.0-SNAPSHOT.jar to the docker image. It also run the container from image.

Hope this helps, More to Come. Happy learning :-)

No comments :

Post a Comment