CI/CD with Jenkins and Ansible
It’s 2018. Kubernetes won the container orchestration battle. Some of us are jealously reading those articles by the Silicon Valley startups (yeah maybe they are already in your city, too!) but then we go back to our good old legacy systems.
Trunk-based development, containers in the cloud are on the road map but in short term they are simply not possible to implement.
A step into the DevOps direction is to eliminate silos (dev, QA, ops) and therefore we have to structure our code in a way that it enables each role to collaborate easily.
I’ve read a lot of very good articles about how to build a Spring Boot backend with some single page Javascript app, also about configuration management, infrastructure provisioning, continuous integration and delivery but now I’m going to combine all that, and provide some scaffolding for you to build on.
Setup
What I have at hand is a Jenkins instance, ssh and a nice runnable Spring Boot jar. Also a RedHat7 VM and a Nexus as artifact repository. So I guess I should be happy I’m not deploying EARs anymore!

Now I’m going to build a deployment pipeline with those tools and put everything into version control, so that everyone on the team has access to everything and knows what happens with their piece of code from commit to deployment (in this case only until a test environment).
I use the following structure:
parent
+- backend
+- frontend
+- deployment
Jenkinsfile
For the sake of simplicity backend
— a Spring Boot app — contains the frontend
ReactJS app, deployment
is where the tools are for continuous delivery, and the Jenkinsfile
in the root directory is the declarative descriptor of our pipeline.

Let’s have a little look into those modules!
Backend
First it inherits from the Spring Boot parent:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
<relativePath/>
</parent>
Let’s include the frontend
app among other dependencies:
<dependencies>
...
<dependency>
<groupId>com.company.skeleton</groupId>
<artifactId>frontend</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
...
</dependencies>
I also use Spotbugs
, Checkstyle
and Jacoco
for static analysis and code coverage, so we have to include those plugins as well. Notice the security plugin of Spotbugs
which is a small shift left on security.
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
</configuration>
</plugin>
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>3.1.3.1</version>
<configuration>
<effort>Max</effort>
<threshold>Low</threshold>
<failOnError>true</failOnError>
<plugins>
<plugin>
<groupId>com.h3xstream.findsecbugs</groupId>
<artifactId>findsecbugs-plugin</artifactId>
<version>LATEST</version>
</plugin>
</plugins>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.0.0</version>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.1</version>
<configuration>
<fileSets>
<fileSet>
<directory>${project.build.directory}</directory>
<includes>
<include>*.exec</include>
</includes>
</fileSet>
</fileSets>
</configuration>
<executions>
<execution>
<id>default-prepare-agent</id>
<phase>process-classes</phase>
<goals>
<goal>prepare-agent</goal>
</goals>
<configuration>
<destFile>${project.build.directory}/jacoco.exec</destFile>
</configuration>
</execution>
<execution>
<id>pre-integration-test</id>
<phase>pre-integration-test</phase>
<goals>
<goal>prepare-agent</goal>
</goals>
<configuration>
<destFile>${project.build.directory}/jacoco-it.exec</destFile>
<propertyName>failsafeArgLine</propertyName>
</configuration>
</execution>
<execution>
<id>post-integration-test</id>
<phase>post-integration-test</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<dataFile>${project.build.directory}/jacoco-it.exec</dataFile>
<outputDirectory>${project.reporting.outputDirectory}/jacoco-it</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
</plugin>
</plugins>
</build>
Now we’re ready to move to frontend.
Frontend
As we need a library that we can include as a maven dependency, we’ll copy the built resources into the public
directory of that jar with the maven-resources-plugin
.
But first we need to build and test this module as well. We’ll use the frontend-maven-plugin
for that but both of these steps could be done with a script or directly within the Jenkinsfile
if one doesn’t like the maven approach.
<build>
<plugins>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
<executions>
<execution>
<id>prepare-package</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/target/classes/public</outputDirectory>
<resources>
<resource>
<directory>${project.basedir}/build</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.6</version>
<executions>
<execution>
<id>install node and yarn</id>
<goals>
<goal>install-node-and-yarn</goal>
</goals>
<configuration>
<nodeVersion>v9.9.0</nodeVersion>
<yarnVersion>v1.5.1</yarnVersion>
</configuration>
</execution>
<execution>
<id>yarn</id>
<goals>
<goal>yarn</goal>
</goals>
<phase>prepare-package</phase>
<configuration>
</configuration>
</execution>
<execution>
<id>yarn build</id>
<goals>
<goal>yarn</goal>
</goals>
<phase>prepare-package</phase>
<configuration>
<arguments>build</arguments>
</configuration>
</execution>
<execution>
<id>test</id>
<goals>
<goal>yarn</goal>
</goals>
<phase>test</phase>
<configuration>>
<arguments>test</arguments>
<environmentVariables>
<CI>true</CI>
</environmentVariables>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Now let’s build everything with Jenkins!
Jenkinsfile
We are going to create the following pipeline here:

