Build CI/CD for Spring Boot with Github Action

Tutorial for setting up CI/CD dockerized Spring Boot with Github Action

When i started my career as a software engineer, usually i build up CI/CD system with Jenkins on AWS EC2. For now, Github serves easy, free(almost) solution for CI/CD. So, let’s build CI/CD with Github Action

1. Dockerize Spring Boot Application

First, we need to dockerize Spring Boot App. Spring Boot contains embedeed WAS for running by itself. Although, Dockerizing gives consistency across your team. We use jib gradle plugin for dockerizing supported by google.

Add jib plugin, task to build.gradle.kts

plugins {
...

id("com.google.cloud.tools.jib") version "2.4.0"
...
}
// jib taskjib {
from {
image = "openjdk:11-jre-slim"
}
to {
image = "demo-app"
tags = setOf("latest", "${project.name}-${project.version}".toString())
}

container {
mainClass = "com.demo.DemoApplicationKt"
ports = listOf("8080")
volumes = listOf("/tmp")
user = "nobody:nogroup"
creationTime = "USE_CURRENT_TIMESTAMP"
}
}

Quite simple! isn’t it? you can customize docker setting with below reference.

2. Setting up static analysis tool

There’re many static analysis tool for helping developers to keep good quality of source code. e.g) SonarQube, Jacoco, etc. In this tutorial, we’re going to onl y set up jacoco for verifying test converge. You can refer jacoco gradle setting below.

plugins {
...

jacoco
...
}
repositories {

jacoco {
toolVersion = "0.8.5"
}
}
tasks.jacocoTestReport {
reports {
// turn off/on reports
html.isEnabled = true
xml.isEnabled = true
csv.isEnabled = false

}

finalizedBy("jacocoTestCoverageVerification")
}

tasks.jacocoTestCoverageVerification {
violationRules {
rule {

limit {
//
default 'COVEREDRATIO'
minimum = "0.30".toBigDecimal()
}
}

rule {
// enable rule
enabled = true

// check rule by classes
element = "CLASS"

// should satisfy branch coverage at least 90%
limit {
counter = "BRANCH"
value = "COVEREDRATIO"
minimum = "0.90".toBigDecimal()
}

// should satisfy line coverage at least 90%
limit {
counter = "LINE"
value = "COVEREDRATIO"
minimum = "0.80".toBigDecimal()
}

// limit line counts: 200
limit {
counter = "LINE"
value = "TOTALCOUNT"
maximum = "200".toBigDecimal()
}

// excludes some classes or package
excludes = listOf(
"*.test.*",
"*.Kotlin*"
)
}
}
}

val testCoverage by tasks.registering {
group = "verification"
description = "Runs the unit tests with coverage"

dependsOn(":test",
":jacocoTestReport",
":jacocoTestCoverageVerification")

tasks["jacocoTestReport"].mustRunAfter(tasks["test"])
tasks["jacocoTestCoverageVerification"].mustRunAfter(tasks["jacocoTestReport"])
}
}

3. Write Github action script for measuring coverage

In our teams development process, our team decided to follow code coverage rule. If code coverage rules are satisfied, feature branch will be merged into develop branch. To be sure all code following coverage rule before merge into develop, write this script on .github/test.yml .

# This is a basic workflow to help you get started with Actions

name: test

# Controls when the action will run. Triggers the workflow on push or pull request
# events but only for the develop branch
on:
pull_request:
branches: [ develop ]

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
timeout-minutes: 30

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 11

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Test coverage with Gradle
run: >
./gradlew testCoverage

On PR page, you can check all test are passed also test coverage rules.

1 check(test.yml) is passed

4. Write Github action script for deploying

For deploying develop branch on staging(for internal test) environment, we write deploy script on .github/deploy-staging.yml . In this tutorial, we will deploy docker image on ECR. Therefore, this github project need some AWS credential before pushing this script on devleop branch. On Github project’s Settings > Secrets page, we can set up AWS ID, Secret key things.

Secrets for AWS and other services

When deploy.yml file is pushed into develop branch, you can see how Github action is going on Actions tab. Once this script is done, Spring Boot docker image is uploaded to ECR then you can get this script’s execution result(success/fail).

# This is a basic workflow to help you get started with Actions

name: deploy-staging

# Controls when the action will run. Triggers the workflow on push or pull request
on:
push:
branches: [ develop ]

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
timeout-minutes: 30

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 11

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}

- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Build with Gradle
run: >
./gradlew asciidoctor jibDockerBuild

- name: Get current date
id: date
run: echo "::set-output name=date::$(date +'%Y%m%d-%H%M%S')"

- name: Push image to Amazon ECR
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: project name # projects name
IMAGE_TAG: staging-${{ steps.date.outputs.date }}
run: |
docker tag $ECR_REPOSITORY $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

- name: Slack notification
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,message,commit,author,action,eventName,ref,workflow,job,took # selectable (default: repo,message)
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} # required
if: always() # Pick up events even if the job fails or is canceled
deployed docker image with datetime tag

5. Integrating other AWS services

Our team deploy Spring Boot services on EKS. But based on your team’s situation, You can intergrate with other AWS services. i.e) ECS, ElasticBeansTalk, etc. For now, Our team use Github action for deploying docker image on ECR. We’re working on improving this CI/CD process for happy developer life 🧑‍💻. In next post, i will describe how to build CI/CD process for Spring boot MSA services deploy on EKS.