{{announcement.body}}
{{announcement.title}}

Autoscaling Your Kubernetes Microservice with KEDA

DZone 's Guide to

Autoscaling Your Kubernetes Microservice with KEDA

Introduction to KEDA—event-driven autoscaler for Kubernetes, Apache Camel, and ActiveMQ Artimis—and how to use it to scale a Java microservice on Kubernetes.

· Java Zone ·
Free Resource

Recently, I've been having a look around at autoscaling options for applications deployed onto Kubernetes. There are quite a few emerging options. I decided to give KEDA a spin. KEDA stands for Kubernetes Event-Driven Autoscaling, and that's exactly what it does.

Introducing KEDA

KEDA is a tool you can deploy into your Kubernetes cluster which will autoscale Pods based on an external event or trigger. Events are collected by a set of Scalers, which integrate with a range of applications like:

  • ActiveMQ Artemis
  • Apache Kafka
  • Amazon SQS
  • Azure Service Bus
  • RabbitMQ Queue
  • and about 20 others

To implement autoscaling with KEDA, you firstly configure a Scaler to monitor an application or cloud resource for a metric. This metric is usually something fairly simple, as the number of messages in a queue.

When the metric goes above a certain threshold, KEDA can scale up a Deployment automatically (called “Scaling Deployments”), or create a Job (called “Scaling Jobs”). It can also scale deployments down again when the metric falls, even scaling to zero. It does this by using the Horizontal Pod Autoscaler (HPA) in Kubernetes.

Since KEDA runs as a separate component in your cluster, and it uses the Kubernetes HPA, it is fairly non-intrusive for your application, so it requires almost no change to the application being scaled itself.

KEDA Architecture on a Kubernetes cluster

KEDA Architecture on a Kubernetes cluster


There are many potential use cases for KEDA. Perhaps the most obvious one is scaling an application when messages are received in a queue. 

Many integration applications use messaging as a way of receiving events. So if we can scale an application up and down based on the number of events received, there's the potential to free up resources when they aren't needed, and also to provide increased capacity when we need it.

This is even more true if we combine something like KEDA with cluster-level autoscaling. If we can scale the cluster's nodes themselves, according to the demand from applications, then this could help save costs.

Many of the KEDA scalers are based around messaging, which is a common pattern for integration. When I think of messaging and integration, I immediately think of Apache Camel and Apache ActiveMQ and wanted to explore whether it's possible to use KEDA to scale a simple microservice that uses these popular projects. So let's see what KEDA can do.

Demo - KEDA with Apache Camel and ActiveMQ Artemis

We'll deploy a Java microservice onto Kubernetes which consumes messages from a queue. The application uses Apache Camel as the integration framework and Quarkus as the runtime. 

We’ll also deploy an ActiveMQ Artemis message broker, and use KEDA’s Artemis scaler to watch for messages on the queue and scale the application up or down.

Architecture of a KEDA-scaled Camel application

Architecture of a KEDA-scaled Camel application


Creating the Demo Application

I’ve already created the example Camel application, which uses Quarkus as the runtime. I’ve published the container image to Docker Hub and I use that in the steps further below. But, if you’re interested in how it was created, read on.

Get the code on GitHub 

Get the image on Docker Hub

I decided to use Quarkus because it boasts super-fast startup times, considerably faster than Spring Boot. When we’re reacting to events, we want to be able to start up quickly and not wait too long for the app to start.

To create the app, I used the Quarkus app generator.

Quarkus is configured using extensions, so I needed to find an extension that would help me create a connection factory to talk to ActiveMQ Artemis. For this, we can use the Qpid JMS Extension for Quarkus, which wraps up the Apache Qpid JMS client for Quarkus applications. This allows me to talk to ActiveMQ Artemis using the open AMQP 1.0 protocol.

The Qpid JMS extension creates a connection factory to ActiveMQ when it finds certain config properties. You only need to set the properties quarkus.qpid-jms.url, quarkus.qpid-jms.username and quarkus.qpid-jms.password. The Extension will do the rest automatically, as it says in the readme:

Table showing config properties for Qpid JMS Quarkus extension

