Autoscaling Your Kubernetes Microservice with KEDA

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:

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:

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.

 

 

 

 

Top