Deploying a Quarkus application on Kubernetes
TL;DR
We will see how to deploy in detail a Quarkus application into a Kubernetes cluster.
To achieve this goal, we will use:
Quarkus and Kubernetes
Usually, when people hear about Quarkus, they often think it's just a tool that speeds up the start of our application and consumes fewer computational resources. However, there's something very interesting as well: Quarkus is a Kubernetes-native framework with various extensions and functionalities that make working with Kubernetes easier.
If you work with Kubernetes, managing multiple applications, and find yourself manually writing Kubernetes resources (such as Deployment, Service, ConfigMap, etc.) for each application, I would recommend using a tool like Helm, for instance. Now, if you are using Quarkus and desire even more convenience and a simpler way (with minimal manifest customization) without the need to worry about templates, etc., let me introduce you to the Quarkus Kubernetes extension.
Kubernetes Extension
The Kubernetes extension generates Kubernetes resources based on configurations. These configurations can be supplied by us (users) to generate the resources.
It is possible to generate resources for OpenShift, vanilla Kubernetes, and Knative. You can create and customize RBAC
resources, Ingress Controllers, Services, Deployments, etc. For more information, refer to the official documentation.
GitOps
This is a valuable feature of the GitOps approach. The Quarkus build can generate all the required resources, allowing configuration through environment variables (overriding application.properties
values). These generated resources serve as input for GitOps tooling.
Creating the Kubernetes cluster
We will create a Kubernetes cluster with Kubernetes in Docker (KinD). As the name suggests, KinD aims to run a Kubernetes cluster inside Docker.
Our Kubernetes cluster will have 1 control plane and 1 worker node!
Installing KinD
An important note here is that we are labeling our nodes with ingress-ready=true
. We will use the NGINX Ingress controller, and this labeling is necessary because NGINX only assigns pods to nodes that have this specific label.
The KinD documentation says:
- extraPortMappings allow the local host to make requests to the Ingress controller over ports 80/443
- node-labels only allow the ingress controller to run on a specific node(s) matching the label selector
After creating, we will install the NGINX Ingress Controller to expose internal Services.
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
Creating the Quarkus application
We will use Quarkus CLI to generate the application. See how to install Quarkus CLI here.
Creating the Quarkus application with all necessary extensions:
quarkus create app dev.matheuscruz:quarkus-k8s --extension='quarkus-container-image-jib,quarkus-kind,resteasy-reactive'
- The
quarkus-container-image-jib
extension assists in creating a container image using Jib. - The
quarkus-kind
extension helps generate Kubernetes resources for vanilla Kubernetes and KinD. - The
resteasy-reactive
extension helps create a REST service.
Explore the Kubernetes resources generated by Quarkus:
The output should look something like this:
Output from cat command
There are two points here. The first one is that quarkus build
does not create the container image, and the container image is not correctly associated with my Docker Registry username, which is not cruz
. This happens because, by default, the quarkus-kubernetes
extension uses the system username if the Docker Registry username is not defined. The image was not created because we needed to instruct quarkus build
to generate our container image.
Configuring our build and container image
To meet our requirements for configuring the container image information and building the container image during quarkus build
, Quarkus offers various configuration options. We will explore the most commonly used ones.
- If you observe the generated resources, you will notice that no namespace is configured. To set the namespace of the resources, use the
quarkus.kubernetes.namespace
configuration to define the namespace. - The
quarkus.container-image.build=true
configuration instructs the Quarkus CLI to generate the container image during the build. - In line "3," we are configuring the group of the container image, now, Quarkus will not get my system user.
- The property
quarkus.container-image.name
sets the container name. - The property
quarkus.container-image.tag
sets the container image tag.
The last two properties instruct Quarkus to generate an Ingress Controller and expose the application using it. The final property defines the value for spec.rules[].host
in the IngressController.
Applying all those properties into the application.properties
and executing quarkus build
again, we will get the following Ingress Controller definition:
As you can see, it generated an Ingress Controller pointing to our Service for us. It is very cool!
All generated resources
---
apiVersion: v1
kind: Service
metadata:
annotations:
app.quarkus.io/build-timestamp: 2024-01-13 - 22:13:14 +0000
labels:
app.kubernetes.io/name: quarkus-k8s
app.kubernetes.io/version: 1.0.0-SNAPSHOT
app.kubernetes.io/managed-by: quarkus
name: quarkus-k8s
namespace: default
spec:
ports:
- name: http
port: 80
protocol: TCP
targetPort: 8080
selector:
app.kubernetes.io/name: quarkus-k8s
app.kubernetes.io/version: 1.0.0-SNAPSHOT
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
app.quarkus.io/build-timestamp: 2024-01-13 - 22:13:14 +0000
labels:
app.kubernetes.io/name: quarkus-k8s
app.kubernetes.io/version: 1.0.0-SNAPSHOT
app.kubernetes.io/managed-by: quarkus
name: quarkus-k8s
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/version: 1.0.0-SNAPSHOT
app.kubernetes.io/name: quarkus-k8s
template:
metadata:
annotations:
app.quarkus.io/build-timestamp: 2024-01-13 - 22:13:14 +0000
labels:
app.kubernetes.io/managed-by: quarkus
app.kubernetes.io/version: 1.0.0-SNAPSHOT
app.kubernetes.io/name: quarkus-k8s
namespace: default
spec:
containers:
- env:
- name: KUBERNETES_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
image: matheuscruzdev/quarkus-k8s:1.0
imagePullPolicy: Always
name: quarkus-k8s
ports:
- containerPort: 8080
name: http
protocol: TCP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
annotations:
app.quarkus.io/build-timestamp: 2024-01-13 - 22:13:14 +0000
labels:
app.kubernetes.io/name: quarkus-k8s
app.kubernetes.io/version: 1.0.0-SNAPSHOT
app.kubernetes.io/managed-by: quarkus
name: quarkus-k8s
namespace: default
spec:
rules:
- host: localhost
http:
paths:
- backend:
service:
name: quarkus-k8s
port:
name: http
path: /
pathType: Prefix
It is possible too see our container image locally:
docker images | grep quarkus-k8s
# ... ommited
matheuscruzdev/quarkus-k8s 1.0 d02096d708c6 4 seconds ago 418MB
GitOps Tip: Avoid Unnecessary deployments
As mentioned in this post, the Kubernetes extension is an excellent tool for the GitOps approach. If you observe the annotation app.quarkus.io/build-timestamp
, you will find the timestamp indicating when this manifest was created. This can be an issue for the GitOps approach because tools recognize differences between manifests and apply a deployment strategy. See more details here on creating idempotent resources with the quarkus.kubernetes.idempotent
property.
Deploying our application
To deploy a Quarkus application into a Kubernetes cluster is very simple; we just need to execute quarkus deploy
. Let's try it:
Seeing the Kubernetes objects:
If you see the output, we got an ErrImagePull
error. This occurs because it was not possible to download the container image from the registry. It makes sense because we did not upload it before.
Quarkus offers the possibility to push the image to a container registry using the following configurations:
quarkus.container-image.username=myusername
quarkus.container-image.password=mypassword
quarkus.container-image.registry=docker.io # used by default
I will use the docker push command to upload the container image to the Docker Registry. The command is docker push <docker-user>/quarkus-k8s:1.0
.
Pushing to registry during build
If you want to push the image during build, you can use the following configurations:
quarkus.container-image.username=myusername
quarkus.container-image.password=mypassword
quarkus.container-image.registry=docker.io # used by default
Execute the build command:
Now, if we execute the quarkus deploy
command and access this endpoint... You will get this:
Nice! We have a Quarkus application running into Kubernetes! without to worry about Kubernetes manifests.
References and resources
Source code
If you'd like to view the entire code, you can access it here.
Thank you
That's all; thank you for reading! See you in the next post. Goodbye!