Table showing config properties for Qpid JMS Quarkus extension


Then, I use Camel’s JMS component to consume the messages. This will detect and use the same connection factory created by the extension. The Camel route looks like this:

Java
 




xxxxxxxxxx
1


1
from("jms:queue:ALEX.BIRD")
2
    .log("Honk honk! I just received a message: ${body}");


Finally, I compile the application into a native binary, not a JAR. This will help it to start up very quickly. You need GraalVM to be able to do this. Switch to your GraalVM (e.g. using Sdkman), then:

./mvnw package -Pnative

Or, if you don’t want to install GraalVM, you can tell Quarkus to use a Docker container with GraalVM baked in, to build the native image. You’ll need Docker running to be able to do this, of course:

./mvnw package -Pnative -Dquarkus.native.container-build=true

The output from this is a native binary application, which should start up faster than a typical JVM-based application. Nice. Good for rapid scale-up when we receive messages!

Finally, I build the native binary into a container image with Docker, and push it up to a registry; in this case, Docker Hub. There’s a Dockerfile provided with the Quarkus quickstart to do the build. Then the final step is  a docker push:

docker build -f src/main/docker/Dockerfile.native -t monodot/camel-amqp-quarkus . 

docker push monodot/camel-amqp-quarkus

Now we’re ready to deploy the app, deploy KEDA, and configure it to auto-scale the app.

Deploying KEDA and the Demo App

1. First, install KEDA on your Kubernetes cluster and create some namespaces for the demo.

To deploy KEDA, you can follow the latest instructions on the KEDA web site, and I installed it using the Helm option:

Shell
 




x


1
$ helm repo add kedacore https://kedacore.github.io/charts
2
$ helm repo update
3
$ kubectl create namespace keda
4
$ helm install keda kedacore/keda --namespace keda
5
$ kubectl create namespace keda-demo



2. Now we need to deploy an ActiveMQ Artemis message broker.

Here’s some YAML to create a Deployment, Service, and ConfigMap in Kubernetes. It uses the vromero/activemq-artemis community image of Artemis on Docker Hub and exposes its console and AMQP ports. I’m customizing it by adding a ConfigMap which:

  • Changes the internal name of the broker to a static name: keda-demo-broker
  • Defines one queue, called ALEX.BIRD. If we don’t do this, then the queue will be created when a consumer connects to it, but it will be removed again when the consumer is scaled down, and so KEDA won't be able to fetch the metric correctly anymore. So we define the queue explicitly.

The YAML:

YAML
 




x


 
1
$ kubectl apply -f - <<API
2
apiVersion: v1
3
kind: List
4
items:
5
- apiVersion: v1
6
  kind: Service
7
  metadata:
8
    creationTimestamp: null
9
    name: artemis
10
    namespace: keda-demo
11
  spec:
12
    ports:
13
    - port: 61616
14
      protocol: TCP
15
      targetPort: 61616
16
      name: amqp
17
    - port: 8161
18
      protocol: TCP
19
      targetPort: 8161
20
      name: console
21
    selector:
22
      run: artemis
23
  status:
24
    loadBalancer: {}
25
- apiVersion: apps/v1
26
  kind: Deployment
27
  metadata:
28
    creationTimestamp: null
29
    labels:
30
      run: artemis
31
    name: artemis
32
    namespace: keda-demo
33
  spec:
34
    replicas: 1
35
    selector:
36
      matchLabels:
37
        run: artemis
38
    strategy: {}
39
    template:
40
      metadata:
41
        creationTimestamp: null
42
        labels:
43
          run: artemis
44
      spec:
45
        containers:
46
        - env:
47
          - name: ARTEMIS_USERNAME
48
            value: quarkus
49
          - name: ARTEMIS_PASSWORD
50
            value: quarkus
51
          image: vromero/activemq-artemis:2.11.0-alpine
52
          name: artemis
53
          ports:
54
          - containerPort: 61616
55
          - containerPort: 8161
56
          volumeMounts:
57
          - name: config-volume
58
            mountPath: /var/lib/artemis/etc-override
59
        volumes:
60
          - name: config-volume
