CI/CD Java apps securely to Azure Kubernetes Service with GitHub Action — Part 1

Jay Lee
9 min readMay 24, 2023

The Cloud Native buzz has undoubtedly created waves of excitement and anticipation, but after all the highs and lows, I like at least one thing which remains persistently: the remarkable adoption of CI/CD as a fundamental practice within enterprises. Nowadays, CI/CD is a general topic in modern application development, irrespective of whether developers are building microservices. For Java developers, this leads to discussions on deploying Java apps on Kubernetes, the leading container platform today. Supporting this trend is a report from the VMWare Tanzu team in their “State of Spring 2022”; 82% of surveyed developers deploy their Spring Boot applications on Kubernetes.

The purpose of the article is to show you how to get started building CI/CD pipelines for Java apps on Azure Kubernetes Service using GitHub Action. Starting with CI, I will cover the basics of securing software delivery. Before delving into the topic, Let’s have some fun playing with the hottest buzz in today’s IT industry, Chat GPT. Honestly, I’m always surprised by the result, but it’s got many parts simply wrong that you wouldn’t know if you were not paying attention to it.

ChatGPT is impressive

This article is not written by Chat GPT btw. :D

NOTE: Sample repository is at https://github.com/eggboy/cicd-java

Build and test with Maven in GitHub Action

Building workflow on GitHub doesn’t necessarily mean that we always have to start from scratch, as GitHub Action provides various templates to get started with any programming language.

Default GitHub Action templates

As we see from the GitHub Actions page above, it already suggests a few templates suitable for the Spring Boot application. Let’s start with “Java with Maven”. GitHub template uses Java 11 and Temurin by default which I change to Java 17 and Microsoft Build of OpenJDK. Make two quick changes and commit them, then the workflow will be automatically triggered. One thing to note here is cache part of actions/setup-java@v3 that uses actions/cache under the hood to cache maven dependencies.


name: CI/CD Spring Boot application

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
build:

runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'microsoft'
cache: 'maven'

- name: Build with Maven
run: mvn -B package --file pom.xml

Basic Scanning with SpotBug, SBOM, and Trivy

There could be tons of things to do in the CI process, like code review, code format, check style, unit test, etc., for any enterprise, but I will save it for another day. For the sake of simplicity, the sample pipeline will be lean to do some basic unit tests, static code analysis with SpotBugs, generate SBOM then scan the dependencies for security vulnerabilities.

If you’re unfamiliar with SpotBugs, it is a static analysis to look for bugs in Java code, and it itself is built using Gradle and GitHub action. It is a bit ironic that SpotBugs itself uses Sonarqube for the scanning. Still, SpotBugs, together with OWASP Find Security Bugs, can be very handy since they can be easily integrated with Maven. Below is a snippet of the pom.xml at this stage.

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>3.12.1</version>
</plugin>
</plugins>
</build>

<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-project-info-reports-plugin</artifactId>
<version>3.4.2</version>
<reportSets>
<reportSet>
<reports>
<report>index</report>
</reports>
</reportSet>
</reportSets>
</plugin>
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>${spotbugs-maven-plugin.version}</version>
<configuration>
<xmlOutput>true</xmlOutput>
<xmlOutputDirectory>target/site</xmlOutputDirectory>
<plugins>
<plugin>
<groupId>com.h3xstream.findsecbugs</groupId>
<artifactId>findsecbugs-plugin</artifactId>
<version>1.12.0</version>
</plugin>
</plugins>
</configuration>
</plugin>
</plugins>
</reporting>

mvn site generates SpotBugs HTML pages ./target/site that show the result of static code analysis.

Bugs are reported by category

Generating a Software Bill of Materials (SBOM) has become an increasingly important part of CI/CD practices these days. I can’t help but mention Cloud Native Buildpack at this point which might seem irrelevant. But CNB embeds SBOM generation in the container-building process by leveraging Syft buildpack under the hood, and this is one of the reasons why I love CNB. Anyway, While various tools like Syft, Trivy, sbom-tool, etc., can be used to generate SBOMs from different sources, such as containers and package managers, I have chosen CycloneDX maven plugin, which generates SBOMs in the CycloneDX format.

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>3.12.1</version>
</plugin>
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>2.7.5</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>makeAggregateBom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

As seen above, SBOM is created withmvn packages that generates bom.json file under target folder.

