• English icon
  • German icon
Erklärvideo

Warum skalierte meine Container App nicht über die Queue?

· Reihe: Container auf Azure

azureazure-container-appskedamanaged-identitydevops

Das Video ist auf Englisch.

Ich habe 200 Nachrichten in eine Azure Storage Queue gelegt, für einen Worker auf Azure Container Apps. 22 Minuten lang wurde keine einzige verarbeitet. Zwei Korrekturen später haben zehn Replicas dieselbe Queue in etwa zwei Minuten geleert.

Dieser Explainer zeigt, wie eine Container App erfährt, dass Nachrichten warten (gar nicht: KEDA zählt sie für sie), welche zwei Dinge meinem Worker gefehlt haben und wo der eigentliche Fehler steckte. Alles hier stammt aus einem echten Durchlauf in meinem eigenen Azure-Abonnement. Das Video ist auf Englisch.

Die wichtigsten Punkte

  • Ohne Scale-Regel zählt niemand Ihre Queue. Ein Worker ohne Ingress bekommt keine HTTP-Requests, die Standard-HTTP-Regel hat also nichts zu zählen. Mein Worker behielt fünfzehn Minuten lang das eine Replica, mit dem er gestartet war; das Plattform-Log meldete "no triggers defined".
  • Eine Queue-Regel übergibt das Zählen an KEDA. Alle 30 Sekunden fragt KEDA die Queue nach der Zahl wartender Nachrichten und teilt durch die Einstellung Queue Length: ein Ziel pro Replica, kein Schwellenwert. 200 Nachrichten ÷ 10 = 20 Replicas, begrenzt auf das Maximum von 10.
  • Die Identität der Regel braucht eine Rolle. Die Scale-Regel meldet sich mit der Managed Identity der App an, aber eine Identität ist nur ein Ausweis. Ohne Datenrolle auf dem Storage Account kann der Scaler nicht zählen, und die App zeigt keinen Fehler. Mit Storage Queue Data Contributor skalierte sie von 1 auf 10.
  • Jede Nachricht weckt die App; eine leere Queue lässt sie gehen. Drei Nachrichten bei null Replicas weckten nach 41 Sekunden ein Replica. Nachdem die Queue leer war, blieben alle zehn Replicas noch fünf Minuten und waren dann innerhalb von 40 Sekunden weg.

Niemand zählt

Ein Queue-Worker hat keinen Ingress: keinen öffentlichen HTTP-Endpunkt, niemand ruft ihn auf, er holt sich seine Arbeit aus der Queue. So habe ich meinen zuerst angelegt: kein Ingress, Minimum null, Maximum zehn, keine Scale-Regel.

Container Apps skaliert über Regeln. Die Standardregel zählt HTTP-Requests, und ein Worker ohne Ingress bekommt keine. Also beobachtete niemand die Queue: Die App behielt fünfzehn Minuten lang das eine Replica, mit dem sie gestartet war, während 200 Nachrichten warteten. Weder hoch auf zehn noch runter auf null. Die Doku warnt, dass so eine App auf null skalieren und nie wieder starten kann; meine hat sich einfach nie verändert. So oder so: Die Queue bewegt die Zahl nicht.

Dieses eine Replica hat auch keine einzige Nachricht verarbeitet. Das führt zum zweiten Problem weiter unten.

Die Queue-Regel

Mit einer Scale-Regel sagen Sie Container Apps, was es beobachten soll. Darunter läuft KEDA, ein Open-Source-Autoscaler. Bei einer Queue-Regel fragt KEDA alle 30 Sekunden die Zahl der wartenden Nachrichten ab und rechnet desiredReplicas = ceil(Nachrichten ÷ queueLength), zwischen Ihrem Minimum und Maximum. Stellen Sie sich eine Poststelle bei Nacht vor: Die Schreibtische sind die Replicas, das Postfach ist die Queue, und KEDA ist der Inspektor, der die Briefe zählt.

Die Regel, die ich hinzugefügt habe, mit der eigenen systemzugewiesenen Managed Identity der App (ohne Connection String):

az containerapp update -n queue-worker -g rg-demo \
--scale-rule-name queue-scaling \
--scale-rule-type azure-queue \
--scale-rule-metadata accountName=mystorage \
queueName=orders queueLength=10 \
--scale-rule-identity system

