Push to ACR with a managed-identity pull, deploy to Container Apps, then watch scale-to-zero wake on traffic.
Run one step, then check it
Read the step before copying. Bash commands belong in Git Bash on Windows or Azure Cloud Shell set to Bash; PowerShell uses different syntax. A JSON, YAML, Bicep or application-code example belongs in the named file, not directly at the terminal prompt. Follow any shell-specific instructions below.
Keep the same terminal open so earlier variables remain available. Copy one complete command block without the terminal's $ prompt. A backslash continues a command onto the next line. If Bash shows a lone >, it is waiting for the rest of a quote, command or file block. Finish the complete block; if the paste was incomplete, press Ctrl+C and copy it again. Do not paste the next step into an unfinished command.
A line such as RG="training-group" stores a value without printing it. Output captured with $(...) is also stored; a blank screen alone does not prove failure. A sleep deliberately waits, and -o none suppresses normal output. Wait for your normal prompt, then run the step's checkpoint. After an error, stop and keep the exact command and message; later steps cannot repair a missing resource. Never share passwords, access tokens or secret values in a screenshot.
The 90% of container work that is not AKS
Job descriptions love the word Kubernetes, and AKS is a real skill — but most container workloads are a web service or an API that needs to run, scale with traffic, and cost nothing when nobody is calling it. Reaching for a full AKS cluster there means paying for control-plane nodes and owning cluster upgrades for a job a managed platform does better. Azure Container Apps is that platform: you hand it an image, it gives you HTTPS, autoscaling, and — the detail that makes it lovable for side projects and real services alike — scale-to-zero, so an idle app runs no replicas and bills accordingly. This lab runs a container the professional way: the image lives in your own Azure Container Registry, and the app pulls it using a managed identity with the AcrPull role, so there is no registry admin password to leak.
You will create a registry with its admin account switched off, import an image, grant a managed identity pull rights, deploy the app scaled to zero, and confirm it serves traffic — all without a single stored credential.
Scale-to-zero is the feature that makes "always deployed" and "costs nothing idle" the same sentence.
You need a free Azure account, the Azure CLI (az), then be signed in with az login. First time? The 15-minute Set up your machine page covers the account, the installs (winget / brew / apt), and sign-in. Prefer zero installs? Run everything in Azure Cloud Shell (Bash), preinstalled and already signed in. You also need permission to assign roles — Owner or User Access Administrator on the subscription — for the az role assignment create command in Step 1; a plain Contributor gets AuthorizationFailed there. On your own subscription you are Owner, which covers it.
Azure Cloud Shell (Bash); source in github.com/kloudcaptain/campux-labs under lab-container-apps-acr. A Basic registry is a few pennies a day and the Container App scales to zero, so the lab is effectively free if torn down. This lab does provision a registry, so run the teardown.
A registry with no admin password
Create the registry with the admin account disabled — the whole point is that access comes from identity, not a shared password — and import a ready-made sample image so there is something to run.
Commands inside $(...) save their output in a variable. Check the printed identifier or the checkpoint before continuing.
# Windows/Git Bash: stop it mangling /subscriptions/... arguments (harmless on macOS/Linux) export MSYS_NO_PATHCONV=1 RG="campux-lab-aca-rg" az group create -n "$RG" -l eastus ACR="campuxacr$RANDOM" az acr create -n "$ACR" -g "$RG" --sku Basic --admin-enabled false # import Microsoft's Container Apps sample image into your registry az acr import -n "$ACR" \ --source mcr.microsoft.com/k8se/quickstart:latest \ --image quickstart:v1 ACR_ID=$(az acr show -n "$ACR" -g "$RG" --query id -o tsv)
adminUserEnabled: false (confirm with az acr show -n "$ACR" -g "$RG" --query adminUserEnabled) and holds quickstart:v1. There is deliberately no password to copy — access must come from an identity, which you create next.An identity that may pull
Create a user-assigned managed identity and grant it exactly one right on the registry: AcrPull. Least privilege — it can pull images and do nothing else.
Commands inside $(...) save their output in a variable. Check the printed identifier or the checkpoint before continuing.
az identity create -n campux-aca-id -g "$RG" ID_ID=$(az identity show -n campux-aca-id -g "$RG" --query id -o tsv) ID_PRINCIPAL=$(az identity show -n campux-aca-id -g "$RG" --query principalId -o tsv) az role assignment create \ --assignee-object-id "$ID_PRINCIPAL" \ --assignee-principal-type ServicePrincipal \ --role AcrPull \ --scope "$ACR_ID"
"roleDefinitionName": "AcrPull" scoped to your registry. This identity is now the only thing that needs to be trusted to fetch the image — and it holds no secret you could accidentally commit. (We pass --assignee-object-id with --assignee-principal-type ServicePrincipal rather than --assignee: a brand-new managed identity takes a moment to appear in the Entra graph, and the name-resolving --assignee form would fail with "Cannot find … in graph database" until it does. Passing the object id and type skips that lookup.)Deploy, scaled to zero
Create the Container Apps environment, then the app — telling it to pull from your registry using the managed identity and to scale between zero and three replicas.
Commands inside $(...) save their output in a variable. Check the printed identifier or the checkpoint before continuing.
# if eastus answers "Creating a new cluster is unavailable at this time", delete the failed
# environment (az containerapp env delete -n campux-aca-env -g "$RG" --yes) and use westus2
az containerapp env create -n campux-aca-env -g "$RG" -l eastus
az containerapp create -n campux-quickstart -g "$RG" \
--environment campux-aca-env \
--image "$ACR.azurecr.io/quickstart:v1" \
--registry-server "$ACR.azurecr.io" \
--registry-identity "$ID_ID" \
--user-assigned "$ID_ID" \
--ingress external --target-port 80 \
--min-replicas 0 --max-replicas 3
FQDN=$(az containerapp show -n campux-quickstart -g "$RG" \
--query properties.configuration.ingress.fqdn -o tsv)
echo "https://$FQDN"
Prove it serves — and scales to zero
Call the app, then read back the scale rule that lets it cost nothing idle.
curl -s -o /dev/null -w "app: %{http_code}\n" "https://$FQDN"
az containerapp show -n campux-quickstart -g "$RG" \
--query "properties.template.scale.{min:minReplicas, max:maxReplicas}" -o table
app: 200 — your container, pulled by identity, is serving. The scale query shows min: 0, which means once traffic stops the platform drains it to zero replicas and you pay for none; the next request pays a brief cold start to wake it. That trade — a little latency for near-zero idle cost — is exactly why Container Apps beats a standing AKS cluster for spiky or low-traffic workloads.Tear it down
One resource group holds the registry, identity, environment, and app. Delete it.
az group delete -n campux-lab-aca-rg --yes
az group exists -n campux-lab-aca-rg # -> false
az group exists returns false. The Basic registry was the only thing billing while idle, and it is now gone.What you can now honestly claim
You published a container image to Azure Container Registry, ran it on Azure Container Apps with a user-assigned managed identity for the pull, kept the registry's admin account disabled the whole time, and configured scale-to-zero. That is credential-free container delivery on a managed platform — the pragmatic answer to "experience with containers and orchestration" that most workloads actually call for, and a sober counterpoint you can offer in an interview to "did you just reach for AKS?". When a workload genuinely needs cluster-level control, AKS is there; knowing when it does not is the senior judgement.
- Disabling the ACR admin account and pulling with a managed identity is the recommended production pattern: the admin account is a single shared username and password with full registry rights, exactly the kind of long-lived credential that leaks. An identity with only
AcrPullcan be scoped, audited, and revoked without rotating a password everyone shares. - Scale-to-zero suits APIs and web apps that tolerate an occasional cold start. For latency-sensitive or always-warm workloads set
--min-replicas 1and accept the standing cost; the point of the lab is that the choice is yours per app, not a platform default you cannot change.