Background
Sections
IntroductionFoundations1. Resource Hierarchy2. Resource Manager3. Identity and RBAC4. Regions and Availability5. Naming and TaggingVirtual Machines1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetVirtual Network1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetBlob Storage1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure SQL Database1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Kubernetes Service1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Container Registry1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetMicrosoft Entra ID1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure RBAC1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Functions1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAPI Management1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure App Configuration1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Machine Learning1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure Monitor1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and CheatsheetAzure AI Foundry1. What and Why2. Core Concepts3. Architecture4. Getting Started5. Deployment6. Integrations7. Production8. Interview Questions9. Glossary and Cheatsheet

3. Architecture

12 min read

This page makes the invisible machinery visible: what actually crosses the wire during a push and a pull, how the Entra token exchange works, where the control-plane/data-plane line falls, how geo-replication behaves when it's behind, and what breaks.

The shape of the thing

ACR is three loosely-coupled systems wearing one name:

  1. A control-plane resource in ARM — the registry's SKU, network rules, policies, identity config, replications, tasks. Reached at management.azure.com, governed by Azure RBAC roles like Owner and Contributor.
  2. A data-plane endpoint at <name>.azurecr.io — an OCI Distribution API implementation. This is what docker, containerd, helm, and oras talk to. Governed by AcrPull / AcrPush / AcrDelete, and by tokens.
  3. A blob backend — Microsoft-managed storage in the registry's region, invisible to you, holding the deduplicated layer blobs. By default the data plane redirects clients to it.

[Image Prompt: 2D minimalistic architecture diagram of Azure Container Registry showing an ARM control plane governing the registry resource on one side and an azurecr.io data plane endpoint serving manifests and redirecting to Microsoft-managed blob storage on the other, with separate RBAC role sets labelled on each side, flat design, clean vector art style, white background]

Authentication: the Entra token exchange

This is the part with no ECR analogue, and it's what az acr login is actually doing.

  1. You authenticate to Microsoft Entra ID — interactively, or as a service principal, or as a managed identity (the identity endpoint on the VM/pod/app hands out a token with no secret involved). You now hold an Entra access token for the ARM audience.
  2. You exchange it at the registry. The client POSTs that token to https://<registry>.azurecr.io/oauth2/exchange. ACR validates it against Entra, checks your Azure RBAC assignments on the registry resource, and returns an ACR refresh token — scoped to this one registry, and comparatively short-lived (hours ⚠️ verify current lifetime).
  3. az acr login writes that refresh token into your Docker config under the login server, with the sentinel username 00000000-0000-0000-0000-000000000000. That's why your ~/.docker/config.json entry for ACR eventually stops working while a Docker Hub one wouldn't: it's a token, not a password, and it is supposed to expire.
  4. Per-operation, the client gets an access token. Before each pull or push, the client hits /oauth2/token with the refresh token and a scope string like repository:team-a/api:pull. ACR issues a narrowly-scoped access token, and the client presents it as a bearer token on the actual API calls.

Two consequences worth internalising:

  • Authorization is evaluated at token-issue time, per scope. Revoking a role assignment does not kill tokens already issued; the window is the token lifetime. Plan incident response around that, not around instant revocation.
  • The pod pulling your image never had a password. With AKS attached to ACR, the kubelet uses the cluster's kubelet identity to do exactly this exchange. That's the whole "no imagePullSecret" story — see Integrations.

The alternatives, in descending order of how much you should want them: managed identity → service principal → repository-scoped token (a password, but narrowly scoped and expiring) → admin user (a shared password with full access, readable by anyone with Contributor).

[Image Prompt: 2D minimalistic numbered sequence diagram of the Azure Container Registry authentication exchange showing a managed identity obtaining an Entra token, exchanging it at the registry oauth2 endpoint for a refresh token, requesting a scoped access token per repository operation, and finally pulling a manifest, flat design, clean vector art style, white background]

The data path: what a push actually does

Trace docker push myregistry.azurecr.io/api:v1.2:

  1. Client asks the registry which layers it already has. For each layer digest, a HEAD /v2/api/blobs/sha256:…. Existing blobs return 200 and are skipped entirely — this is layer deduplication, and it's registry-wide, so a layer another team pushed last month counts.
  2. Missing layers are uploaded. A POST starts an upload session, the bytes go up (chunked or monolithic), and a PUT with the digest finalises it. The registry verifies the SHA-256 — a corrupted upload is rejected, not stored.
  3. The config blob is uploaded the same way.
  4. The manifest is PUT at /v2/api/manifests/v1.2. Only now does the image exist as a unit. The registry computes the manifest digest and returns it in the Docker-Content-Digest header. Capture that value in CI — it is the only unambiguous name for what you just built.
  5. Tag semantics apply. If v1.2 already pointed somewhere and immutability is off, it now points here and the old manifest becomes untagged but still stored and still billed. If immutability is on, step 4 fails with a 409-shaped error — which is the correct, desirable behaviour and the reason to turn it on.
  6. Side effects fire. Webhooks POST. Event Grid emits ImagePushed. Defender for Cloud scanning, if enabled, picks the image up. Replication to other regions begins. Quarantine, if enabled, holds the image unpullable until a scanner clears it.

A pull is the mirror image, and simpler: GET the manifest (by tag or digest), then GET each missing layer. The layer GET typically returns a 307 redirect to the Microsoft-managed blob storage endpoint, and the client follows it. That redirect is why naive egress firewalls break ACR pulls: allowing *.azurecr.io isn't enough, because the bytes come from a storage host. Dedicated data endpoints (Premium) exist precisely to make that traffic allow-listable as <registry>.<region>.data.azurecr.io.

Control plane vs. data plane — where the line falls

Azure's split is sharper than AWS's and ACR is the textbook case.

Operation Plane Endpoint Governed by
Create/delete the registry, change SKU, set network rules, add a replication Control management.azure.com Owner / Contributor
See the registry exists, read its properties Control management.azure.com Reader
docker pull, helm pull, resolve a manifest Data <name>.azurecr.io AcrPull
docker push Data <name>.azurecr.io AcrPush
az acr repository delete Data <name>.azurecr.io AcrDelete
az acr repository list Data (via CLI convenience) <name>.azurecr.io AcrPullnot Reader
az acr credential show (read admin password) Control management.azure.com Contributor

The classic mistake is assigning Reader to a service account and expecting it to pull. It cannot. The mirror mistake is assuming Owner implies data access — it doesn't directly, though Owner/Contributor can grant themselves AcrPull, or read the admin password if the admin user is enabled. That escalation path is the strongest argument for keeping the admin user disabled and enforcing it with Azure Policy.

Three more line-crossings worth naming:

  • Network rules are control-plane settings that govern the data plane. Setting public_network_access_enabled = false doesn't touch your role assignments; it makes the data endpoint unreachable from anywhere but a private endpoint. A pipeline with perfect RBAC will still fail, with a DNS or timeout error rather than a 403.
  • Portal browsing uses the data plane. "Repositories" in the portal blade calls <name>.azurecr.io. On a Premium registry with public access disabled and no private endpoint reaching your browser, the blade shows an error even for an Owner. This looks like a bug and is not one.
  • ACR Tasks straddles both. Creating a task is control plane; the task itself pushes over the data plane using the task's identity.

[Image Prompt: 2D minimalistic layered diagram showing Azure Resource Manager governing the container registry resource on the control plane and the azurecr.io endpoint governing images on the data plane, with Reader, Contributor, AcrPull, and AcrPush roles placed on the correct side, and the admin-user credential shown as a bypass path, flat design, clean vector art style, white background]

Storage, deduplication, and why "delete" doesn't free space

Layers are stored once per registry per digest, referenced by manifests. This has three consequences that explain most storage surprises:

  • Deleting a tag frees nothing. It only removes a pointer. The manifest becomes untagged and keeps every layer alive.
  • Deleting a manifest frees only its unreferenced layers. Anything shared with another image stays.
  • Untagged manifests accumulate silently. A CI pipeline that pushes :latest on every merge produces one orphaned manifest per merge, forever. This is the single most common cause of an ACR storage bill nobody can explain. The fix is the retention policy for untagged manifests ⚠️ verify tier availability, or a scheduled az acr manifest list-metadata sweep.

Encryption at rest is on by default with Microsoft-managed keys; customer-managed keys (Premium) route it through a Key Vault key, which must be configured at registry creation with a user-assigned managed identity — you cannot retrofit CMK onto an existing registry ⚠️ verify current behaviour, as this constraint has moved before. In transit, TLS only.