Dasselbe im Template, so wie Container Apps es gespeichert hat:

scale:
minReplicas: 0
maxReplicas: 10
rules:
- name: queue-scaling
azureQueue:
accountName: mystorage
queueName: orders
queueLength: 10
identity: system

Der Befehl aus der Doku schlug bei mir fehl. Azure CLI 2.88.0 antwortete mit unrecognized arguments: --scale-rule-identity system: Dieses Flag kommt aus der CLI-Erweiterung containerapp (az extension add -n containerapp), die noch im Preview ist. Mit der Erweiterung (1.3.0b5) ging die Regel durch.

Ausweis und Schlüssel

Mit Regel und aktiver Identität sahen die nächsten vier Minuten genauso aus wie die ersten fünfzehn: ein Replica, 200 Nachrichten, kein Fehler an der App.

Die Scale-Regel authentifiziert sich mit der Identität der App. Die Identität ist der Ausweis, eine Rolle ist der Schlüssel. Ohne Rolle auf dem Storage Account bleibt die Tür zu: KEDA kann die Länge der Queue nicht lesen und skaliert nichts. Der Worker hatte dasselbe Problem. Sein eigenes Log meldete alle zehn Sekunden:

error: HttpResponseError: This request is not authorized to perform this operation using this permission.

Die Lösung war eine einzige Rollenzuweisung für die Identität der App auf dem Storage Account:

principal=$(az containerapp show -n queue-worker -g rg-demo --query identity.principalId -o tsv)
storageId=$(az storage account show -n mystorage -g rg-demo --query id -o tsv)
az role assignment create --assignee-object-id $principal \
--assignee-principal-type ServicePrincipal \
--role "Storage Queue Data Contributor" --scope $storageId

Storage Queue Data Contributor erlaubt dem Worker, Nachrichten zu lesen und zu löschen, und ließ den Scaler sie zählen. 93 Sekunden nach der Rollenzuweisung war die erste Nachricht weg, 13 Sekunden später liefen zehn Replicas.

Zehn Replicas, dann keins

Ein ReplicaZehn Replicas (mein Durchlauf)
200 Nachrichten × 5 s Arbeitetwa 17 Minutenetwa 2 Minuten nach der ersten Nachricht
Nachrichten pro Replica20019 bis 26

Die Queue war 223 Sekunden nach der Rollenzuweisung leer. Alle zehn Replicas blieben noch fünf Minuten: KEDA wartet nach einer leeren Queue fünf Minuten, bevor es auf Ihr Minimum herunterskaliert, hier null. Dann waren sie innerhalb von 40 Sekunden weg: 10 → 9 → 6 → 3 → 0.

Der letzte Test: bei null Replicas drei Nachrichten, die Queue Length weiterhin zehn pro Replica. 41 Sekunden später startete ein Replica, 25 Sekunden danach waren die drei Nachrichten erledigt. Die Queue Length legt fest, wie viele Nachrichten ein Replica übernehmen soll, nicht wann das erste startet. Die Standard-Aktivierungsschwelle von KEDA für die Queue ist null: Jede Nachricht weckt die App.

Wo der eigentliche Fehler steckte

Die App selbst zeigte nichts: läuft, kein Fehler. Der Live-Stream des System-Logs (az containerapp logs show --type system) meldete nur:

unable to get external metric azure-queue-orders for queue-worker: … encountered error

Kein Wort über Berechtigungen. Den vollständigen Eintrag hatte der Log-Analytics-Workspace hinter der Container-Apps-Umgebung, in der Tabelle ContainerAppSystemLogs_CL:

KEDAScalerFailed: GET https://mystorage.queue.core.windows.net/orders
RESPONSE 403: 403 This request is not authorized to perform this operation using this permission.
ERROR CODE: AuthorizationPermissionMismatch

Wenn ein Queue-Worker nicht skaliert: Fragen Sie die System-Logs in Log Analytics ab und prüfen Sie zuerst die Rolle.

Was passierte, als ich es ausprobiert habe