61
            configMap:
62
              name: artemis
63
- apiVersion: v1
64
  kind: ConfigMap
65
  metadata:
66
    name: artemis
67
    namespace: keda-demo
68
  data:
69
    broker-0.xml: |
70
      <?xml version="1.0" encoding="UTF-8" standalone="no"?>
71
      <configuration xmlns="urn:activemq" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:activemq /schema/artemis-configuration.xsd">
72
        <core xmlns="urn:activemq:core" xsi:schemaLocation="urn:activemq:core ">
73
          <name>keda-demo-broker</name>
74
          <addresses>
75
            <address name="DLQ">
76
              <anycast>
77
                <queue name="DLQ"/>
78
              </anycast>
79
            </address>
80
            <address name="ExpiryQueue">
81
              <anycast>
82
                <queue name="ExpiryQueue"/>
83
              </anycast>
84
            </address>
85
            <address name="ALEX.BIRD">
86
              <anycast>
87
                <queue name="ALEX.BIRD"/>
88
              </anycast>
89
            </address>
90
          </addresses>
91
        </core>
92
      </configuration>
93
API



3. Next, we deploy the demo Camel application and add some configuration.

So we need to create a Deployment. I’m deploying my demo image monodot/camel-amqp-quarkus from Docker Hub. You can deploy my image, or you can build and deploy your image if you prefer.

We use the environment variables QUARKUS_QPID_JMS_* to set the URL, username and password for the ActiveMQ Artemis broker. These will override the properties quarkus.qpid-jms.* in my application’s properties file:

YAML
 




xxxxxxxxxx
1
33


 
1
$ kubectl apply -f - <<API
2
apiVersion: apps/v1
3
kind: Deployment
4
metadata:
5
  creationTimestamp: null
6
  labels:
7
    run: camel-amqp-quarkus
8
  name: camel-amqp-quarkus
9
  namespace: keda-demo
10
spec:
11
  replicas: 1
12
  selector:
13
    matchLabels:
14
      run: camel-amqp-quarkus
15
  strategy: {}
16
  template:
17
    metadata:
18
      creationTimestamp: null
19
      labels:
20
        run: camel-amqp-quarkus
21
    spec:
22
      containers:
23
      - env:
24
        - name: QUARKUS_QPID_JMS_URL
25
          value: amqp://artemis:61616
26
        - name: QUARKUS_QPID_JMS_USERNAME
27
          value: quarkus
28
        - name: QUARKUS_QPID_JMS_PASSWORD
29
          value: quarkus
30
        image: monodot/camel-amqp-quarkus:latest
31
        name: camel-amqp-quarkus
32
        resources: {}
33
API



4. Now we tell KEDA to scale down the pod when it sees no messages and scales it back up when there are messages.

We do this by creating a ScaledObject. This tells KEDA which Deployment to scale, and when to scale it. The triggers section defines the Scaler to be used. In this case, it's the ActiveMQ Artemis scaler, which uses the Artemis API to query messages on an address (queue):

YAML
 




x


1
$ kubectl apply -f - <<API
2
apiVersion: keda.k8s.io/v1alpha1
3
kind: ScaledObject
4
metadata:
5
  name: camel-amqp-quarkus-scaler
6
  namespace: keda-demo
7
spec:
8
  scaleTargetRef:
9
    deploymentName: camel-amqp-quarkus
10
  pollingInterval: 30
11
  cooldownPeriod:  30  # Default: 300 seconds
12
  minReplicaCount: 0
13
  maxReplicaCount: 2
14
  triggers:
15
  - type: artemis-queue
16
    metadata:
17
      managementEndpoint: "artemis.keda-demo:8161"
18
      brokerName: "keda-demo-broker"
19
      username: 'QUARKUS_QPID_JMS_USERNAME'
20
      password: 'QUARKUS_QPID_JMS_PASSWORD'
21
      queueName: "ALEX.BIRD"
22
      brokerAddress: "ALEX.BIRD"
23
      queueLength: '10'
24
API