And we’re using the declarative way.
In the Build
stage we build frontend
and backend
in parallel.
Of course we have to keep in mind that backend
depends on the artifact produced by the frontend
module, therefore we have to include another step to create the runnable jar after the two parallel builds but this time we can skip running the tests.
pipeline {
agent { label 'RHEL' }
tools {
maven 'Maven 3.3.9'
jdk 'jdk1.8.0'
}
stages {
stage('Build') {
parallel {
stage('Build Backend'){
steps {
dir('backend'){
sh 'mvn clean test spotbugs:spotbugs checkstyle:checkstyle'
}
}
post {
always {
junit 'backend/target/surefire-reports/*.xml'
findbugs canComputeNew: false, defaultEncoding: '', excludePattern: '', healthy: '', includePattern: '', pattern: '**/spotbugsXml.xml', unHealthy: ''
checkstyle canComputeNew: false, defaultEncoding: '', healthy: '', pattern: '**/checkstyle-result.xml', unHealthy: ''
jacoco()
}
}
}
stage('Build Frontend'){
steps {
dir('frontend'){
sh 'mvn clean install'
}
}
}
}
}
stage('Create runnable jar'){
steps {
dir('backend'){
sh 'mvn deploy -DskipTests'
}
}
}
}
}
Probably you noticed I used mvn deploy
and not mvn install
, and that’s because we’re using an artifact repository here which is Nexus.
That’s our single source of truth where all our built artifacts are stored. This is the place where we will pull the artifacts from in all of our environments.
This artifact repository has to be defined in the backend
‘s pom.xml
.
<distributionManagement>
<repository>
<uniqueVersion>true</uniqueVersion>
<id>Releases</id>
<layout>default</layout>
<url>http://nexus.edudoo.com/</url>
</repository>
<snapshotRepository>
<uniqueVersion>false</uniqueVersion>
<id>Snapshots</id>
<layout>default</layout>
<url>http://nexus.edudoo.com/</url>
</snapshotRepository>
</distributionManagement>
Deployment
As I mentioned I have a RedHat7 virtual machine and ssh access. The simplest tool that requires ssh access only is Ansible, so we go with that, and it has to be installed on the Jenkins node.

Another decision to make is how to run our application. We could create some shell scripts to start/stop the java jar but a little bit more sophisticated solution is to use a process/service manager.
I could have chosen Supervisor or others but that’s not out of the box supported on our RedHat Linux machine, so let’s just stick with systemd.
The steps that we are going to perform each time are as follows:
- prepate the environment by installing the required packages,
- prepare and push the configuration of the application,
- pull the jar from Nexus,
- create (or update) and (re)start the systemd service.
In our case creating the environment means that the packages are updated and java is installed. These are defined in the role common
:
- name: Ensure kernel is at the latest version
yum: name=kernel state=latest
- name: Install latest Java 8
yum: name=java-1.8.0-openjdk.x86_64 state=latest
The deploy
role contains the rest, first we pull the jar into a directory in /opt
:
- name: Create skeleton directory
file: path=/opt/skeleton state=directory
- name: Download skeleton runnable jar
get_url:
url: http://nexus.edudoo.com/artifact/maven/content?g=com.edudoo.skeleton&a=backend&v=0.0.1-SNAPSHOT&r=snapshots
dest: /opt/skeleton/skeleton.jar
backup: yes
force: yes
Now the configuration management part:
- name: Ensure app is configured
template:
src: application.properties.j2
dest: /opt/skeleton/application.properties
- name: Ensure logging is configured
template:
src: logback-spring.xml.j2
dest: /opt/skeleton/logback-spring.xml
The Spring boot app is configured by the application.properties
file placed next to the runnable jar. With the template
above we can replace its content from environment to environment.
Let’s have a look at the template itself:
server.port={{skeleton_port}}
logging.config=/opt/skeleton/logback-spring.xml
logging.file=/opt/skeleton/skeleton.log
When we’re running our ansible script, skeleton_port
will be replaced by a provided value. We’re coming back to this later.
(The same applies for the log configuration.)
Finally the part about the service:
- name: Install skeleton systemd unit file
template: src=skeleton.service.j2 dest=/etc/systemd/system/skeleton.service
- name: Start skeleton
systemd: state=restarted name=skeleton daemon_reload=yes
The template actually doesn’t contain any variables at the moment (but could, eg. java args to dynamically control the memory consumption):
[Unit]
Description=Skeleton Service
[Service]
User=root
WorkingDirectory=/opt/skeleton/
ExecStart=/usr/bin/java -Xmx256m -jar skeleton.jar
SuccessExitStatus=143
TimeoutStopSec=10
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
What’s left to define is an inventory file (eg. dev-servers
) containing the environments:
[test]
11.22.33.44[prod]
11.22.33.45
11.22.33.46
and a playbook ( site.yml
) which holds together all the steps:
---
- hosts: test
remote_user: clouduser
roles:
- common
- deploy
vars:
- skeleton_port: 80
Note that we define a value for the variable skeleton_port
here which will be replaced in the template of the application.properties file.
So let’s add that to our Jenkinsfile
:
...
stage('Deploy to test'){
steps {
dir('deployment'){ //do this in the deployment directory!
echo 'Deploying to test'
sh 'ansible-playbook -i dev-servers site.yml'
}
}
}
...
Now we are ready, we only need to commit everything into a git repository, and let Jenkins know that the Jenkinsfile
can be pulled from there.
Configuring Jenkins
In Jenkins you should create a new Multibranch Pipeline
and on the configuration page the only thing to set is the source:

Save, run and enjoy!
The code is available at https://github.com/balazsmaria/skeleton