Getestet am 20. Oktober 2026 in meinem eigenen Azure-Abonnement (westeurope, Azure CLI 2.88.0, containerapp-Erweiterung 1.3.0b5); danach wurde alles gelöscht. Der Worker ist meine eigene Python-App (azure-storage-queue mit ManagedIdentityCredential): Sie holt eine Nachricht, arbeitet 5 Sekunden daran und löscht sie. qwatch.py, ebenfalls von mir, hat die Nachrichten gesendet und Replica-Zahl und Queue-Länge abgefragt. Das Anlegen der Umgebung dauerte 17,6 Minuten, der Cloud-Build 68 Sekunden, die App 41 Sekunden.

Der erste Versuch, ohne Ingress und ohne Regel:

> az containerapp show -n queue-worker -g rg-demo --query "{ingress:properties.configuration.ingress, scale:properties.template.scale}" -o json
{
"ingress": null,
"scale": {
"cooldownPeriod": 300,
"maxReplicas": 10,
"minReplicas": 0,
"pollingInterval": 30,
"rules": null
}
}

Weitere zehn Minuten davon (nach den ersten drei):

3 s replicas 1 queue 200
302 s replicas 1 queue 200
602 s replicas 1 queue 200
summary: first replica: 3 s | peak: 1 replicas at 3 s | queue empty: no | 0 replicas: not reached

Nach der Rolle: das Hochskalieren, das Abarbeiten und das Herunterskalieren:

80 s replicas 1 queue 200
93 s replicas 1 queue 199
106 s replicas 10 queue 196
223 s replicas 10 queue 0
482 s replicas 10 queue 0
494 s replicas 9 queue 0
507 s replicas 6 queue 0
520 s replicas 3 queue 0
533 s replicas 0 queue 0

Drei Nachrichten bei null:

28 s replicas 0 queue 3
41 s replicas 1 queue 3
66 s replicas 1 queue 0

Mein Watcher fragte etwa alle 13 Sekunden ab, der Sprung von 1 auf 10 Replicas lag also irgendwo zwischen 93 und 106 Sekunden; die Doku beschreibt Scale-up-Schritte von 1, 4, 8, 16 pro Auswertung, die meine Abfragen nicht auflösen konnten. Noch ein Detail: Eine CPU-Regel mit Minimum null nahm die API an (az containerapp update zeigte die Regel in der Antwort mit leeren Metadaten, dieselbe Anzeige-Eigenheit wie bei der HTTP-Regel im vorigen Video); laut Doku können CPU- und Memory-Regeln nicht auf null skalieren, was dieser Durchlauf nicht weiter getestet hat.

Weitere Scaler im Modul

EreignisquelleTyp der Scale-RegelMetrik pro ReplicaHinweis
Azure Storage Queueazure-queuequeueLengthManaged Identity oder Connection String
Azure Service Busazure-servicebusmessageCount50 Nachrichten ÷ 5 = 10 Replicas; Topics brauchen subscriptionName
Azure Event Hubsazure-eventhubunprocessedEventThresholdmehr Replicas als Partitionen bringen nichts
Apache KafkakafkalagThresholddie Zahl der Partitionen begrenzt sinnvolle Replicas
CroncrondesiredReplicas in einem Zeitfenstermit anderen Regeln gewinnt die höchste Zahl

Weiterführende Links

Video-Transkript (Englisch)

I put two hundred messages into a queue for a worker on Azure Container Apps. For twenty-two minutes, not one of them was processed. One replica, the running copy of the app, sat there doing nothing. Two fixes later, ten replicas cleared the same queue in about two minutes.

In this video, you'll see how a Container App finds out that messages are waiting, and the two things mine was missing. And at the end, where the real error was hiding.

Why didn't the worker scale on its own? A queue worker has no ingress, no public HTTP endpoint. Nobody calls it. It pulls its work from the queue. So I created it like that. Then I asked Container Apps for the app's ingress and its scale settings. No ingress, a minimum of zero, a maximum of ten, and no scale rule. Then I sent two hundred messages. Fifteen minutes later, how many replicas were running? Still one. The replica it started with. Not ten, and not zero either. My watcher checked the replicas and the queue every thirty seconds, for ten minutes. One replica, two hundred messages, the whole time. Container Apps scales on rules. The default rule counts HTTP requests, and a worker without ingress gets none. Then I asked for the platform's own system log, the log Container Apps writes about scaling, not my app's output. It said it plainly: no triggers defined. Nothing was watching, not the requests and not the queue. It started one replica when I created it, and with nothing counting, nothing ever changed that. Not up to ten, and not down to zero. And that one replica? It didn't process a single message. We'll get to why.

