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 ​
- The Keda, Telemetry, and SAP BTP Operator modules are enabled in your Kyma cluster. See Quick Install.
- Your subaccount has an entitlement for Cloud Logging Service with the
standardplan. See Configure Entitlements and Quotas for Subaccounts. kubectlis installed and configured to access your Kyma cluster.
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.
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:
bashbtp share services/instance <instance-id> --subaccount <subaccount-id>In each Kyma cluster that needs access, create a service instance with the
reference-instanceplan pointing to the shared instance:bashkubectl 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>" EOFCreate 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.
Save the demo application manifest to a file and apply it:
bashcat > /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.yamlVerify that the Pod is running:
bashkubectl get pods -n keda-cls-demoThe output looks similar to this example:
bashNAME READY STATUS RESTARTS AGE fake-metrics-<hash> 1/1 Running 0 30sConfirm the
/metricsendpoint is reachable:bashkubectl run curl-test --image=curlimages/curl --rm -it --restart=Never --quiet \ -- curl -s http://fake-metrics.keda-cls-demo.svc.cluster.local:8080/metricsThe 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.
Create a MetricPipeline resource that sends the scraped metrics to CLS using the OTLP credentials from the binding Secret:
bashkubectl 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 EOFVerify that the pipeline is ready:
bashkubectl get metricpipeline cls-metric-pipelineThe output looks similar to this example:
bashNAME CONFIGURATION GENERATED GATEWAY HEALTHY AGENT HEALTHY FLOW HEALTHY AGE cls-metric-pipeline True True True True 2mIn the CLS OpenSearch Dashboards, confirm that the
queue_depthmetric is arriving:Get the Dashboards URL:
SAP BTP cockpit: Find the
dashboards-endpointvalue under View Credentials for your CLS binding.SAP BTP Operator module: Run the following command:
bashecho "https://$(kubectl get secret cloud-logging-binding -n cls -o jsonpath='{.data.dashboards-endpoint}' | base64 -d)"
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:
bashkubectl 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 && echoIn the navigation menu, go to Discover, select the
metrics-otel-v1-*index pattern, and filter for documents withname: 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.
Create a Secret with your CLS OpenSearch credentials:
bashkubectl 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)" EOFCreate a TriggerAuthentication that references the Secret:
bashkubectl 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.
Export the OpenSearch endpoint and username from your CLS service binding Secret:
bashexport 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)Create the ScaledObject:
bashcat <<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 EOFNOTE
The
targetValueof10means KEDA targets one replica per 10 units ofqueue_depth. With aqueue_depthof 42, KEDA targets 5 replicas (ceil(42/10)).Verify that KEDA has picked up the scaler:
bashkubectl get scaledobject cls-queue-depth-scaler -n keda-cls-demoThe output looks similar to this example:
bashNAME 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 ​
Check the KEDA-managed HPA:
bashkubectl get hpa -n keda-cls-demoThe output looks similar to this example:
bashNAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE keda-hpa-cls-queue-depth-scaler Deployment/fake-metrics 10/10 1 10 1 2mSimulate a metric spike by updating the
QUEUE_DEPTHenvironment variable and restarting the Pod:bashkubectl set env deployment/fake-metrics QUEUE_DEPTH=80 -n keda-cls-demo kubectl rollout restart deployment/fake-metrics -n keda-cls-demoAfter the next Telemetry scrape and CLS ingestion cycle (within 1-2 minutes), KEDA queries CLS and adjusts the replica count.
Watch the Pods scale up:
bashkubectl get pods -n keda-cls-demo -wSet the metric back to a lower value to observe scale-down:
bashkubectl set env deployment/fake-metrics QUEUE_DEPTH=5 -n keda-cls-demo kubectl rollout restart deployment/fake-metrics -n keda-cls-demoAfter 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:
kubectl delete namespace keda-cls-demo
kubectl delete metricpipeline cls-metric-pipeline
kubectl delete namespace clsIf you used the SAP BTP cockpit tab, delete the CLS instance in the cockpit under Services → Instances and Subscriptions.