Cloud-Native & DevOps · Sep 2026 · 22 min read
Deploying Cloud-Native Apps with Azure Container Apps
A granular build: a secure, identity-based connection to Azure Container Registry, KEDA-based autoscaling rules, continuous deployment via Azure Pipelines, and safe revision management for zero-downtime rollouts.
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