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.
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
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.
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
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
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:
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 hello
folder 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)
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"
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 ;).