Skip to content

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

See here: How to install KinD with a package manager

cat <<EOF | kind create cluster --name dev --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  kubeadmConfigPatches:
  - |
    kind: InitConfiguration
    nodeRegistration:
      kubeletExtraArgs:
        node-labels: "ingress-ready=true"
  extraPortMappings:
  - containerPort: 80
    hostPort: 80
    protocol: TCP
  - containerPort: 443
    hostPort: 443
    protocol: TCP
- role: worker
EOF

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:

quarkus build && cat target/kubernetes/kubernetes.yml

The output should look something like this:

Output from cat command
---
apiVersion: v1
kind: Service
metadata:
  annotations:
    app.quarkus.io/build-timestamp: 2024-01-13 - 21:32:52 +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
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 - 21:32:52 +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
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 - 21:32:52 +0000
      labels:
        app.kubernetes.io/managed-by: quarkus
        app.kubernetes.io/version: 1.0.0-SNAPSHOT
        app.kubernetes.io/name: quarkus-k8s
    spec:
      containers:
        - env:
            - name: KUBERNETES_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
          image: cruz/quarkus-k8s:1.0.0-SNAPSHOT
          imagePullPolicy: Always
          name: quarkus-k8s
          ports:
            - containerPort: 8080
              name: http
              protocol: TCP

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.

1
2
3
4
5
6
7
8
9
quarkus.kubernetes.namespace=default
quarkus.container-image.build=true
quarkus.container-image.group=matheuscruzdev
quarkus.container-image.name=k6-with-quarkus
quarkus.container-image.tag=1.0

# Ingress Controller configs
quarkus.kubernetes.ingress.expose=true
quarkus.kubernetes.ingress.host=localhost
  1. 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.
  2. The quarkus.container-image.build=true configuration instructs the Quarkus CLI to generate the container image during the build.
  3. In line "3," we are configuring the group of the container image, now, Quarkus will not get my system user.
  4. The property quarkus.container-image.name sets the container name.
  5. 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:

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

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:

quarkus deploy

Seeing the Kubernetes objects:

kubectl get pods

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:

quarkus build -Dquarkus.container-image.push=true

Now, if we execute the quarkus deploy command and access this endpoint... You will get this:

Hello from RESTEasy Reactive

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! 👋

Comments