Scale Kubernetes pods based on Azure Service Bus Queue using Keda

In this blog post, we will use KEDA (Kubernetes Event-driven Autoscaling) for autoscaling pod count based on Azure Service Bus Queue length. I will be using the local Kubernetes cluster but it should work with Azure Kubernetes Service (AKS) also. All code used in this blog post can be found https://github.com/lets-learn-it/keda-examples/tree/master/azure-service-bus-queue.

Plan of Action as below,

  1. Deploy KEDA in Kubernetes cluster
  2. Create Azure Service Bus Queue using Terraform
  3. Writing Kubernetes configuration files
  4. Testing

Deploy KEDA

KEDA can be installed in the Kubernetes cluster using Helm. More info https://keda.sh/docs/2.6/deploy/.  I deployed KEDA 2.6 in Kubernetes 1.22.x.

Azure Service Bus Queue

We need to create a resource group in order to create a service bus namespace & queue inside that namespace. Below is terraform code to achieve that.

resource "azurerm_resource_group" "kedaq-rg" {
  name     = "keda-demo-rg"
  location = "West Europe"
}

resource "azurerm_servicebus_namespace" "keda-namespace" {
  name                = var.servicebus-namespace-name
  location            = azurerm_resource_group.kedaq-rg.location
  resource_group_name = azurerm_resource_group.kedaq-rg.name
  sku                 = "Standard"

}

resource "azurerm_servicebus_queue" "keda-demoq" {
  name                = var.servicebus-queue-name
  namespace_name      = azurerm_servicebus_namespace.keda-namespace.name
  resource_group_name = azurerm_resource_group.kedaq-rg.name

  enable_partitioning = true
}

For the service bus namespace, we can use the default shared access policy but for the queue, we need to create one access policy. & make sure to create manage access policy (as per KEDA docs).

resource "azurerm_servicebus_queue_authorization_rule" "queuerule" {
  name                = "queuerule"
  namespace_name      = azurerm_servicebus_namespace.keda-namespace.name
  queue_name          = azurerm_servicebus_queue.keda-demoq.name
  resource_group_name = azurerm_resource_group.kedaq-rg.name

  # As per KEDA docs,
  # Service Bus Shared Access Policy needs to be of type Manage.
  # Manage access is required for KEDA to be able to get metrics from Service Bus.
  manage = true
  listen = true
  send   = true
}

make sure to declare variables used in the above configuration & also need to do output some values i.e. connection strings.

output "namespace_primary_connection_string" {
    value     = azurerm_servicebus_namespace.keda-namespace.default_primary_connection_string
    sensitive = true
}

output "queue_primary_connection_string" {
    value     = azurerm_servicebus_queue_authorization_rule.queuerule.primary_connection_string
    sensitive = true
}

Create resources

Run plan & apply to create resources in Azure. After running terraform, I got 1 Azure service bus namespace & one queue inside it.

Kubernetes configuration files

I will deploy Nginx as a deployment which will do nothing. We will add messages & remove messages from the queue manually to test scaling.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: demo-app
spec:
  selector:
    matchLabels:
      app: demo-app
  template:
    metadata:
      labels:
        app: demo-app
    spec:
      containers:
      - name: demo-app
        image: nginx
        resources:
          limits:
            memory: "128Mi"
            cpu: "100m"
        ports:
        - containerPort: 8080
        env:
          # This is not required when using triggerauthentication object
          - name: keda-secret
            valueFrom: 
              secretKeyRef:
                name: queue-policy-secret
                key: connection-string

Everything in the above YAML is normal except the environment variable keda-secret. We can authorize KEDA ScaledObject to access our queue in multiple ways. One of the ways needs this secret.

We will see 2 ways to authorize KEDA to access the queue. There is another way of using identity but for simplicity, we will not see this way in this post.

Way 01 Use TriggerAuthentication

First create secret using default primary connection string of Azure service bus namespace, like below

apiVersion: v1
kind: Secret
metadata:
  name: namespace-secret
