Spring Boot CI/CD on Kubernetes using Terraform, Ansible and GitHub: Part 10

Martin Hodges
7 min readNov 7, 2023

--

Use Terraform, Ansible and GitHub Actions to automate running your Spring Boot application on Kubernetes: Part 10

This is part of a series of articles that creates a project to implement automated provisioning of cloud infrastructure in order to deploy a Spring Boot application to a Kubernetes cluster using CI/CD. In this part we use GitHub Actions to rebuild our Spring Boot Application when we update it.

Follow from the start — Introduction

In this article we look at how you can use GitHub Actions to automate the building of your Docker image with the result that whenever you push new changes to your GitHub repository, your Docker image will be built and pushed to your Docker Hub account, automatically.

You can find the files described in this article in the .github folder of this repository:
https://github.com/MartinHodges/Quick-Queue-Application/tree/part10

GitHub Actions

First a quick introduction to GitHub Actions in case you are not familiar with them.

GitHub (and other suppliers) provide you with the ability to run automation scripts on their virtual machines. Whenever the script is triggered, GitHub spins up a Virtual Machine (VM), runs your script and then closes down the VM.

You define your scripts in a hidden folder in the root of your project repository under .github/workflows. The script is written as a yaml file. By including the scripts as part of your code base, it is automatically under version control.

Scripts can be triggered automatically on GitHub events (like pushing a new version to a given branch or branches) or it can be triggered manually. This latter case is useful should the workflow fail for some reason and needs to be re-run without a change to the code.

Our CI Actions

In the case of our Spring Boot application, we need our GitHub Actions to do the following:

  1. Trigger when pushing to the main branch (or manually)
  2. Start an Ubuntu VM
  3. Install Java 17
  4. Set up Gradle
  5. Build our application using Gradle
  6. Set up Docker
  7. Build our Docker image using Docker
  8. Login to our Docker Hub account
  9. Push our image to Docker Hub

These 9 steps form our CI pipeline. Luckily GitHub allows common Actions to be scripted by others and then you can just use their Actions in the same way as you would use a library. We will see this in action in the next section.

Creating our CI Actions

In your Spring Boot Application, create the hidden folders .github/workflows. In here create your script in a file called build.yml. The name is not set and you can choose another name if you wish.

name: 'Build'
run-name: 'Build qqapp (build version: ${{github.run_number}})'

# Controls when the action will run.
on:
# Triggers the workflow on push or pull request events but only for the main branch
push:
branches: [main]
# # Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains multiple jobs
build_core:
# The type of runner that the job will run on
runs-on: ubuntu-latest

steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- name: Checkout project sources
uses: actions/checkout@v3
- name: Install Java
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 17

- name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Run build with Gradle Wrapper
run: ./gradlew build -x test

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Push image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/qqapp:${{github.run_number}}, ${{ secrets.DOCKERHUB_USERNAME }}/qqapp:latest

As I normally do, I will break this down into sections to describe each part.

name: 'Build'
run-name: 'Build qqapp (build version: ${{github.run_number}})'

This names the Action (Build) along with the name of this instance when it runs (Build qqapp (build version: ${{github.run_number}})). You will notice that it uses github.run_number which is an internal counter that GitHub maintains. The run number becomes useful as a build number and we use this elsewhere as an image version.

# Controls when the action will run.
on:
# Triggers the workflow on push or pull request events but only for the main branch
push:
branches: [main]
# # Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

As the comment shows, this section determines when the build will run. In this case there are two triggers:

  • A push to the main branch (branches: [main])
  • Manually from the GitHub website (workflow_dispatch)

You can trigger on multiple branches. Also, when manually triggering using a workflow dispatch, you can prompt the user for values that you can use in your Action. In this case we do not need any inputs from the user.

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
build_app:
# The type of runner that the job will run on
runs-on: ubuntu-latest

This section is where the main work of the Action is defined, starting with the definition of your jobs. In this example, we are starting with a single job that we are calling build_app. Later we will add other jobs.

You may remember that the first thing that needs to be done is that you need to tell GitHub what sort of VM should be used to run your Action. In this case it is the latest version of Ubuntu.

    steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- name: Checkout project sources
uses: actions/checkout@v3

Now we define the steps in our job, starting with checking out the source code for the project. To do this we use the actions/checkout@v3 action.

      - name: Install Java
uses: actions/setup-java@v3
with:
distribution: temurin
java-version: 17

This step installs Java using the actions/setup-java@3 action. This installs version 17.

      - name: Setup Gradle
uses: gradle/gradle-build-action@v2
- name: Run build with Gradle Wrapper
run: ./gradlew build -x test

These two steps load Gradle and then use the project’s gradlew wrapper to build the application (in this case skipping the unit tests with -x test). In a production environment, you should not skip unit tests.

      - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2

By using the docker/setup-buildx-action@v2 action, this section builds the Docker image on the VM.

     - name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Push image
uses: docker/build-push-action@v4
with:
context: .
file: ./Dockerfile
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/qqapp:${{github.run_number}}, ${{ secrets.DOCKERHUB_USERNAME }}/qqapp:latest

Once the Docker image has been built, docker/login-action@v3 logs in the VM into your Docker Hub account using GitHub secrets. More about them in a moment.

After logging in to your Docker Hub account, docker/build-push-action@v4 pushes the image that was created in the earlier steps into your repository. Note that there are two tags created:

  • ${{ secrets.DOCKERHUB_USERNAME }}/qqapp:${{github.run_number}}
  • ${{ secrets.DOCKERHUB_USERNAME }}/qqapp:latest

The first names the version after the run number we mentioned earlier. The second creates a second tag marked as the latest. If your Kubernetes deployment has a pull rule of Always, the latest will always be pulled. Alternatively you can select a particular version.

GitHub Secrets

Adding credentials to any code repository should never be done as it means anyone with access to the code has access to your credentials. Your Actions though, require access to your credentials so they can access your Docker Hub account.

GitHub allows you to store secrets that your Actions can access. The secrets are held against each code repository and are not duplicated if the repository is forked.

Within your application repository, head to the Settings tab. Scroll down the menu in the lefthand panel and select Secrets and variables. From the menu that appears select Actions. Choose the Secrets tab and then click New repository secret button.

In the dialog that appears, enter the secret name and then add the secret. You need to secrets:

  • DOCKERHUB_USERNAME — This is your Docker Hub username
  • DOCKERHUB_TOKEN — This is your Docker Hub access token

To create a Docker Hub access token, log in to your account. Go to your Account Settings and then select Security. Click the New Access Token button.

Choose a name for your token, eg: GitHub access to qqapp, and select Read & Write permissions. Click Generate and copy the resulting key into the DOCKERHUB_TOKEN. You can only do this once. After this you will not be able to see the token.

Testing

You have now completed your CI portion of your automation. Make a change to your Spring Boot application and push it to your main branch.

You should now see the Action automatically start. Once it is completed, look into your Docker Hub account and you should see two tags associated with your repository, created and pushed by your GitHub Action.

If your Kubernetes cluster is up, you should now be able to apply your deployment manifest. If you have tagged the latest version in your deployment, this should load and run your new version.

Summary

In this article we created a Continuous Integration (CI) pipline using GitHub Actions. The Action we built compiles the application, builds the Docker image and pushes it to Docker Hub both as a versioned and a latest version image.

Next we will automate the Continuous Deployment (CD) pipeline.

Series Introduction

Previous — Accessing a Spring Boot application using a Kubernetes Service

Next — Building your Continuous Deployment (CD) pipeline

--

--