Groovy App Deployment

As I’ve mentioned before, I do a decent bit of Groovy development at work. Most of that code runs under the context of a SaaS application that I’ve mentioned before, so worrying about most things related to the execution environment isn’t relevant. Recently, though, I had a need to write a little app to sync items from one platform to another platform so that items that existed in our monitoring platform could be maintained in our CMDB platform automatically. I decided to do this in Groovy rather than something like Python because:

  1. I already had some Groovy code I could use.
  2. Any Groovy code I created for this app could be potentially re-used later within the context of the SaaS app.

Writing the code was no problem at all, but my question came to be how I would deploy my app. I knew it would run as a containerized CronJob on a Kubernetes server I have, but I had to do a little bit of research on what building that container looked like. In the Python world it’s usually just a matter of installing the necessary libraries from the requirements.txt file and then copying over the code. Groovy would be different since I wanted to compile my code rather than having it be interpreted at runtime.

My first step was to create a “fat jar”. Most of my Groovy code in the SaaS platform runs under a context where the libraries are already available, so even if I end up building a .jar file, that .jar only needs my code. For this app, it would be running in a container of its own, so I needed to include all of the libraries I leveraged. A so-called “fat jar” or “shadow jar” will accomplish this by bundling up all of the libraries alongside my own code. This in itself leverages a library. I use Gradle to manage my build, and it was important to note that this library goes into the plugins section of my build.gradle file, not the dependencies section.


plugins {
    id 'groovy'
    id 'application'
    id 'com.github.johnrengelman.shadow' version '7.1.2'
}

With this in place, instead of building with ./gradlew build, I use a new task: ./gradlew shadowJar. This makes the fat jar file with all of my dependencies in it. It’s also worth noting that the fat jar has its own definition in the build.gradle file; it doesn’t automatically use the definition that may be there for a jar.

shadowJar {
    archiveBaseName.set('LmCwConfig')
    archiveVersion.set('1.0.0')
    archiveClassifier.set('')  // Optional: Removes the "-all" classifier from the filename
    manifest {
        attributes 'Main-Class': 'com.domain.App'
    }
}

One dependency I didn’t need to worry about, though, was Groovy itself. Since this would ultimately run in a container, I could just use one of the Groovy images as my base. There are tons of Groovy and JDK combinations available, so it was easy enough for me to pick the one I needed in my Dockerfile. It was a much simpler file than I’m used to with Python because all I really need to do is define the base image, set the working directory, copy my fat jar, and then define the command (which is also not super important because K8s can give it something different.)

FROM groovy:4.0-jdk21-alpine
WORKDIR /usr/src/lmcwconfig
COPY app/build/libs/LmCwConfig-1.0.0.jar .
CMD ["java", "-jar", "/usr/src/lmcwconfig/LmCwConfig-1.0.0.jar"]

Simple! One problem I did run into coming from mostly deploying Python applications is that I would actually forget to rebuild the jar file when testing out my deployment. I would update my code, rebuild my image, deploy it to my test Kubernetes environment, and then see the exact same behavior because I never made a fresh jar. Ultimately, I ended up throwing together a quick build script I could use to take care of all of this for me so I would stop missing steps… and not need to keep typing so many commands:

#!/usr/bin/env bash

if [ -z "$1" ]; then
  echo "No version number passed as a CLI parameter!"
  exit 1
fi

ORIGINAL_VERSION="$1"
PATH_VERSION=$(echo "$ORIGINAL_VERSION" | tr '.' '_')

echo "--== Creating new LmCwConfig Docker Release: v${ORIGINAL_VERSION} ==--"

echo "*** Cleaning old build. ***"
./gradlew clean > /dev/null

echo "*** Creating new build. ***"
./gradlew shadowJar > /dev/null

echo "*** Creating Docker image. ***"
docker build -t "lmcwconfig:${ORIGINAL_VERSION}" --platform=linux/amd64 . > /dev/null

echo "*** Saving Docker image. ***"
docker save -o "${HOME}/Downloads/lmcwconfig_${PATH_VERSION}.tar" "lmcwconfig:${ORIGINAL_VERSION}" > /dev/null

if [ -e "${HOME}/Downloads/lmcwconfig_${PATH_VERSION}.tar.gz" ]; then
  echo "*** Deleting old archive. ***"
  rm "${HOME}/Downloads/lmcwconfig_${PATH_VERSION}.tar.gz" > /dev/null
fi

echo "*** Compressing image archive. ***"
gzip "${HOME}/Downloads/lmcwconfig_${PATH_VERSION}.tar" > /dev/null

echo "*** New image archive available at: ${HOME}/Downloads/lmcwconfig_${PATH_VERSION}.tar.gz ***"
echo "--== Complete ==--"

All this needs is a version number passed as a CLI parameter, and it takes care of the rest! I actually found this process of deploying something Java-based to be a bit more pleasant than doing Python builds once I got everything sorted out.