type: Opaque
data:
  # This is azure service bus namespace connection string
  connection: RW5kcG9pbnQ9c2I6Ly9rZWRhLXNlcnZpY2VidXMtbmFtZXNwYWNlLnNlcnZpY2VidXMud2luZG93cy5uZXQvO1NoYXJlZEFjY2Vzc0tleU5hbWU9Um9vdE1hbmFnZVNoYXJlZEFjY2Vzc0tleTtTaGFyZWRBY2Nlc3NLZXk9bGQ4akIyV042SWRlbzJkMWJobXpYR01SZWRIOXg5ZloremFxSGtmVUQrcz0=

Now, once we have a secret, we can create TriggerAuthentication for that secret.

apiVersion: keda.sh/v1alpha1
kind: TriggerAuthentication
metadata:
  name: azure-servicebus-auth
spec:
  secretTargetRef:
    - key: connection
      name: namespace-secret # name of secret
      parameter: connection # key in secret

The remaining piece in the puzzle is ScaledObject. Which we can create using below YAML,

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: azure-servicebus-queue-scaledobject
  namespace: default
spec:
  scaleTargetRef:
    kind: Deployment
    name: demo-app
  pollingInterval: 30
  cooldownPeriod: 60
  minReplicaCount: 1
  maxReplicaCount: 4

  triggers:
  - type: azure-servicebus
    metadata:
      queueName: keda-demoq
      messageCount: "5"
    authenticationRef:
        # reference to TriggerAuthentication
        name: azure-servicebus-auth

That's it, It will create HPA & if HPA is not created then check the logs of the operator. Make sure to check status of ScaledObject.

Testing

I sent 6 messages to the queue using the service bus explorer. And KEDA up scaled pods to 2. To check to downscale, remove some messages from the queue (make sure to wait for cooldownPeriod).

Way 02 Use pod's environment variable

We can use the pod's environment variable for authorization also. I personally discourage using it. In this method, we need the connection string of the queue as an environment variable of deployment. But before starting way 02, remove resources from way 01.

The secret will get changed (not connection string of namespace but of the queue itself. for this reason, we created authorization rule in terraform). And we don't need TriggeredAuthentication.

apiVersion: v1
kind: Secret
metadata:
  name: queue-policy-secret
type: Opaque
data:
  # This is queue connection string
  connection: RW5kcG9pbnQ9c2I6Ly9rZWRhLXNlcnZpY2VidXMtbmFtZXNwYWNlLnNlcnZpY2VidXMud2luZG93cy5uZXQvO1NoYXJlZEFjY2Vzc0tleU5hbWU9cXVldWVydWxlO1NoYXJlZEFjY2Vzc0tleT1pWTNzSlh5OWorL0wvZVhMYmU5RmFseWVDU1pMWHM4WDFQVTZ2dkhzeW1nPTtFbnRpdHlQYXRoPWtlZGEtZGVtb3E=

The only change in ScaledObject is connectionFromEnv parameter & removing TriggerAuthentication reference.

apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
  name: azure-servicebus-queue-scaledobject
  namespace: default
spec:
  scaleTargetRef:
    kind: Deployment
    name: demo-app
  pollingInterval: 30
  cooldownPeriod: 60
  minReplicaCount: 1
  maxReplicaCount: 4

  triggers:
  - type: azure-servicebus
    metadata:
      queueName: keda-demoq
      messageCount: "5"
      # ENV var of deployment
      connectionFromEnv: keda-secret

This time, you can't see anything in front of Authentication but it should show Ready status & it will work.

Cleanup

Make sure to clean all resources, especially those you created in Azure.  No one wants to pay the cost for unused resources.

References

[1] https://keda.sh/docs/2.6/scalers/azure-service-bus/
[2] https://github.com/kedacore/keda/blob/main/pkg/scalers/azure_servicebus_scaler.go

Parikshit Patil

Parikshit Patil

Currently working as Software Engineer at Siemens Industry Software Pvt. Ltd. Certified AWS Certified Sysops Administrator - Associate.
Kavathe-Ekand, MH India