Deploy Azure Container Apps with managed identity and Key Vault secrets
For developers deploying a containerised app to Azure Container Apps and pulling secrets from Azure Key Vault without hardcoding credentials. You will create the app, attach a managed identity, grant Key Vault access, wire secret references, and verify the container starts and serves traffic end to end.
TL;DR — You can deploy to Azure Container Apps without storing secrets in the app config by attaching a managed identity, granting it
Key Vault Secrets Useron the vault, and defining ACA secrets as Key Vault references. The most common failure is using the wrong identity on the secret reference or forgetting to grant data-plane access on the vault. Reading time: ~5 min
Goal
When you finish, an Azure Container App will be running your container image, reachable over its HTTPS ingress URL, and its runtime secrets will be loaded from Azure Key Vault via a managed identity instead of being stored directly in the app definition.
Prerequisites
- Azure subscription with permission to create resource groups, managed identities, Container Apps, and role assignments; check with
az account show - Azure CLI
>= 2.60; check withaz version - Container Apps extension installed; check with
az extension show -n containerapp - Docker image already pushed to a registry Azure Container Apps can pull from, for example
myregistry.azurecr.io/myapp:2026-08-10 - One Azure region that supports Container Apps and Log Analytics, for example
westeurope - App port your container listens on, for example
8080 - Secret names and values already created in Key Vault, for example
db-passwordandapi-key - If using ACR, permission to pull from that registry, or admin credentials for a quick test
Steps
Step 1: Set shell variables
SUBSCRIPTION_ID="<your-subscription-id>"
LOCATION="westeurope"
RG="rg-aca-kv-demo"
ENV="aca-env-demo"
APP="myapp-demo"
KV="kvacademo2026"
UAMI="uami-aca-kv-demo"
LOGS="log-aca-kv-demo"
IMAGE="myregistry.azurecr.io/myapp:2026-08-10"
TARGET_PORT="8080"
az account set --subscription "$SUBSCRIPTION_ID"
You should see no output and echo $? should return 0.
Step 2: Create the resource group, Log Analytics workspace, and Container Apps environment
az group create -n "$RG" -l "$LOCATION"
az monitor log-analytics workspace create \
-g "$RG" \
-n "$LOGS" \
-l "$LOCATION"
LAW_ID=$(az monitor log-analytics workspace show -g "$RG" -n "$LOGS" --query customerId -o tsv)
LAW_KEY=$(az monitor log-analytics workspace get-shared-keys -g "$RG" -n "$LOGS" --query primarySharedKey -o tsv)
az containerapp env create \
-g "$RG" \
-n "$ENV" \
-l "$LOCATION" \
--logs-workspace-id "$LAW_ID" \
--logs-workspace-key "$LAW_KEY"
You should see JSON output ending with provisioning state Succeeded for the environment.
Step 3: Create the Key Vault and add secrets
az keyvault create \
-g "$RG" \
-n "$KV" \
-l "$LOCATION" \
--enable-rbac-authorization true
az keyvault secret set --vault-name "$KV" --name "db-password" --value "supersecret-demo-password"
az keyvault secret set --vault-name "$KV" --name "api-key" --value "demo-api-key-123"
You should see each secret command return an id like https://<vault>.vault.azure.net/secrets/db-password/<version>.
Step 4: Create a user-assigned managed identity
az identity create -g "$RG" -n "$UAMI"
UAMI_ID=$(az identity show -g "$RG" -n "$UAMI" --query id -o tsv)
UAMI_CLIENT_ID=$(az identity show -g "$RG" -n "$UAMI" --query clientId -o tsv)
UAMI_PRINCIPAL_ID=$(az identity show -g "$RG" -n "$UAMI" --query principalId -o tsv)
KV_ID=$(az keyvault show -g "$RG" -n "$KV" --query id -o tsv)
You should see the identity created and the variables populated; echo "$UAMI_CLIENT_ID" should print a GUID.
Step 5: Grant the managed identity access to read Key Vault secrets
az role assignment create \
--assignee-object-id "$UAMI_PRINCIPAL_ID" \
--assignee-principal-type ServicePrincipal \
--role "Key Vault Secrets User" \
--scope "$KV_ID"
You should see a role assignment JSON object. If Azure RBAC propagation is slow, this step still succeeds but secret resolution may fail for a few minutes.
Step 6: Create the Container App with the managed identity attached
If your image is in ACR and you want the quickest path, use registry username/password once for image pull. Replace the placeholders with your registry values.
az containerapp create \
-g "$RG" \
-n "$APP" \
--environment "$ENV" \
--image "$IMAGE" \
--target-port "$TARGET_PORT" \
--ingress external \
--min-replicas 1 \
--max-replicas 1 \
--user-assigned "$UAMI_ID" \
--registry-server "myregistry.azurecr.io" \
--registry-username "<registry-username>" \
--registry-password "<registry-password>"
You should see JSON output with properties.configuration.ingress.fqdn set to a *.azurecontainerapps.io hostname.
Step 7: Add Key Vault secret references to the Container App
This stores references in ACA, not the secret values. The identityref must be the full resource ID of the user-assigned identity from Step 4.
az containerapp secret set \
-g "$RG" \
-n "$APP" \
--secrets \
db-password="keyvaultref:https://$KV.vault.azure.net/secrets/db-password,identityref:$UAMI_ID" \
api-key="keyvaultref:https://$KV.vault.azure.net/secrets/api-key,identityref:$UAMI_ID"
You should see the app update complete without printing the secret values.
Step 8: Map the secrets into environment variables and restart the revision
az containerapp update \
-g "$RG" \
-n "$APP" \
--set-env-vars \
DB_PASSWORD=secretref:db-password \
API_KEY=secretref:api-key
You should see a new revision created. properties.latestRevisionName changes.
Step 9: Get the app URL and inspect logs if startup fails
APP_FQDN=$(az containerapp show -g "$RG" -n "$APP" --query properties.configuration.ingress.fqdn -o tsv)
echo "https://$APP_FQDN"
az containerapp logs show \
-g "$RG" \
-n "$APP" \
--follow false \
--tail 100
You should see either your app startup logs or a clear secret/identity error. A healthy app usually logs its HTTP listener binding, for example Listening on :8080.
Verify it works
Run the app URL and check both HTTP status and app behavior.
curl -i "https://$APP_FQDN/"
Expected shape:
HTTP/2 200
content-type: text/html; charset=utf-8
server: envoy
If your app exposes a health endpoint, test that too:
curl -i "https://$APP_FQDN/health"
Expected shape:
HTTP/2 200
content-type: application/json
To prove the secret reference resolved, inspect the revision and logs:
az containerapp revision list -g "$RG" -n "$APP" -o table
az containerapp logs show -g "$RG" -n "$APP" --tail 50
Expected result: one active revision in Running state and no errors mentioning Key Vault, MSI, or secret resolution.
Common pitfalls
Using the wrong identity in identityref
Mistake: passing the managed identity client ID or name instead of the full resource ID in identityref.
Symptom: the app updates, but startup logs contain errors similar to Failed to resolve secret or Managed Identity not found.
Fix: rerun az containerapp secret set with identityref:$UAMI_ID, where UAMI_ID came from az identity show --query id -o tsv.
Granting management-plane access but not Key Vault data-plane access
Mistake: assigning Owner/Contributor on the resource group and assuming that allows reading secrets.
Symptom: logs show 403 Forbidden from https://<vault>.vault.azure.net/ during secret resolution.
Fix: assign Key Vault Secrets User on the vault scope to the managed identity principal ID, then wait 1-5 minutes and restart the app revision.
Forgetting --enable-rbac-authorization true on a new vault
Mistake: creating a vault expecting Azure RBAC, but the vault is still using access policies.
Symptom: the role assignment exists, but secret reads still fail with 403.
Fix: create the vault with --enable-rbac-authorization true, or switch the vault permission model to Azure RBAC before testing again.
Container listens on the wrong port
Mistake: setting --target-port 8080 while the container actually listens on 3000 or only on 127.0.0.1.
Symptom: curl -i https://<fqdn>/ returns HTTP/2 502 or HTTP/2 503, and logs show the app started but ingress cannot reach it.
Fix: redeploy or update the app with the correct --target-port, and bind the app to 0.0.0.0 inside the container.
RBAC propagation impatience
Mistake: testing immediately after az role assignment create.
Symptom: first revision fails with Key Vault 403, then later retries start working without any config change.
Fix: wait 2-10 minutes, then force a new revision with az containerapp update -g "$RG" -n "$APP" --set-env-vars FORCE_REDEPLOY=$(date +%s).
Secret URI typo or version confusion
Mistake: referencing https://<vault>.vault.azure.net/secret/... instead of /secrets/..., or pinning an old version unintentionally.
Symptom: logs show Secret not found or the app reads an outdated value after rotation.
Fix: use https://$KV.vault.azure.net/secrets/<name> to follow the latest version, or include the exact version only when you intentionally want a pinned secret.
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI