Skip to content
Cloud & Infra Intermediate Tutorial

Deploy a Containerized App to Azure Container Apps with Autoscaling and Managed Identity

Ship a Flask app to Azure Container Apps with zero registry passwords, KEDA-based HTTP autoscaling, and a system-assigned managed identity pulling secrets from Key Vault.

Lenn Voss
Lenn Voss
Cloud & Infrastructure Writer · Jul 12, 2026 · 9 min read
Deploy a Containerized App to Azure Container Apps with Autoscaling and Managed Identity

What you'll build

You'll containerize a small Flask app, ship it to Azure Container Apps without ever touching a registry password, wire up KEDA-based HTTP autoscaling, and have the app fetch a secret from Key Vault using a system-assigned managed identity instead of a connection string.

Prerequisites

  • An Azure subscription where you have at least Contributor + User Access Administrator on the resource group (you'll be creating role assignments).
  • Azure CLI 2.60.0 or later. Check with az --version, update with az upgrade.
  • Docker Desktop (or Docker Engine on Linux) if you want to test the image locally. Not required, we'll build in Azure with ACR Tasks.
  • A bash shell (macOS, Linux, or WSL2). PowerShell works too, just adjust the line continuations.

Log in and register what you need (the provider registrations are one-time per subscription):

az login
az account set --subscription "<your-subscription-id>"
az extension add --name containerapp --upgrade
az provider register --namespace Microsoft.App
az provider register --namespace Microsoft.OperationalInsights

1. Create a resource group

RG=rg-aca-demo
LOCATION=eastus
az group create --name $RG --location $LOCATION

2. Containerize a minimal app

app.py:

import os
from flask import Flask
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient

app = Flask(__name__)

@app.route("/")
def index():
    return "Hello from Azure Container Apps"

@app.route("/secret")
def secret():
    vault = os.environ.get("KEY_VAULT_NAME")
    if not vault:
        return "KEY_VAULT_NAME not set", 500
    client = SecretClient(
        vault_url=f"https://{vault}.vault.azure.net/",
        credential=DefaultAzureCredential(),
    )
    return client.get_secret("my-secret").value

requirements.txt:

flask
gunicorn
azure-identity
azure-keyvault-secrets

Dockerfile:

FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 8080
CMD ["gunicorn", "--bind", "0.0.0.0:8080", "app:app"]

3. Build and push with ACR Tasks

Registry names must be globally unique, lowercase, alphanumeric only.

ACR=acrdemo$RANDOM
az acr create --resource-group $RG --name $ACR --sku Basic
az acr build --registry $ACR --image hello-app:v1 .

az acr build uploads your build context, builds the image inside ACR, and pushes the result, no local Docker daemon involved.

4. Create the Container Apps environment

az containerapp env create \
  --name aca-env-demo \
  --resource-group $RG \
  --location $LOCATION

This provisions the environment (plus a Log Analytics workspace for logs) that your app runs in. One environment can host multiple container apps.

5. Deploy with a system-assigned identity

az containerapp create \
  --name hello-app \
  --resource-group $RG \
  --environment aca-env-demo \
  --image "$ACR.azurecr.io/hello-app:v1" \
  --target-port 8080 \
  --ingress external \
  --min-replicas 1 \
  --max-replicas 10 \
  --system-assigned \
  --registry-server "$ACR.azurecr.io" \
  --registry-identity system

--registry-identity system tells the CLI to create the system-assigned identity, grant it AcrPull on the registry, and configure the app to authenticate that way at pull time. No --registry-username, no --registry-password, nothing sitting in your app config that could leak.

6. Tune autoscaling

Container Apps scales via KEDA under the hood. Every app already gets a default HTTP scaler (roughly 10 concurrent requests per replica before scaling out). Override it with an explicit rule:

az containerapp update \
  --name hello-app \
  --resource-group $RG \
  --min-replicas 1 \
  --max-replicas 10 \
  --scale-rule-name http-rule \
  --scale-rule-type http \
  --scale-rule-metadata concurrentRequests=50

Note the flag: it's --scale-rule-metadata, not --scale-rule-http-concurrency. There's no dedicated concurrency flag; every scaler (http, cpu, memory, custom KEDA scalers) takes its tuning knobs as key=value pairs through --scale-rule-metadata. For the HTTP scaler, concurrentRequests is the key that sets the per-replica threshold before KEDA adds another replica.

Scale rule type Good for Notes
http Request-driven APIs --scale-rule-metadata concurrentRequests=<n> sets concurrent requests per replica
cpu / memory Compute-bound workloads Set via --scale-rule-metadata type=Utilization value=<percent>
azure-queue Queue consumers Scales on queue length, needs an auth secret reference
Other KEDA scalers Service Bus, Event Hubs, cron, Kafka, etc Same --scale-rule-type mechanism, different metadata keys

Setting --min-replicas 0 scales to zero when idle, cheaper, but expect a cold start of a few seconds on the next request.

7. Grant the identity access to Key Vault

KV=kv-aca-demo$RANDOM
az keyvault create --name $KV --resource-group $RG --location $LOCATION --enable-rbac-authorization true
az keyvault secret set --vault-name $KV --name my-secret --value "correct horse battery staple"

PRINCIPAL_ID=$(az containerapp show --name hello-app --resource-group $RG --query identity.principalId -o tsv)
KV_ID=$(az keyvault show --name $KV --resource-group $RG --query id -o tsv)

az role assignment create \
  --assignee "$PRINCIPAL_ID" \
  --role "Key Vault Secrets User" \
  --scope "$KV_ID"

az containerapp update --name hello-app --resource-group $RG --set-env-vars KEY_VAULT_NAME=$KV

DefaultAzureCredential picks up the managed identity endpoint automatically once the app is running inside Container Apps. No client secret, no connection string, nothing to rotate.

Verify it works

FQDN=$(az containerapp show --name hello-app --resource-group $RG --query properties.configuration.ingress.fqdn -o tsv)
curl https://$FQDN/
curl https://$FQDN/secret

The first call returns the greeting; the second returns the secret value, confirming the managed identity round-trip worked end to end.

To watch autoscaling react, throw some load at it (hey -z 60s -c 50 https://$FQDN/ works well, install via brew install hey or go install github.com/rakyll/hey@latest), then check replica counts per revision:

az containerapp revision list --name hello-app --resource-group $RG -o table

You should see the replica count climb above 1 as concurrency exceeds the threshold, then settle back down once traffic stops.

Troubleshooting

  • ImagePullBackOff or unauthorized pulling the image: the AcrPull role assignment can take up to a minute to propagate. Re-run the az containerapp create/update with --registry-identity system, or just wait and trigger a new revision.
  • Curl to the FQDN times out or 404s: check --target-port matches what your app actually binds inside the container, and that the process listens on 0.0.0.0, not 127.0.0.1.
  • /secret returns 403 from Key Vault: RBAC role assignments in Key Vault propagate a bit slower than the old access-policy model. Give it a minute, and confirm you scoped the assignment to the vault's resource ID, not the resource group.
  • CLI complains about unknown --registry-identity or --scale-rule-* flags: your containerapp extension is stale. Run az extension add --name containerapp --upgrade and try again. Also double-check you're using --scale-rule-metadata key=value pairs, not a made-up flag like --scale-rule-http-concurrency, that one doesn't exist and the CLI will reject it outright.

Next steps

Look at user-assigned managed identities once multiple container apps need to share one identity across a registry or Key Vault. Layer in Dapr for service-to-service calls and pub/sub, add a custom domain with a managed TLS certificate, and once you're happy with the setup manually, move it into Bicep or Terraform for repeatability. For CI/CD, the azure/container-apps-deploy-action GitHub Action wraps the build-and-deploy steps into one pipeline job.

Lenn Voss
Written by
Lenn Voss · Cloud & Infrastructure Writer

Lenn writes about cloud platforms, Kubernetes internals, and the infrastructure decisions that quietly make or break engineering organizations. Based in Berlin's vibrant tech scene, they have a talent for turning dense platform-engineering topics into prose that people actually finish reading.

Discussion 0

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading