Skip to content

Scale Applications Using SAP BTP Cloud Logging Service Metrics ​

This tutorial shows how to configure KEDA to autoscale a Kubernetes workload. The scaling signal is the queue_depth metric stored in SAP BTP Cloud Logging Service (CLS).

SAP BTP Cloud Logging Service is a managed observability backend built on OpenSearch. If your application metrics already flow into CLS through the Kyma Telemetry module, you can use them as autoscaling signals. No separate metrics store is needed.

KEDA 2.20 ships a native opensearch scaler that queries CLS directly using an inline query. This tutorial covers deploying a demo app that emits a queue_depth metric, setting up Telemetry scraping, and creating a ScaledObject that uses that metric as the scaling signal.

Prerequisites ​

Steps ​

Create a CLS Instance and Credentials ​

Share a CLS Instance Across Clusters (Optional) ​

To reuse a single CLS instance across multiple Kyma clusters in the same global account, use SAP Service Manager instance sharing. This lets you create a pointer instance in each cluster that references the shared instance, without affecting its lifecycle when the pointer is deleted.

  1. Mark your CLS instance as shareable. In the SAP BTP cockpit, choose Share Instance from the ... menu for your CLS instance. Alternatively, use the btp CLI:

    bash
    btp share services/instance <instance-id> --subaccount <subaccount-id>
  2. In each Kyma cluster that needs access, create a service instance with the reference-instance plan pointing to the shared instance:

    bash
    kubectl apply -f - <<EOF
    apiVersion: services.cloud.sap.com/v1
    kind: ServiceInstance
    metadata:
      name: cloud-logging-pointer
      namespace: cls
    spec:
      serviceOfferingName: cloud-logging
      servicePlanName: reference-instance
      parameters:
        instance_name_selector: "<SHARED_INSTANCE_NAME>"
    EOF
  3. Create a service binding on the pointer instance as described in the SAP BTP Operator module tab.

For cross-subaccount sharing, see Working with Multiple Subaccounts.

Deploy the Demo Application ​

The demo application exposes a Prometheus-format queue_depth gauge metric at the /metrics endpoint. KEDA uses this value (as stored in CLS) to determine the desired replica count.

The QUEUE_DEPTH value is set by an init container at Pod startup. To change the value, update the environment variable and restart the Pod.

  1. Save the demo application manifest to a file and apply it:

    bash
    cat > /tmp/demo-app.yaml << 'EOF'
    apiVersion: v1
    kind: Namespace
    metadata:
      name: keda-cls-demo
    ---
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: fake-metrics-nginx-config
      namespace: keda-cls-demo
    data:
      default.conf.template: |
        server {
            listen 8080;
            root /usr/share/nginx/html;
    
            location /metrics {
                default_type "text/plain; version=0.0.4; charset=utf-8";
                try_files /metrics.txt =404;
            }
    
            location /health {
                default_type text/plain;
                return 200 "OK";
            }
    
            location / {
                return 404;
            }
        }
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: fake-metrics
      namespace: keda-cls-demo
      labels:
        app: fake-metrics
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: fake-metrics
      template:
        metadata:
          labels:
            app: fake-metrics
        spec:
          initContainers:
          - name: generate-metrics
            image: busybox:1.36
            command:
            - sh
            - -c
            - printf '# HELP queue_depth The current depth of the queue\n# TYPE queue_depth gauge\nqueue_depth %d\n' "$QUEUE_DEPTH" > /data/metrics.txt
            env:
            - name: QUEUE_DEPTH
              value: "10"
            volumeMounts:
            - name: metrics-data
              mountPath: /data
          containers:
          - name: fake-metrics
            image: nginx:alpine
            ports:
            - containerPort: 8080
            resources:
              requests:
                memory: "64Mi"
                cpu: "100m"
              limits:
                memory: "128Mi"
                cpu: "200m"
            volumeMounts:
            - name: nginx-config
              mountPath: /etc/nginx/templates
            - name: metrics-data
              mountPath: /usr/share/nginx/html
            livenessProbe:
              httpGet:
                path: /health
                port: 8080
              initialDelaySeconds: 5
              periodSeconds: 10
            readinessProbe:
              httpGet:
                path: /health
                port: 8080
              initialDelaySeconds: 5
              periodSeconds: 5
          volumes:
          - name: nginx-config
            configMap:
              name: fake-metrics-nginx-config
          - name: metrics-data
            emptyDir: {}
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: fake-metrics
      namespace: keda-cls-demo
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/port: "8080"
        prometheus.io/path: "/metrics"
    spec:
      selector:
        app: fake-metrics
      ports:
      - port: 8080
        protocol: TCP
        targetPort: 8080
      type: ClusterIP
    EOF
    
    kubectl apply -f /tmp/demo-app.yaml
  2. Verify that the Pod is running:

    bash
    kubectl get pods -n keda-cls-demo

    The output looks similar to this example:

    bash
    NAME                           READY   STATUS    RESTARTS   AGE
    fake-metrics-<hash>            1/1     Running   0          30s
  3. Confirm the /metrics endpoint is reachable:

    bash
    kubectl run curl-test --image=curlimages/curl --rm -it --restart=Never --quiet \
      -- curl -s http://fake-metrics.keda-cls-demo.svc.cluster.local:8080/metrics

    The output looks similar to this example:

    bash
    # HELP queue_depth The current depth of the queue
    # TYPE queue_depth gauge
    queue_depth 10

Configure the Telemetry Module to Forward Metrics to CLS ​

The Kyma Telemetry module scrapes Prometheus metrics from annotated Services and forwards them to your CLS instance. The demo application manifest already includes the Prometheus scraping annotations.

  1. Create a MetricPipeline resource that sends the scraped metrics to CLS using the OTLP credentials from the binding Secret:

    bash
    kubectl apply -f - <<EOF
    apiVersion: telemetry.kyma-project.io/v1beta1
    kind: MetricPipeline
    metadata:
      name: cls-metric-pipeline
    spec:
      input:
        prometheus:
          enabled: true
          namespaces:
            include:
              - keda-cls-demo
        istio:
          enabled: false
        runtime:
          enabled: false
        otlp:
          enabled: true
      output:
        otlp:
          endpoint:
            valueFrom:
              secretKeyRef:
                name: cloud-logging-binding
                namespace: cls
                key: ingest-otlp-endpoint
          tls:
            cert:
              valueFrom:
                secretKeyRef:
                  name: cloud-logging-binding
                  namespace: cls
                  key: ingest-otlp-cert
            key:
              valueFrom:
                secretKeyRef:
                  name: cloud-logging-binding
                  namespace: cls
                  key: ingest-otlp-key
    EOF
  2. Verify that the pipeline is ready:

    bash
    kubectl get metricpipeline cls-metric-pipeline

    The output looks similar to this example:

    bash
    NAME                  CONFIGURATION GENERATED   GATEWAY HEALTHY   AGENT HEALTHY   FLOW HEALTHY   AGE
    cls-metric-pipeline   True                      True              True            True           2m
  3. In the CLS OpenSearch Dashboards, confirm that the queue_depth metric is arriving:

    1. Get the Dashboards URL:

      • SAP BTP cockpit: Find the dashboards-endpoint value under View Credentials for your CLS binding.

      • SAP BTP Operator module: Run the following command:

        bash
        echo "https://$(kubectl get secret cloud-logging-binding -n cls -o jsonpath='{.data.dashboards-endpoint}' | base64 -d)"
    2. Open the URL in your browser and log in with the Dashboards credentials from your CLS binding. If you used the SAP BTP Operator module tab, retrieve them with:

      bash
      kubectl get secret cloud-logging-binding -n cls -o jsonpath='{.data.dashboards-username}' | base64 -d && echo
      kubectl get secret cloud-logging-binding -n cls -o jsonpath='{.data.dashboards-password}' | base64 -d && echo
    3. In the navigation menu, go to Discover, select the metrics-otel-v1-* index pattern, and filter for documents with name: queue_depth. The metric appears within 1-2 minutes after the MetricPipeline becomes healthy.

Create a KEDA TriggerAuthentication for CLS ​

KEDA must authenticate with the CLS OpenSearch REST API to run queries. Store the CLS credentials in a Kubernetes Secret and reference them from a TriggerAuthentication.

  1. Create a Secret with your CLS OpenSearch credentials:

    bash
    kubectl apply -f - <<EOF
    apiVersion: v1
    kind: Secret
    metadata:
      name: cls-keda-auth
      namespace: keda-cls-demo
    type: Opaque
    stringData:
      username: "$(kubectl get secret cloud-logging-binding -n cls -o jsonpath='{.data.backend-username}' | base64 -d)"
      password: "$(kubectl get secret cloud-logging-binding -n cls -o jsonpath='{.data.backend-password}' | base64 -d)"
    EOF
  2. Create a TriggerAuthentication that references the Secret:

    bash
    kubectl apply -f - <<EOF
    apiVersion: keda.sh/v1alpha1
    kind: TriggerAuthentication
    metadata:
      name: cls-trigger-auth
      namespace: keda-cls-demo
    spec:
      secretTargetRef:
        - parameter: username
          name: cls-keda-auth
          key: username
        - parameter: password
          name: cls-keda-auth
          key: password
    EOF