$ cat target/bom.json
{
"bomFormat" : "CycloneDX",
"specVersion" : "1.4",
"serialNumber" : "urn:uuid:f0cf1144-897c-412a-9aa4-6449286652ab",
"version" : 1,
"metadata" : {
"timestamp" : "2023-02-20T12:13:35Z",
"tools" : [
{
"vendor" : "OWASP Foundation",
"name" : "CycloneDX Maven plugin makeAggregateBom",
"version" : "2.7.5",
"hashes" : [
{
"alg" : "MD5",
"content" : "11cd12fd5f8b60a961c385ef2be00f52"
},
{
"alg" : "SHA-1",
"content" : "3576a6392958d12709ba803922ca8ee0c7a233ed"
},
{
"alg" : "SHA-256",
"content" : "802bf55759d1a44b12e01879614bae19c096302d881d1b52bf36e6051b9dc1ef"
},
...

SBOM is ready to be scanned, and this is where Trivy comes to shine. Trivy is awesome that can scan not only the SBOM of Java applications but also the container image. I see the growing usage of Trivy everywhere, and I’d like to send my kudos to the Aqua Security team for this awesome open-source project. Here, I’m only two steps away from finishing the CI part, 1. install Trivy and 2. use Trivy to scan the SBOM. The action below should be simple enough to understand.

name: CI/CD Spring Boot to Azure Kubernetes Service

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:
test:
name: Unit Test and SpotBugs
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'microsoft'
cache: 'maven'
## Unit test and SBOM generation is carried out in 'mvn package', and SpotBugs report is generated in 'mvn site'
- name: Build with Maven
run: mvn -B clean package site
- name: Upload SBOM(Cyclonedx)
uses: actions/upload-artifact@v3
with:
name: bom.json
path: './target/bom.json'
- name: Upload SpotBugs Report
uses: actions/upload-artifact@v3
with:
name: spotbugs-site
path: './target/site/'
scan:
name: Scan dependencies with Trivy
needs: test
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- name: Install latest Trivy CLI
run: |
wget https://github.com/aquasecurity/trivy/releases/download/v0.37.3/trivy_0.37.3_Linux-64bit.deb
sudo dpkg -i trivy_0.37.3_Linux-64bit.deb
- uses: actions/download-artifact@v3
with:
name: bom.json
- name: Run Trivy with SBOM
run: trivy sbom ./bom.json

trivy sbom shows the identified vulnerabilities in the dependencies.

Total of 7 Vulnerabilities are found

The GitHub Action page shows two artifacts, SBOM(bom.json) and SpotBugs sites(spotbugs-site).

GitHub Action job result

Building Container Image with Cloud Native Buildpack

NOTE: I will use GitHub-hosted runners provided by GitHub actions. It’s a GitHub agent running on Virtual Machine on Azure, so CNB works just fine without any hassles, unlike running it on Kubernetes. In case you’re using GitHub self-hosted runner, I’m writing a new article, “Building Java Containers natively with GitHub self-hosted runner on Kubernetes” that will show different options to build container images with runners on Kubernetes. Stay tuned.

Building a container image is a two-step process, first to create an image using CNB and then push the container image to ACR. I’m going to tweak a bit so that I will log in to ACR first to generate the necessary docker config files. Then I will use CNB to push the container image to ACR directly.

In my previous article, “Safely access Azure Kubernetes Service in GitHub Action with AAD Federated Identity”, I covered the best practice for using Azure CLI in the GitHub Action using AAD Federated Identity. Here is the snippet of the pipeline. az acr login --expose-token generates Access Token and returns the login server URL for Azure Container Registry. docker login with a special username 00000000–0000–0000–0000–000000000000 provides a convenient way to log in to ACR using CLI. JWT Access Token generated by ACR has 3 hrs of lifecycle which is more secure than using basic credentials of username and password.

name: CI/CD Spring Boot to Azure Kubernetes Service

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

permissions:
id-token: write
contents: read

jobs:
...
container:
name: Build container with CNB and push to ACR
needs: scan
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: 'Az CLI Login'
uses: azure/login@v1
with:
client-id: ${{ secrets.CLIENT_ID }}
tenant-id: ${{ secrets.TENANT_ID }}
subscription-id: ${{ secrets.SUBSCRIPTION_ID }}

- name: ACR Login with AZ CLI
run: |
ACR_JSON=$(az acr login --name acrjay --expose-token)
TOKEN=$(echo $ACR_JSON | jq -r .accessToken)
LOGINSERVER=$(echo $ACR_JSON | jq -r .loginServer)
echo "LOGINSERVER=$LOGINSERVER" >> $GITHUB_ENV

docker login ${LOGINSERVER} --username 00000000-0000-0000-0000-000000000000 --password-stdin <<< $TOKEN

With Docker credentials in place, it’s time to incorporate CNB into the pipeline. The essential steps are 1. Install the latest CNB version 2. Configure the image name and tags by leveraging the repository name and project version in Maven 3. Runpack build to create the image. If you want details of utilizing CNB within GitHub Action, kindly refer to my previous article titled — “Creating Spring Boot container in a minute with Cloud Native Buildpacks for Azure Container Platform”.

  container:
name: Build container with CNB and push to ACR
needs: scan
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: 'Az CLI Login'
uses: azure/login@v1
with:
client-id: ${{ secrets.CLIENT_ID }}
tenant-id: ${{ secrets.TENANT_ID }}
subscription-id: ${{ secrets.SUBSCRIPTION_ID }}

- name: ACR Login with AZ CLI
run: |
ACR_JSON=$(az acr login --name acrjay --expose-token)
TOKEN=$(echo $ACR_JSON | jq -r .accessToken)
LOGINSERVER=$(echo $ACR_JSON | jq -r .loginServer)
echo "LOGINSERVER=$LOGINSERVER" >> $GITHUB_ENV

docker login ${LOGINSERVER} --username 00000000-0000-0000-0000-000000000000 --password-stdin <<< $TOKEN

- name: Install pack CLIs including pack and yq
uses: buildpacks/github-actions/setup-pack@v5.0.0
with:
pack-version: '0.29.0'

- name: Set the image name and version
run: |
VERSION=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout)
REPO_NAME=${{ github.event.repository.name }}
echo "IMAGE=$REPO_NAME:$VERSION" >> $GITHUB_ENV

- name: Pack build
run: |
pack build ${LOGINSERVER}/${IMAGE} --builder paketobuildpacks/builder:base --buildpack paketo-buildpacks/java-azure --env BP_JVM_VERSION=17 --publish

Deploying Container Image to AKS

As seen previously, the basics to securely access AKS are put together in my old article — “Safely access Azure Kubernetes Service in GitHub Action with AAD Federated Identity”.

  deployment:
name: Deploy image to AKS
needs: container
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3

- name: 'Az CLI Login'
uses: azure/login@v1
with:
client-id: ${{ secrets.CLIENT_ID }}
tenant-id: ${{ secrets.TENANT_ID }}
subscription-id: ${{ secrets.SUBSCRIPTION_ID }}

- uses: azure/setup-kubectl@v3
name: Setup kubectl

- name: Setup kubelogin
uses: azure/use-kubelogin@v1
with:
kubelogin-version: 'v0.0.26'

- name: Set AKS context
id: set-context
uses: azure/aks-set-context@v3
with:
resource-group: 'sandbox-rg'
cluster-name: 'rbac-cluster'
admin: 'false'
use-kubelogin: 'true'

However, one thing is missing from my old article, which is the role assignment. The service Principal I’m using with Federated Identity should have permission to access Kubernetes to create Deployment and Service. Create a ClusterRole github-action-role, and bind it to the service principal. Make sure to use Object Id, not Client Id.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: github-action-role
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get","create"]
- apiGroups: [""]
resources: ["services"]
verbs: ["get","create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: github-action-role
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: github-action-role
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: ${{ Object Id of Service Principal }}

In the final step, I will use Kustomize to deploy my app to AKS.

      - name: Deploy image using Kustomize
env:
IMAGE: ${{needs.container.outputs.IMAGE}}
LOGINSERVER: ${{needs.container.outputs.LOGINSERVER}}
run: |
curl -s "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" | bash
cd k8s
kustomize edit set image cicd-java=${LOGINSERVER}/${IMAGE}
kustomize build . | kubectl apply -f -

The final GitHub Action workflow is at https://github.com/eggboy/cicd-java/blob/main/.github/workflows/maven.yml

Wrapping Up

In the first part of CI/CD Java Apps on AKS, I focus on Continuous Integration. I look into the fundamentals of securing the code by incorporating SpotBugs and Trivy for static code and dependency scanning. Additionally, the pipeline generates an SBOM (Software Bill of Materials) for further scanning and analysis. The second part of the article will focus on Continuous delivery on Kubernetes leveraging Load Test and CD tools. Stay tuned!

If you liked my article, please leave a few claps or start following me. You can get notified whenever I publish something new. Let’s stay connected on Linkedin, too! Thank you so much for reading!

--

--

Jay Lee

Cloud Native Enthusiast. Java, Spring, Python, Golang, Kubernetes.