Geo-replication: one name, many copies, eventual consistency

On Premium, a replication is a regional copy of the registry's content. The mechanics:

  • One login server. myregistry.azurecr.io resolves via Azure Traffic Manager to the nearest healthy replica. Clients need no configuration and no per-region URL.
  • Push once, to whichever replica you reach; ACR syncs the rest asynchronously.
  • Pulls are local, which is the point — lower latency, and no cross-region egress charges on the pull path.
  • Each replica bills as an additional registry at the Premium daily rate ⚠️ verify pricing.

The behaviour that catches people: replication is eventual. A pipeline that pushes in West Europe and immediately deploys in East US can pull a tag that hasn't arrived yet, or — worse — pull the previous manifest that the tag pointed at in that region. Symptom: "the same tag is running two different builds in two regions." Two fixes, both good:

  1. Deploy by digest. A digest that hasn't replicated yet fails loudly with a 404 and your deployment retries; a stale tag fails silently and runs the wrong code.
  2. Gate on replication. Poll the target region (or check the replication status) before the dependent deployment stage runs.

Zone redundancy is a separate, orthogonal Premium setting: it spreads the registry (and each replica you mark) across availability zones within a region, protecting against a zone failure rather than a region failure. It is set per replication and generally must be chosen when the registry or replica is created ⚠️ verify current behaviour.

Scaling model and where the ceilings are

ACR doesn't have "instances" you scale. It has tier-derived limits:

  • Throughput — read and write operations per minute, and bandwidth, both step up with the tier. Exceed them and you get HTTP 429 with a Retry-After header. Well-behaved clients (containerd, recent docker) back off; poorly-behaved CI matrices don't.
  • The realistic throttle scenario is a large cluster scale-out or a node-pool upgrade: hundreds of kubelets pulling the same large image within seconds. Mitigations, in order of effectiveness: smaller images, geo-replication so the load spreads across replicas, and — for AKS specifically — node image caching and staggered rollouts.
  • Storage is soft: you're billed past the included allowance rather than blocked, up to a very large per-registry ceiling ⚠️ verify current maximum.
  • Scope of the limits is per registry, not per subscription — a genuinely simpler story than most Azure services. The counts that are per-subscription-per-region are the ARM-side ones (number of registries, ACR Tasks concurrency) ⚠️ verify.

Failure modes

Symptom Usual cause What to do
unauthorized: authentication required on pull No AcrPull, or the token expired, or Reader was assigned instead Check the data-plane role assignment; re-run az acr login
ImagePullBackOff in AKS with 401 Cluster not attached to the registry / kubelet identity missing AcrPull az aks update --attach-acr, or assign AcrPull to the kubelet identity
ImagePullBackOff with a DNS or timeout error Private endpoint without the Private DNS zone link, or public access disabled Fix privatelink.azurecr.io zone linkage; verify from a node with nslookup
Pull works, layers hang Egress firewall allows *.azurecr.io but not the storage redirect Enable dedicated data endpoints and allow-list them
429 Too Many Requests Throughput limit for the tier Raise tier, geo-replicate, shrink images, stagger rollouts
Tag runs different code in two regions Replication lag plus tag-based deployment Deploy by digest
Storage bill growing with no new images Untagged manifests from repeated tag pushes Retention policy for untagged manifests
Cannot delete the registry with Terraform Resource lock, or a private endpoint / replication still attached Check CanNotDelete locks and child resources
Portal "Repositories" blade errors for an Owner Data plane unreachable from the browser (public access off) Expected; browse from inside the network or use a jump host

The trade-offs, stated plainly

  • Geo-replication buys latency and resilience, and costs a full registry per region plus eventual consistency. Use it when you deploy in multiple regions; don't turn it on "for safety" in a single-region architecture.
  • Private endpoints buy a real boundary and cost you DNS complexity and portal usability. Every private-endpoint incident in ACR is a DNS incident.
  • Tag immutability buys reproducibility and costs your team the :latest habit. Worth it.
  • Premium buys features, not just headroom. Sizing a registry by storage is the wrong axis; size it by whether you need private networking, replication, CMK, or scope maps.

Next: Getting Started →

← Back to the Azure Container Registry overview · ← Previous: Core Concepts