Create the ScaledObject ​

The ScaledObject tells KEDA to query CLS for the latest queue_depth value and scale the demo application accordingly.

  1. Export the OpenSearch endpoint and username from your CLS service binding Secret:

    bash
    export CLS_OPENSEARCH_ENDPOINT=https://$(kubectl get secret cloud-logging-binding -n cls -o jsonpath='{.data.backend-endpoint}' | base64 -d)
    export CLS_OPENSEARCH_USERNAME=$(kubectl get secret cloud-logging-binding -n cls -o jsonpath='{.data.backend-username}' | base64 -d)
  2. Create the ScaledObject:

    bash
    cat <<EOF | kubectl apply -f -
    apiVersion: keda.sh/v1alpha1
    kind: ScaledObject
    metadata:
      name: cls-queue-depth-scaler
      namespace: keda-cls-demo
    spec:
      scaleTargetRef:
        name: fake-metrics
      minReplicaCount: 1
      maxReplicaCount: 10
      triggers:
        - type: opensearch
          metadata:
            addresses: "${CLS_OPENSEARCH_ENDPOINT}"
            username: "${CLS_OPENSEARCH_USERNAME}"
            index: "metrics-otel-v1-*"
            query: |
              {
                "size": 0,
                "query": {
                  "bool": {
                    "filter": [
                      { "term": { "name": "queue_depth" } },
                      { "range": { "time": { "gte": "now-1m" } } }
                    ]
                  }
                },
                "aggs": {
                  "latest_value": {
                    "max": { "field": "value" }
                  }
                }
              }
            valueLocation: "aggregations.latest_value.value"
            targetValue: "10"
            skipTLSVerify: "false"
          authenticationRef:
            name: cls-trigger-auth
    EOF

    NOTE

    The targetValue of 10 means KEDA targets one replica per 10 units of queue_depth. With a queue_depth of 42, KEDA targets 5 replicas (ceil(42/10)).

  3. Verify that KEDA has picked up the scaler:

    bash
    kubectl get scaledobject cls-queue-depth-scaler -n keda-cls-demo

    The output looks similar to this example:

    bash
    NAME                     SCALETARGETKIND      SCALETARGETNAME   MIN   MAX   READY   ACTIVE   FALLBACK   PAUSED   TRIGGERS     AUTHENTICATIONS    AGE
    cls-queue-depth-scaler   apps/v1.Deployment   fake-metrics      1     10    True    True     False      False    opensearch   cls-trigger-auth   2m

Observe Autoscaling in Action ​

  1. Check the KEDA-managed HPA:

    bash
    kubectl get hpa -n keda-cls-demo

    The output looks similar to this example:

    bash
    NAME                              REFERENCE                    TARGETS   MINPODS   MAXPODS   REPLICAS   AGE
    keda-hpa-cls-queue-depth-scaler   Deployment/fake-metrics      10/10     1         10        1          2m
  2. Simulate a metric spike by updating the QUEUE_DEPTH environment variable and restarting the Pod:

    bash
    kubectl set env deployment/fake-metrics QUEUE_DEPTH=80 -n keda-cls-demo
    kubectl rollout restart deployment/fake-metrics -n keda-cls-demo

    After the next Telemetry scrape and CLS ingestion cycle (within 1-2 minutes), KEDA queries CLS and adjusts the replica count.

  3. Watch the Pods scale up:

    bash
    kubectl get pods -n keda-cls-demo -w
  4. Set the metric back to a lower value to observe scale-down:

    bash
    kubectl set env deployment/fake-metrics QUEUE_DEPTH=5 -n keda-cls-demo
    kubectl rollout restart deployment/fake-metrics -n keda-cls-demo

    After the cooldown period, the replica count returns to the minimum of 1. This may take up to 5 minutes.

Result ​

Your workload scales automatically in response to the queue_depth metric stored in SAP BTP Cloud Logging Service. KEDA polls CLS on each reconciliation cycle and adjusts the HPA target accordingly.

Clean Up ​

To remove the resources created in this tutorial, run:

bash
kubectl delete namespace keda-cls-demo
kubectl delete metricpipeline cls-metric-pipeline
kubectl delete namespace cls

If you used the SAP BTP cockpit tab, delete the CLS instance in the cockpit under Services → Instances and Subscriptions.