By the way, to get the credentials to use the Artemis API, KEDA will look for any environment variables which are set on the Camel app's Deployment object. This means that you don’t have to specify the credentials twice. So here I’m using QUARKUS_QPID_JMS_USERNAME and PASSWORD. These identifiers reference the same environment variables on the demo app’s Deployment.

5. Now let’s put some test messages onto the queue.

You can do this in a couple of different ways: either point and click using the Artemis web console, or use the Jolokia REST API.

Either way, we need to be able to reach the artemis Kubernetes Service, which isn’t exposed outside the Kubernetes cluster. You can expose it by setting up an Ingress, or a Route in OpenShift, but I just use kubectl’s port forwarding feature instead. It’s simple. This allows me to access the ActiveMQ web console and API on localhost port 8161:

Java
 




xxxxxxxxxx
1


1
$ kubectl port-forward -n keda-demo svc/artemis 8161:8161


Leave that running in the background.

Now, in a different terminal, hit the Artemis Jolokia API with curl, via the kubectl port-forwarding proxy. We want to send a message to an Artemis queue called ALEX.BIRD.

This part requires a ridiculously long API call, so I’ve added some line breaks here to make it easier to read. This uses ActiveMQ’s Jolokia REST API to put a message in the Artemis queue:

Shell
 




x





1
curl -X POST --data "{\"type\":\"exec\",\
2
\"mbean\":\
3
\"org.apache.activemq.artemis:broker=\\\"keda-demo-broker\\\",component=addresses,address=\\\"ALEX.BIRD\\\",subcomponent=queues,routing-type=\\\"anycast\\\",queue=\\\"ALEX.BIRD\\\"\",\
4
\"operation\":\
5
\"sendMessage(java.util.Map,int,java.lang.String,boolean,java.lang.String,java.lang.String)\",\
6
\"arguments\":\
7
[null,3,\"HELLO ALEX\",false,\"quarkus\",\"quarkus\"]}" http://quarkus:quarkus@localhost:8161/console/jolokia/


(If you have any issues with this, just use the Artemis web console to send a message; you'll find it at http://localhost:8161/console)

6. Once you've put messages in the queue, you should see the demo app pod starting up and consuming the messages. In the screenshot on the left, there was previously no Pod running, but when a message was sent, KEDA scaled up the application (shown in yellow):

KEDA scaled up the demo app (camel-amqp-quarkus) when it noticed a message


After all, messages are consumed, there will be no messages left on the queue. KEDA waits for the cooldown period (in this demo I’ve used 30 seconds as an example) and then scales down the deployment to zero, so no pods are running.

You can also see this behavior if you watch the pods using kubectl get pods:

Shell
 




xxxxxxxxxx
1
12


 
1
$ kubectl get pods -n keda-demo -w
2
NAME                       READY   STATUS    RESTARTS   AGE
3
artemis-7d955bf44b-892k4   1/1     Running   0          84s
4
camel-amqp-quarkus-748c5f9c77-nrf5k   0/1     Pending   0          0s
5
camel-amqp-quarkus-748c5f9c77-nrf5k   0/1     Pending   0          0s
6
camel-amqp-quarkus-748c5f9c77-nrf5k   0/1     ContainerCreating   0          0s
7
camel-amqp-quarkus-748c5f9c77-nrf5k   1/1     Running             0          3s
8
camel-amqp-quarkus-748c5f9c77-nrf5k   1/1     Terminating         0          30s
9
camel-amqp-quarkus-748c5f9c77-nrf5k   0/1     Terminating         0          32s
10
camel-amqp-quarkus-748c5f9c77-nrf5k   0/1     Terminating         0          43s
11
camel-amqp-quarkus-748c5f9c77-nrf5k   0/1     Terminating         0          43s


Conclusion

KEDA is a useful emerging project for autoscaling applications on Kubernetes and it's got lots of potential uses. I like it because of its relative simplicity. It's very quick to deploy and non-intrusive to the applications that you want to scale.  

KEDA makes it possible to scale applications based on events, which is something that many product teams could be interested in, especially with the potential savings on resources.

Topics:
amqp, apache camel, integration, java, kubernetes, messaging, serverless

Published at DZone with permission of Tom Donohue . See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}