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.
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 withaz 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
ImagePullBackOffor unauthorized pulling the image: theAcrPullrole assignment can take up to a minute to propagate. Re-run theaz containerapp create/updatewith--registry-identity system, or just wait and trigger a new revision.- Curl to the FQDN times out or 404s: check
--target-portmatches what your app actually binds inside the container, and that the process listens on0.0.0.0, not127.0.0.1. /secretreturns 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-identityor--scale-rule-*flags: yourcontainerappextension is stale. Runaz extension add --name containerapp --upgradeand try again. Also double-check you're using--scale-rule-metadata key=valuepairs, 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 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
No comments yet
Be the first to weigh in.