In this article, we're going to be looking at how to use Gitlab CI/CD to build, test and deploy a Spring Boot web application to a server instance. Gitlab has a generous offering of unlimited free private repositories and 2000 minutes of CI/CD runner.
We will be using Gitlab as our cloud Git repository in this article and we encourage you to create an account if you don't already have one.
For the purpose of this article, we will create a simple Spring Boot application using Spring Initializr. After downloading the generated project, the pom.xml should look like this:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Next, let's create a controller mapping for the index endpoint. The endpoint will simply return a hello world string concatenated with the current timestamp:
@Controller
public class IndexController {
@GetMapping("/")
@ResponseBody
public String index() {
return "Hello World " + LocalDateTime.now();
}
}
We can test the new endpoint by starting the application and visiting http://localhost:8080 from our browser, we should see the hello world string. Now that we have a working application, we need to create a new project on Gitlab, commit our code locally and push it to the remote repo using the project URL generated by Gitlab.
Let's execute the following commands from the project's root directory on our local machine:
git init
git add .
git commit -m "initial commit"
git remote add origin https://gitlab.com/SeunMatt/gitlab-ci-demo.git
git push origin master
Note that your project URL will be slightly different depending on the chosen username and project name.
For this tutorial, we will create a new DigitalOcean droplet, install JRE (Java Runtime Environment) and Nginx on it.
Nginx will serve as a reverse proxy server that will run on port 80 and forward HTTP traffic to our Spring Boot application that will be running on port 8080 internally.
We'll be using the Terraform configuration files from another article to provision the droplet.
In this section, we're going to see how we can configure Gitlab to auto-deploy our Spring Boot application whenever we push new changes.
We'll start by adding .gitlab-ci.yml to our project, then create a new CI/CD user on the server and finally define our pipeline with the necessary scripts.
In order for us to use Gitlab CI/CD, we need to add a .gitlab-ci.yml file to the root of our project directory.
The .gitlab-ci.yml is a simple plaintext file that defines the jobs we want to execute and what scripts we want to run in each job. Let's create a .gitlab-ci.yml file in our project's root directory with the following content:
stages:
- build
- deploy
The snippet above defines two stages that should run in the order they appear. We can see stages as a grouping of one or more jobs. The combination of different stages makes a pipeline.
The name of each stage should reflect the type of job(s) in it. For example, the build stage will have jobs that test and package our application as a jar file, whereas the deploy stage will contain jobs that copy the generated jar file to the server.
Let's define a job that will run the Maven package command and generate a single jar file. We will call the job maven-build:
maven-build:
image: maven:3-jdk-11
stage: build
script: "mvn package -B"
artifacts:
paths:
- target/gitlab-ci-demo.jar
In the snippet above, we configured the job to use a docker container that contains Maven version 3 and JDK 11.
The stage keyword indicates that this job belongs to the build stage, and the script keyword indicates what command to execute in the docker container once it's ready.
Another special keyword to take note of is artifacts. It instructs Gitlab CI/CD runner to preserve the end result of our script command so we can use it in subsequent jobs. In this case, we're keeping the final jar file so we can reference it in the coming sections.
The complete .gitlab-ci.yml file looks like this:
stages:
- build
- deploy
maven-build:
image: maven:3-jdk-11
stage: build
script: "mvn package -B"
artifacts:
paths:
- target/gitlab-ci-demo.jar
To see this in action, let's commit and push our changes to Gitlab. Gitlab will auto-detect the .gitlab-ci.yml file and trigger Gitlab CI/CD runner.
We can monitor the progress of the running jobs on Gitlab by going to the CI/CD menu >> Jobs:
We can click on the running button to see the live output from the different commands. Once the build is complete, the running button will change to green and the text will now be passed:
For our deployment process, we will first create a CI/CD user on our server, then use the scp command to copy the jar file from Gitlab to our server.
Once the jar file is on our server, we will ssh to the server, move the jar file to the appropriate directory and restart the application service using systemctl.
Let's SSH into the server instance we provisioned earlier and execute the following commands to create and configure the CI/CD user:
adduser --quiet --shell $SHELL --disabled-password --gecos 'GitlabCI User' gitlab-ci
usermod -a -G sudo gitlab-ci
echo 'gitlab-ci:changemepassword' | chpasswd
printf 'Match User gitlab-ci\n\tPasswordAuthentication yes\n' >> /etc/ssh/sshd_config
systemctl restart sshd
echo 'gitlab-ci ALL=(ALL) NOPASSWD: /bin/mv, NOPASSWD: /usr/bin/systemctl, NOPASSWD: /bin/cp' | sudo EDITOR='tee -a' visudo
First, we created a new user gitlab-ci and then we change the user's password to changemepassword. Then we updated the ssh_config file to allow our new user to authenticate via password and we restart the sshd service.
The last command is very important, it disables prompting for sudo password when we execute any of the following commands: mv, systemtctl and cp.
We need to set this because we'll be using the user in an automated process devoid of human intervention to supply a sudo password.
We need to add gitlab-ci's password as an environment variable on Gitlab so that we can reference it, securely, in our .gitlab-ci.yml file. To do that, we need to login to our Gitlab account >> Settings >> CI/CD >> Variables.
Once we expand the Variables section, we will see the button to add a new variable, we should click on it to add the variable:
Now that we have created our user and added the user's password to CI/CD variable on Gitlab, let's update our .gitlab-ci.yml file with the deploy job:
deploy-master:
before_script:
- apt-get update -qq && apt-get install -y -qq sshpass
stage: deploy
script:
- sshpass -V
- export SSHPASS=$CI_USER_PASS
- sshpass -e scp -o StrictHostKeyChecking=no target/gitlab-ci-demo.jar [email protected]:/home/gitlab-ci
- sshpass -e ssh -tt -o StrictHostKeyChecking=no [email protected] sudo mv /home/gitlab-ci/gitlab-ci-demo.jar /opt/java/webapps
- sshpass -e ssh -tt -o StrictHostKeyChecking=no [email protected] sudo systemctl restart gitlab-ci-demo.service
In this job definition, we use before_script to specify what command should be executed on the server before executing other commands in the script tag. Additionally, in the script section, we exported the password for the CI user into the current environment as SSHPASS.
By so doing, sshpass will auto-respond to password prompts from ssh and scp commands with the value of the environment variable SSHPASS.
The SSH commands we executed are pretty straightforward, we first use scp to copy the file to the server and then we move the file to the right directory and finally restarted the service using systemtctl.
The complete .gitlab-ci.yml fill will have the following contents:
stages:
- build
- deploy
maven-build:
image: maven:3-jdk-11
stage: build
script: "mvn package -B"
artifacts:
paths:
- target/gitlab-ci-demo.jar
deploy-master:
before_script:
- apt-get update -qq && apt-get install -y -qq sshpass
stage: deploy
script:
- sshpass -V
- export SSHPASS=$CI_USER_PASS
- sshpass -e scp -o StrictHostKeyChecking=no target/gitlab-ci-demo.jar [email protected]:/home/gitlab-ci
- sshpass -e ssh -tt -o StrictHostKeyChecking=no [email protected] sudo mv /home/gitlab-ci/gitlab-ci-demo.jar /opt/java/webapps
- sshpass -e ssh -tt -o StrictHostKeyChecking=no [email protected] sudo systemctl restart gitlab-ci-demo.service
Let's commit our changes and push. As usual, Gitlab will start the runner automatically and we can monitor the progress of the pipeline. Once the pipeline has passed, we can navigate to our domain/IP address and see our web application running.
With what we've set up so far, every time we push to our git repository, Gitlab will build and deploy. What if we want Gitlab to only deploy if we push to master? We can easily achieve that by adding a rules tag to the deploy-master:
deploy-master:
rules:
- if: '$CI_COMMIT_BRANCH =~ /^master$/'
before_script:
- apt-get update -qq && apt-get install -y -qq sshpass
stage: deploy
script:
- sshpass -V
- export SSHPASS=$CI_USER_PASS
- sshpass -e scp -o StrictHostKeyChecking=no target/gitlab-ci-demo.jar [email protected]:/home/gitlab-ci
- sshpass -e ssh -tt -o StrictHostKeyChecking=no [email protected] sudo mv /home/gitlab-ci/gitlab-ci-demo.jar /opt/java/webapps
- sshpass -e ssh -tt -o StrictHostKeyChecking=no [email protected] sudo systemctl restart gitlab-ci-demo.service
This way, if we push to any other branch other than master, only the maven-build job will run. However, if we merge or push to the master branch both maven-build and deploy-master will run.
In this article, we've looked at the basics of setting up a CI/CD pipeline on Gitlab and in the process we deployed a simple Spring Boot application.
Gitlab CI/CD has more capabilities and features than those mentioned, I encourage you to read the official documentation to know more. The complete source code is available on Gitlab.
You can follow me on Twitter to see more of my articles and tech musings https://twitter.com/SeunMatt2
Also published on: https://smattme.com/blog/technology/auto-deploy-spring-boot-app-using-gitlab-ci-cd