GitHub Actions to build your Java/Scala application, test and deploy it to Kubernetes cluster
By: Fedor Malyshkin
Tags: Java, Scala, Kubernetes, Gradle, Github Actions

Over the last couple of years, I’ve been working with several CI/CD servers like TeamCity and Jenkins. I find these products quite powerful because of their ability to run distributed builds (even now, 3 months later, Jenkins in Kubernetes amazes me :) ), high level of integration with different build systems and thesupport of a variety of programming languages, not mention of their flexible authentication/authorization systems.

However, when I work with my pet projects, I want some subset of features from Jenkins. Am I ready for any hosting or cloud Jenkins solution — NO! So, after I’ve got the next invitation from GitHub to use its “Actions” I decided to study what it is. A clone of “GitLab CI/CD”! No, of course, I don’t want to accuse anyone of stealing the concept — it’s only an idea that came in my mind when I’d read some documentation.

Can GitHub Actions be a replacement for Jenkins (or any other dedicated CI/CD solution) in a big project (with a number of services, complex logic of deployment, different languages, private repositories) — no! Can it be useful (at least for me) — definitely!

I read plenty of articles about GitHub Action on Medium and haven’t found something that describes my needs. However, as a result nowI know how to build and deploy React apps :).

What I want from the future pipeline:

cache build/compile dependencies (I don’t want to wait an additional time during each build when dependencies will be downloaded again and again and again like it wasn’t done in the previous build)

run existing tests on each push

further steps should be done only on for master branch:

build new docker image (with tags latest , and SHA of the last commit)

, and SHA of the last commit) rolling update to my Kubernetes cluster

Let’s start off by creating our pipeline definition. GitHub Action reuses GitLab and Jenkins approach — the usage of the repository itself for pipeline definition storage (of course if you don’t use something like Shared Libraries in Jenkins). So we will create a YAML file .github/workflows/ci-cd.yml , the name of the file doesn’t play a significant role it only should be meaningful.

I’ll present all parts of YML step-by-step with some comments (comments will be BELOW each part).

We start with global things:

name: ci-cd



on:

push:



jobs:

build-test-deploy:

runs-on: ubuntu-18.04

steps:

there we declare the name for our pipeline, conditions for its start (on each push in any branch). Also we define a single job ( build-test-deploy ) we are going to run: as our steps depend on previous step’s results I don’t see a reason for its separation. And of course, we specify a docker image where everything will be executed.

- uses: actions/checkout@v2

- name: set-up-jdk

uses: actions/setup-java@v1

with:

java-version: '11' # The JDK version to make available on the path.

java-package: jdk # (jre, jdk, or jdk+fx) - defaults to jdk

architecture: x64 # (x64 or x86) - defaults to x64

there we defined 2 steps: checkout of the current branch and specific JDK installation.

- name: cache-local-repository

uses: actions/cache@v2

with:

path: |

~/.m2/repository

~/.gradle/caches

~/.gradle/wrapper

key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}

restore-keys: |

${{ runner.os }}-gradle-

there we defined our build booster — cache for dependencies. On each successful build it is compressed and stored somewhere wtih the name of our key (can be used for different params like OS, build tool, JDS version, etc.) by an automatically created step.

- name: grant-execute-permission-for-gradlew

run: chmod +x gradlew

- name: build-with-gradle

run: ./gradlew build

- name: run-tests

run: ./gradlew scalatest

there we defined 3 simple steps for gradle: make its wrapper executable, build and test our code.

- name: login-to-docker-hub

if: ${{ github.ref == 'refs/heads/master' }}

uses: docker/login-action@v1

with:

username: ${{ secrets.DOCKER_USERNAME }}

password: ${{ secrets.DOCKER_PASSWORD }}

- name: set-up-docker-buildx

if: ${{ github.ref == 'refs/heads/master' }}

uses: docker/setup-buildx-action@v1

there we defined 2 steps (according to the fresh release of https://github.com/docker/build-push-action action): signing into DockerHub for pushing fresh image after its creation and setup for Docker’s Buildx.

It is the first step where we use the keyword if that checks our branch and executes the step only when we have a positive result in the condition (for us it means that we won’t run this step on any branch except master ).

In addition, it is the first place where we use GitHub Secrets which are essential for sensitive data, such as passwords or certificates. We can save these in GitHub as secrets and then use them in workflows as environment variables of the form ${{ secrets.XXXX }} .

- name: get-short-sha # https://github.community/t/substring-function/18401

if: ${{ github.ref == 'refs/heads/master' }}

run: |

id=$(echo ${{github.sha}} | cut -b-7)

echo "::set-output name=sha::sha-$id"

id: get-short-sha-id

there we use GitHub’s concept of outputs to generate a string with a tag value for our docker image (there is no easier way for this at the moment). Also you see I use id keyword that will help reference those outputs in other steps.

- name: build-and-push-docker-image

if: ${{ github.ref == 'refs/heads/master' }}

uses: docker/build-push-action@v2

with:

push: true

file: docker/Dockerfile

context: build/libs

tags: |

fedormalyshkin/conway-life-stream-server:latest

fedormalyshkin/conway-life-stream-server:${{steps.get-short-sha-id.outputs.sha}}

there we defined the step for image building and its publication on DockerHub

- name: set-k8s-context

if: ${{ github.ref == 'refs/heads/master' }}

uses: azure/k8s-set-context@v1

with:

method: kubeconfig

kubeconfig: ${{ secrets.KUBE_CONFIG }}

there we initialized configuration for our Kubernetes client ( kubectl ) that we are going to use in the last step. As you see we again use GitHub Secret record in which we keep the whole content of .kube/config file (with required context enabled by default).

- name: roll-update-k8s

if: ${{ github.ref == 'refs/heads/master' }}

run: kubectl set image deployment/stream-server-deployment conway-life-stream-server=fedormalyshkin/conway-life-stream-server:${{steps.get-short-sha-id.outputs.sha}}

the last step runs a rolling update in Kubernetes cluster by changing docker’s image name for the fresh one we created in the previous step.

The complete version is available there: https://github.com/fedor-malyshkin/conway-life-stream-server/blob/master/.github/workflows/ci-cd.yml

To sum up, GitHub extends its list of actions and they become more and more interesting in usage, whereas for me it already looks fine for some applications.

Wish you the best in your development automation :)