Contents

Deploy your first Java app on Kubernetes using Skaffold on Rancher Desktop

Skaffold is a tool to automate your Kubernetes development experience. I’ve used it extensively for the past year for rapid code prototyping. It has native support for a lot of use cases, but getting used to it takes some finetuning beyond the usual getting started. In this article, I will show you how to configure Skaffold to quickstart a Java application on Rancher Desktop.

What is Skaffold

Skaffold is a command line tool that streamlines the development workflow for Kubernetes applications. It provides automation for building, testing, and deploying containers, as well as managing application build and providing real-time feedback. This makes it easier for developers to iterate on their code and deploy changes to Kubernetes, enhancing the developer experience.

Skaffold is useful in any situation where developers want to quickly deploy changes while they code them, and test them in a real Kubernetes environment.

Skaffold lifecycle is as follows:

It basically monitors your filesystem for any changes, compiles your code, builds your docker image, renders your manifests and deploys everything to your cluster of choice, and you’re ready to test!

What is Rancher

Rancher is an enterprise grade Kubernetes distribution. It has been acquired by SUSE and offers additional components and packaging on top of Kubernetes vanilla.

It also offers a desktop developer experience through Rancher Desktop, a tool which installs a kubernetes cluster on your laptop using a trimmed down k3s distribution. This is what I will use in this article.

Note
I love Rancher Desktop because it’s straightforward to install and use, and comes with everything you need to start playing with Kubernetes. Nonetheless, Skaffold works as well with any local or remotely hosted Kubernetes cluster, so feel free to use whichever you like. Just keep in mind that you’ll also probably need docker, kubectl and helm CLIs installed by your own.

Getting Skaffold to run alongside Rancher Desktop requires to solve some peculiarities. Let’s put all that at test with a Java application.

Your First Skaffold sample

Prerequisites

Note
I use Ubuntu 22.04 WSL2 on Windows for this tutorial. Results on a genuine Ubuntu 22.04 should behave the same. If you are using any other Linux distro, adapt your commands accordingly.

Let’s first install Rancher Desktop by following the official installation instructions. Rancher Desktop installs everything needed to get you started on Kubernetes, namely a Kubernetes Cluster, plus several CLI tools to interact with it: docker or nerdctl, kubectl and helm. On WSL2, you can mount the Docker Socket and the Kubernetes context directly in your WSL environment by going into Preferences -> WSL --> Integration and selecting your WSL distro. The CLIs will be injected in the WSL path at startup, so you will be all set.

Beware!
Skaffold plays well with Docker; not that well with containerd. Be sure to setup your Rancher Desktop cluster tu use dockerd in the parameters.

Now that the Rancher Desktop environment is installed, we need to install Skaffold in our WSL2 Ubuntu distro, using the following command:

curl -Lo skaffold https://storage.googleapis.com/skaffold/releases/latest/skaffold-linux-amd64 && \
sudo install skaffold /usr/local/bin/

Finally, we need a Java 17 SDK or greater. Trust your Distro’s package manager on this one ;)

Bootstrap a sample Java app

A simple way to have a neat Java sample app is to use Spring Initializr. Spring is the de-facto standard Web Framework for Java, and Spring Initializr is the quickest way to bootstrap a new Spring web application.

Spring Initializr is available as a website or as a REST API that you can comfortably call from your terminal of choice using cURL. Let’s do that!

curl -vs https://start.spring.io/starter.zip \
  -d type=maven-project \
  -d language=java \
  -d dependencies=web,actuator \
  -d artifactId=hello \
  -d name=hello \
  | jar -xv
More about Spring Initializr
Spring Initializr is a convenient website allowing you to bootstrap a new Spring project within seconds. You can add dependencies to suit your needs, and all come bundled with a convenient Maven wrapper to spare you the hassle of installing Maven by your own

To test your code locally, you can use the convenient Maven wrapper provided by Spring.

First, we need to make mvnw executable (this will be needed later for Skaffold, too):

chmod +x mvnw
Reminder
The minimum required version of Java is JDK 17

Then, still in our Spring app directory, we can run the following command:

mvnw spring-boot:run

This will launch our Spring Boot app and expose an actuator status at http://localhost:8080/actuator/health.

You can check that everything went fine by browsing to this URL using your favorite browser (which should be Firefox, btw ;) )

status: "UP"

Create a container image for the Java application

To deploy our Java application into Kubernetes, we need to package it as a Container Image.

