Using bicep to define Service Bus scaling rules for Azure Container Apps

Posted by Rik Hepworth on Friday, May 17, 2024

I recently needed to set KEDA scaling rules on an Azure Container app that used the number of messages in a Service Bus queue. There’s plenty of info out there on the internet about scaling rules, but not when it comes to Service Bus, so I’m writing up what I learned here.

Problem Space

A recent project made use of Azure Container Apps for backend services, driven through messages placed on Service Bus queues. To ensure performance, we need to create scaling rules to add instances of the containers based on the number of messages in the queue.

A worked example

Service Bus Authorisation Rule

For our scaler to work, we need to create an Authorisation Rule that allows KEDA to read the number of messages in our queue. Whilst it’s possible to creat Authorisation Rules at the queue level, frankly I find it easier to create them at the namespace level.

Note: There is a maximum number of rules you can create, so I tend to create one rule and use it for all my Container Apps within an application. If you need to ensure that each container app only has rights to look at its own queue, you’ll probably need to create the rule at the queue level.

As I’ve mentioned in other posts, I like to keep different services in separate modules, so Service Bus is created in its own file, then I have another for the Container App. However, I want to keep the Authorisation Rule with the scaler rule, so I reference the Service Bus instance as an existing resource.

I then create the rule, specifying the Service Bus namespace resouce as its parent to create it at the namespace level.

// reference existing Service Bus
resource ServiceBus 'Microsoft.ServiceBus/namespaces@2021-11-01' existing = {
  name: ServiceBusName
}

// Create service bus authorisation rule for autoscaler
resource ServiceBusAuthorisationRule 'Microsoft.ServiceBus/namespaces/AuthorizationRules@2021-11-01' = {
  name: ServiceBusAuthorisationRuleName
  parent: ServiceBus
  properties: {
    rights: [
      'Send'
      'Listen'
      'Manage'
    ]
  }
}

Container App scaling rule

This isn’t a post about creating Container App environments or the Container Apps themselves, so the sample bicep below shows the stuff we need rather than the whole resource definition.

We can’t create the scaling rule without the Authorisation Rule, but we don’t reference that resource directly, so we need to add a dependency to make sure we only deploy after it’s created.

We must provide an appropriate access key in order to connect to Service Bus. That gets defined as a secret within the Container App and as you can see, I’m using the listkeys function to get the ARM fabric to populate our secret with the connection string.

We need to specify the minimum and maximum number of instances we want, setting the upper and lower bounds for our scaler. I use a parameter to pass the maximum value in, which is used here.

The scaling rule needs the correct type so KEDA knows it’s a queue-based scaler - queue-based-autoscaling. Then within my scaling rule I reference another parameter specifying the number of messages that must be on the queue before a new instance is deployed, and the name of the Service Bus queue to watch.

// create Container App in managed environment
resource ContainerApp 'Microsoft.App/containerApps@2023-08-01-preview' = {
  name: ContainerAppName
  dependsOn: [
    ServiceBusAuthorisationRule
  ]
  location: location
  ...
  properties: {
    managedEnvironmentId: ContainerAppEnvironment.id
    configuration: {
      ...
      secrets: [
        {
          name: ServiceBusAuthorisationRuleName
          value: listKeys(listKeysEndpoint, ServiceBus.apiVersion).primaryConnectionString
        }
      ]
    }
    template: {
      containers: [
        ...
      ]
      scale: {
        minReplicas: 1
        maxReplicas: containerAppMaxReplicas
        rules: [
          {
            name: 'queue-based-autoscaling'
            custom: {
              type: 'azure-servicebus'
              metadata: {
                queueName: ServiceBusQueueName
                messageCount: autoscalerRuleMessageCount
              }
              auth: [
                {
                  secretRef: ServiceBusAuthorisationRuleName
                  triggerParameter: 'connection'
                }
              ]
            }
          }
        ]
      }
    }
  }
}

And that’s it. You can combine the queue-based scaling rule with others - I usually add at least a CPU utilisation rule, but those are already well documented. A big thanks to my good friend Tom Kerkhove who has a sample in his GitHub which he pointed me at when the internet gods failed to answer my cries for help!