Skip to main content
Qubicweb logo

Qubicweb reading view

Source: Developer

Deploying Cloud-Native Apps with Azure Container Apps

By Developer6 min readPublished 17 September 2026
Browse sourceDeveloper

Internal Read means the full piece stays on Qubicweb, with its source path and context still visible.

Brief points

  • Why Container Apps, and Not Just "Kubernetes but Managed" Azure Container Apps sits in a specific gap: more managed than AKS (no nodes to patch, no cluster to operate), but buil...
  • It's the right target when you want Kubernetes-shaped scaling behavior without operating a control plane.
  • This is a granular build: a secure ACR connection authenticated by managed identity (not a password), autoscaling rules, continuous deployment via Azure Pipelines, and revision...

Why Container Apps, and Not Just "Kubernetes but Managed"

Azure Container Apps sits in a specific gap: more managed than AKS (no nodes to patch, no cluster to operate), but built on the same underlying primitives Kubernetes users already know — KEDA for event-driven autoscaling, Dapr for service-to-service patterns, and the same container image you'd deploy anywhere else. It's the right target when you want Kubernetes-shaped scaling behavior without operating a control plane.

This is a granular build: a secure ACR connection authenticated by managed identity (not a password), autoscaling rules, continuous deployment via Azure Pipelines, and revision management for a rollout that doesn't cause downtime.

Before the how, the what — three terms this build leans on:

  • Container Apps Environment — the boundary a set of container apps share: networking, logging, and (if you use it) Dapr configuration. Apps in the same environment can talk to each other by name; apps in different environments are network-isolated from each other by default. Think of it as roughly analogous to a Kubernetes namespace plus its shared networking, minus you having to run the cluster underneath it.
  • Revision — every deployment to a Container App creates a new, immutable revision. In single-revision mode, traffic cuts over entirely to the newest one; in multiple-revision mode, several revisions can run simultaneously with traffic split between them — which is what makes a canary rollout or an instant rollback possible without redeploying anything.
  • Scale rule — the condition Container Apps evaluates to decide how many replicas to run, built on KEDA. An HTTP scale rule counts concurrent requests; a queue-based rule counts messages waiting — the same "scale on the signal that reflects real demand, not a CPU proxy" principle from the AKS FinOps article, applied here through KEDA instead of the Kubernetes-native autoscaler.

01 — Azure Container Registry: A Secure Connection, Not a Password

az acr create \
  --resource-group rg-containerapps \
  --name acrcontainerappsdemo \
  --sku Standard \
  --admin-enabled false

--admin-enabled false is deliberate — the admin account is a single shared username/password for the whole registry, exactly the kind of long-lived credential the OIDC pattern in the Azure Load Testing article was built to avoid. Container Apps authenticates to ACR with a managed identity instead:

az containerapp identity assign \
  --resource-group rg-containerapps \
  --name my-containerapp \
  --system-assigned

az role assignment create \
  --assignee "$(az containerapp identity show --resource-group rg-containerapps --name my-containerapp --query principalId -o tsv)" \
  --role AcrPull \
  --scope "$(az acr show --resource-group rg-containerapps --name acrcontainerappsdemo --query id -o tsv)"

az containerapp registry set \
  --resource-group rg-containerapps \
  --name my-containerapp \
  --server acrcontainerappsdemo.azurecr.io \
  --identity system

The container app's own system-assigned identity is granted exactly one role (AcrPull) scoped to exactly one registry — it can pull images, nothing else, and there's no credential anywhere to rotate or leak.

02 — Creating the Container App

az containerapp env create \
  --resource-group rg-containerapps \
  --name env-containerapps-prod \
  --location eastus

az containerapp create \
  --resource-group rg-containerapps \
  --name my-containerapp \
  --environment env-containerapps-prod \
  --image acrcontainerappsdemo.azurecr.io/my-app:v1.0 \
  --registry-server acrcontainerappsdemo.azurecr.io \
  --registry-identity system \
  --ingress external \
  --target-port 8080 \
  --cpu 0.5 --memory 1.0Gi \
  --min-replicas 1 --max-replicas 10

--ingress external exposes the app to the internet through Container Apps' built-in load balancer and TLS termination; use internal instead for a backend service other apps in the same environment should reach but the public internet shouldn't.

03 — Autoscaling: HTTP Concurrency and Queue Depth

HTTP-triggered scaling — scale out based on concurrent requests per replica, not CPU:

az containerapp update \
  --resource-group rg-containerapps \
  --name my-containerapp \
  --min-replicas 1 --max-replicas 10 \
  --scale-rule-name http-concurrency-rule \
  --scale-rule-type http \
  --scale-rule-http-concurrency 50

Queue-triggered scaling — scale a background worker based on how many messages are waiting, so idle time costs nothing and a backlog gets processed faster automatically:

az containerapp update \
  --resource-group rg-containerapps \
  --name my-queue-worker \
  --min-replicas 0 --max-replicas 10 \
  --scale-rule-name queue-based-autoscaling \
  --scale-rule-type azure-queue \
  --scale-rule-metadata "accountName=mystorageaccount" "cloud=AzurePublicCloud" "queueLength=5" "queueName=work-items" \
  --scale-rule-auth "connection=queue-connection-secret"

--min-replicas 0 on the queue worker is the detail worth noticing — a worker that scales all the way to zero when the queue is empty costs nothing while idle, and KEDA scales it back up the moment a message arrives.

04 — Continuous Deployment with Azure Pipelines

# azure-pipelines.yml
trigger:
  branches:
    include: [main]

pool:
  vmImage: ubuntu-latest

variables:
  acrName: acrcontainerappsdemo
  imageName: my-app

stages:
  - stage: BuildAndPush
    jobs:
      - job: Build
        steps:
          - task: Docker@2
            inputs:
              containerRegistry: 'acr-service-connection'
              repository: '$(imageName)'
              command: 'buildAndPush'
              Dockerfile: '**/Dockerfile'
              tags: '$(Build.BuildId)'

  - stage: Deploy
    dependsOn: BuildAndPush
    jobs:
      - job: DeployContainerApp
        steps:
          - task: AzureCLI@2
            inputs:
              azureSubscription: 'azure-service-connection'
              scriptType: bash
              scriptLocation: inlineScript
              inlineScript: |
                az containerapp update \
                  --resource-group rg-containerapps \
                  --name my-containerapp \
                  --image $(acrName).azurecr.io/$(imageName):$(Build.BuildId)

Every push to main builds a uniquely-tagged image, pushes it to ACR, then updates the Container App to that exact tag — $(Build.BuildId) as the tag (not latest) means every deployment is traceable back to the exact pipeline run that produced it.

05 — Revisions: Zero-Downtime Rollouts and Instant Rollback

By default, a Container App runs in single-revision mode — a new deployment fully replaces the old one. Switch to multiple-revision mode to control the cutover explicitly:

az containerapp revision set-mode \
  --resource-group rg-containerapps \
  --name my-containerapp \
  --mode Multiple
# List revisions to find the current and previous ones
az containerapp revision list \
  --resource-group rg-containerapps \
  --name my-containerapp \
  --query "[].{Name:name, Active:properties.active, Traffic:properties.trafficWeight, Created:properties.createdTime}" \
  -o table

If a new revision is misbehaving, roll back by shifting traffic to the previous one — no redeploy, no rebuild, just a traffic-weight change:

az containerapp ingress traffic set \
  --resource-group rg-containerapps \
  --name my-containerapp \
  --revision-weight my-containerapp--previous-revision=100 my-containerapp--broken-revision=0

Or copy a known-good older revision forward as a fresh one:

az containerapp revision copy \
  --resource-group rg-containerapps \
  --name my-containerapp \
  --from-revision my-containerapp--previous-revision

This is the same rollback principle as the GitOps article's ArgoCD rollback — "redeploy a previous known-good state" should be a single fast command available under incident pressure, not a rebuild-from-source scramble.

Cost Notes

Container Apps bills per vCPU-second and GiB-second actually consumed, not per provisioned instance-hour — a workload that scales to zero (like the queue worker above) costs nothing while idle, which is a meaningfully different cost model from an always-on VM or an AKS node pool that keeps running whether or not it's doing work.

Closing Thoughts

None of the individual pieces here are exotic — a registry, a scale rule, a pipeline, a revision policy. What makes a Container Apps deployment production-ready is the same discipline as everywhere else in this series: identity-based registry auth instead of a password, scaling on the signal that reflects real demand, and a rollback path that's a command, not a redeploy.

GitHub Repository: azure-container-apps-cicd-lab — the ACR setup, Container App Bicep, Azure Pipelines YAML, and revision-management scripts, ready to run.

Reviewed against current Azure CLI (az containerapp, az acr) as of September 2026.

Azure Container Apps · Azure Container Registry · Azure Pipelines · KEDA · CI/CD · DevOps

Originally published on my portfolio.

Trust

Spot something off?