For this, we can create a Dockerfile in the root directory of our app, that will get the jar file from the target folder, and launch it. There are plenty of base images we can choose from, I based this sample on Google’s Distroless images, which are lightweight images, but feel free to use whatever you prefer.

FROM gcr.io/distroless/java17-debian11
COPY ./target/*.jar /app.jar
EXPOSE 8080
CMD ["/app.jar"]

We can then build it using the docker command supplied by Rancher. This will send the Docker context to the Rancher distro, through the Docker socket.

docker buildx build . -t hello

Our image will then pop up into the list of Rancher available images:

See? It's here ;)

It’s now time to build a kubernetes deployment for our image; let’s use Helm for that.

Create a simple Helm chart to deploy your app

Helm is the de facto standard package manager for Kubernetes. It is a convenient templating language to help you get yaml manifests under control. We will use Helm CLI to create a default template and customize it to deploy our java application

First things first, let’s use Helm CLI to create a new Helm chart:

helm create hello

We then need to edit the file values.yaml in the hellofolder as such:

image:
  # Change the default nginx value to something meaningful
  repository: hello
...

In the directory hello/templates, we have to edit the file deployment.yaml to change the port and urls of the probes:

          ports:
            - name: http
              containerPort: 8080
              protocol: TCP
          livenessProbe:
            httpGet:
              path: /actuator/health
              port: http
          readinessProbe:
            httpGet:
              path: /actuator/health
              port: http
          resources:
            ...

We are now ready to deploy the chart on our Rancher cluster:

helm install hello ./hello --debug --atomic --timeout 1m

The Helm install command provides some NOTES to help you test the release. Let’s use them:

  export POD_NAME=$(kubectl get pods  -l "app.kubernetes.io/name=hello,app.kubernetes.io/instance=hello" -o jsonpath="{.items[0].metadata.name}") && \
  export CONTAINER_PORT=$(kubectl get pod  $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") && \
  kubectl  port-forward $POD_NAME 8080:$CONTAINER_PORT 

The last command port-forward binds a local port to the remote deployed service. It runs in the foreground.

You can now browse to http://localhost:8080/actuator/health and should see the actuator endpoint in action.

Let’s uninstall our app once it has been validated to make room for Skaffold:

helm uninstall hello

Now that we have a working Kubernetes application, let’s tie this up together using skaffold.

Tie it up together with Skaffold configuration

Base Dockerfile & Helm config

We have a working kubernetes application, but this cost us a lot of boilerplate and command line, isn’t it ? As we saw earlier, Skaffold will automate all this process for us.

To get started with Skaffold, we must run skaffold init which will search through the files of the current folder for build configurations and kubernetes manifests. Once those are found, Skaffold will create a skaffold.yaml file, describing how your project will be built and deployed.

This is where things get tricky : Skaffold defaults work well with very simple cases, but are quite unusable on Rancher with a Java application. We are going to tune them so that our Java app deploys correctly.

Let’s get to it by typing the following:

skaffold init

Skaffold will ask you how to build two (sic) images, one called busybox and one called hello. For busybox, we select none:

? Choose the builder to build image busybox  [Use arrows to move, type to filter]
  Buildpacks (pom.xml)
  Docker (Dockerfile)
> None (image not built from these sources)

For our hello image, we will be using the created Dockerfile:

? Choose the builder to build image hello  [Use arrows to move, type to filter]
  Buildpacks (pom.xml)
> Docker (Dockerfile)
  None (image not built from these sources)
Note
You may wonder why Skaffold wants to build a busybox image. It’s because this one is used for testing purposes in the default helm chart. Just tell Skaffold that this image is not built by the current sources.

By default, Skaffold will search for build files, and when faced with pom.xml, it will ask you if you want your source to be build by Buildpacks (which is the default Skaffold builder fo Java code). As we use a Dockerfile, this has no effect for our project; answer whatever you want.

? Which builders would you like to create kubernetes resources for?  [Use arrows to move, space to select, <right> to all, <left> to none, type to filter]
> [x]  Buildpacks (pom.xml)

This will generate a basic Skaffold configuration in the skaffold.yml file.

Some finetuning to make it work

Now the fun begins…

From Skaffold’s point of view, Rancher Desktop is a remote cluster, being in a different WSL distro. Skaffold considers that it should push Docker images to a registry for Rancher to see them, which makes perfect sense, but is not what we want, as the build is in fact local from Rancher’s point of view.

Let’s specify that we want a local build, without push by adding this to skaffold.yaml (otherwise Skaffold tries to push the hello image to the Docker hub):

build:
  artifacts:
    - image: hello
[...]
  local:
    push: false
```

One more thing to tune is the way Skaffold uses Helm. By default, Helm installs are not atomic, and have no timeout. A good practice is to ad these two option to be sure th have something repeatable, that doesn’t hang up when your pod is in CrashLoopBackOff.

Let’s do that by adding the following to our deployment part:

deploy:
  helm:
    flags:
      install: ["--atomic","--timeout=1m"]
      upgrade: ["--atomic","--timeout=1m"]

Java build

Let’s stop here for a minute, shall we?

By default, Skaffold relies on Buildpacks to build your image, and the Dockerfile is of no use. What they don’t tell you in the Rancher documentation, is that for some obscure reason, Buildpacks don’t work with the current version of Rancher (v1.9.1, see this Github issue).

That’s why we rely on a good old Dockerfile, as stated in the Rancher Desktop documentation.

Another reason why you would rely on a Dockerfile is when you use Skaffold for development, and any other CI tool for your other environments: having your Maven goal wrapped up in a single container executing all your phases each and every time is not the best idea if you want to spare some minutes of CI, and I cannot see an easy way to break that in smaller parts in Skaffold at the time being.

So let’s stick with the plan, have a Skaffold process for development, and another different thing for CI. A Dockerfile perfectly fits the requirements.

Ok, but if there is no Buildpack, how does Skaffold actually build our Java application?

That’s the trickiest part. Out of the box, it doesn’t. That’s why we will use Skaffold’s hooks system in order to launch Maven before the Docker build (see documentation).

Add this to your skaffold.yaml:

build:
  artifacts:
    - image: hello
      docker:
        dockerfile: Dockerfile
      hooks:
        before:
        - command: ["sh", "-c", "./mvnw package"]
          os: [darwin, linux]
        - command: ["cmd.exe", "/C", "mvnw.exe package"]
          os: [windows]

This adds a pre-hook to the build phase of Skaffold, invoking the command in the hook before building the Docker image.

Let’s now get this deployed in Rancher.

The Tag process

Docker images are tagged to distinguish each and every version. By default Skaffold uses latest to tag the generated image (that’s because we are not in a git repo). By default, Rancher searches for latest on the Docker hub, and tries to find our image here. On top of that, Helm doesn’t care, and defaults the tag to the helm chart version, which is 1.16.0

Thus, we have to tell Rancher that our image is not on DockerHub by forcing imagePullPolicy to Never in the helm chart, and tell helm that we want the latest tag.

To do so, Skaffold allows us to directly set values to the helm chart, like so:

deploy:
  helm:
    releases:
      - name: hello
        chartPath: hello
        valuesFiles:
          - hello/values.yaml
        version: 0.1.0
        setValues:
          image.pullPolicy: Never
          image.tag: "latest"
Note

As soon as you use a distant repository, using latest tag may send you in cache hell, with apps that don’t correctly update when you change your code, just because of cache policies.

A better solution is to customize the way Skaffold generates the tag and injects it to your helm chart. There are several strategies, depending on what you want to achieve. The simplest one for development purposes is to use the image digest as a tag in your helm chart. This is straightforward and done through setValueTemplates, like that :

      setValueTemplates:
          image.tag: "{{.IMAGE_DIGEST_hello}}"

Now we should be all set up to deploy our application.

Deploy the application in Rancher

Let’s give it a try. Use Skaffold’s run command to deploy the app once:

skaffold run

Hello pods should be up and running in no time!

$ kubectl get pods
NAME                     READY   STATUS    RESTARTS   AGE
hello-5fcc6df995-bhf8r   1/1     Running   0          17s

Some advanced options

Now that we have a working solution, it’s time to pimp up our experience a little more with one more options : automatic port forwarding using the --port-forward option:

skaffold run --port-forward

Now you can browse to the application directly from your development environment.

Once finished, you can tear down everything with skaffold delete.

Wrap-up

We’ve seen how to deploy a simple Java application inside our Kubernetes cluster using a regular Dockerfile and Maven based compilation process. We’ve also seen how to leverage Tags to update our deployment as soon as our code changes locally, thus reducing feedback loop. Last, we’ve seen some neat advanced features to activate port forwarding.

This is as not-so-simple as it gets when using a basic Java compilation process, but there is a lot more to discover in the official Skaffold documentation. Let’s keep that for another article ;).