So who counts the messages? A scale rule. Container Apps runs KEDA, an open-source autoscaler, under the hood. A queue rule tells it which queue to watch. Think of a post room at night. The desks are the replicas, the mail tray is the queue, and KEDA is the inspector who counts the letters. Every thirty seconds, KEDA asks the queue how many messages are waiting, and divides by the queue length setting: the messages each replica should take. That's a target per replica, not a threshold. With a queue length of ten, two hundred messages ask for twenty replicas. My maximum was ten, so ten. Here's the rule I added, with one update command. A rule with a name, of the type azure queue. Then the storage account, the queue name, and ten messages per replica. And the app's own managed identity to sign in. No connection string. I ran it, exactly as the docs show it. It failed: unrecognized arguments. That flag needs the containerapp extension for the Azure CLI, still in preview. With the extension installed, the rule went in. This series runs one Azure service at a time, for real. Subscribe to follow the series.

Did the rule fix it? The rule was in, the identity was on, and two hundred messages were waiting. What happened over the next four minutes? Nothing. My watcher counted the replicas and the queue every fifteen seconds, for four minutes. One replica, two hundred messages, and no error on the app. Back to the post room. The inspector walks to the tray every thirty seconds, counts the letters, and switches on desks. The managed identity is the inspector's badge. But a badge isn't a key. Without a role on the storage account, the door stays locked, and the inspector can't count. The worker had the same problem. I asked Container Apps for the worker's own console log, its last six lines. It said: this request is not authorized to perform this operation using this permission. So I gave the app's identity one role on the storage account: Storage Queue Data Contributor. It lets the worker read and delete messages, and it let the scaler count them. Then I started the watcher again, counting every ten seconds until the app was back at zero. Ninety-three seconds later, the first message was gone. Thirteen seconds after that, ten replicas were running.

How fast did ten replicas clear the queue? Each message took five seconds of work. One replica would need about seventeen minutes for all two hundred. Ten replicas emptied the queue about two minutes after the first message. They shared the work evenly. To check, I counted the processed messages per replica in the app's console logs, across the whole run. Nineteen to twenty-six messages each. Every replica just takes the next message. Then the queue was empty, and all ten stayed for five more minutes. KEDA waits five minutes after the queue is empty before it scales down to your minimum, here zero. My watcher was still counting, every ten seconds. Ten replicas for five minutes. Then they went in forty seconds: nine, six, three, zero. One last test. At zero replicas, I sent three messages. The queue length was still ten per replica. Does a replica start? Yes. Here's that run: the watcher sent the three messages and counted every ten seconds. Forty-one seconds later, one replica was running, and twenty-five seconds after that, the three messages were done. The queue length sets how many messages each replica should take, not when the first one starts. Any message at all wakes the app.

To sum it up. Without a rule, nothing counts your queue, so the replica count doesn't move. A queue rule lets KEDA count the messages every thirty seconds and divide by your target per replica. Any message wakes the app. The rule's identity needs a role on the storage account, or it counts nothing. And when the queue is empty, the replicas stay five more minutes, then go.

And the error from the start. The app itself showed nothing. I asked Container Apps for the live system log stream, the last thirty lines. It only said: unable to get external metric, encountered error. It never said permission. Log Analytics did: the workspace behind the Container Apps environment. I queried its table, Container App System Logs, for this app: the time, the reason and the message, oldest first. There was the full line: KEDA scaler failed. Response four-oh-three, authorization permission mismatch. So when a queue worker doesn't scale, query that table, and check the role before anything else.

My worker was running and doing nothing. The video on screen now shows an app that says healthy while it's broken. If this was useful, subscribe for more practical .NET and Azure.

Brauchen Sie das in Ihrer eigenen Azure-Umgebung?

Ich entwickle, migriere und betreibe .NET-Plattformen auf Azure, mit Azure DevOps und Terraform. Wenn Ihr Team das sauber aufsetzen möchte, sehen Sie sich mein Angebot an.

Zu den Leistungen