Warmup Guide — Containers, Docker, ACR & AKS Internals
Zero-to-principal primer for Phase 07. We start from "what is a container image, physically" and build up to the exact mechanisms behind
docker push, ACR deduplication, garbage collection, geo-replication, and the AKS scheduler's decision to leave a podPending. By the end you can answer "why did:latestchange under me?", "why did deleting tags reclaim no storage?", and "why won't this pod schedule?" from first principles — and you will have built both a registry and a scheduler.
Table of Contents
- Chapter 1: What a Container Actually Is
- Chapter 2: The Image Model — Layers, Config, and the Digest
- Chapter 3: Manifests and Tags — Immutable Content, Mutable Names
- Chapter 4: OCI Distribution — Push, Pull, and Deduplication
- Chapter 5: Azure Container Registry — The Managed OCI Registry
- Chapter 6: Garbage Collection — Mark and Sweep from the Tags
- Chapter 7: Geo-Replication — Copy Only What's Missing
- Chapter 8: AKS — The Control Plane and the Scheduler
- Chapter 9: Requests, Limits, QoS, and Why a Pod is Pending
- Chapter 10: The Serverless Container Paths — ACA and ACI
- Lab Walkthrough Guidance
- Success Criteria
- Interview Q&A
- References
Chapter 1: What a Container Actually Is
From zero. A container is not a lightweight VM. A VM virtualizes hardware — it runs a full guest kernel on a hypervisor. A container virtualizes the operating-system view: it is one or more normal Linux processes that the kernel has been told to isolate and limit. Three kernel features do all the work:
- namespaces — isolate what a process can see:
pid(its own process tree),net(its own interfaces),mnt(its own filesystem mounts),uts(hostname),ipc,user. Two containers each think they are PID 1 on their own machine. - cgroups (control groups) — limit and account what a process can use: CPU shares, memory ceiling, I/O. This is the same accounting the AKS scheduler reasons about as requests and limits (Ch. 9).
- a union/overlay filesystem — stack read-only layers with one writable top layer, so many containers share the same underlying image bytes (Ch. 2).
Why it exists. Sharing one kernel makes containers start in milliseconds and pack
hundreds to a host, where VMs start in seconds and pack tens. The cost is weaker isolation
(a shared kernel is a shared attack surface), which is exactly why Azure also offers
Confidential Containers and why multi-tenant clusters lean on user namespaces and
policy.
The principal's framing. A container is a process with a private view and a resource budget, started from an immutable filesystem image. Everything in this phase is about that image (how it's stored and shipped — Ch. 2–7) and that resource budget (how it's scheduled — Ch. 8–9).
Chapter 2: The Image Model — Layers, Config, and the Digest
What it is. A container image is two things:
- an ordered stack of read-only layers, each layer a tarball of filesystem changes
(files added/modified/deleted) contributed by one
Dockerfileinstruction; - a config — a small JSON document with the entrypoint, env, working dir, exposed
ports, and the ordered list of layer digests (the
rootfs.diff_idsandhistory).
Dockerfile image = layers (bottom→top) + config
───────────── ────────────────────────────────────
FROM debian:12 ─────────▶ [ layer0: debian rootfs ] ← shared base
RUN apt-get install ─────────▶ [ layer1: apt packages ]
COPY ./app /app ─────────▶ [ layer2: your app ] ← changes every build
CMD ["/app/run"] ─────────▶ config: { Cmd, Env, diff_ids… }
The key idea — content addressing. Every layer and the config are named by the
sha256 hash of their bytes: a digest, written sha256: followed by 64 hex chars.
$$\text{digest}(b) = \texttt{"sha256:"} ,\Vert, \mathrm{hex}!\left(\mathrm{SHA256}(b)\right)$$
This single decision buys three properties for free:
- Deduplication. Identical bytes have identical digests, so the registry (and your local
Docker cache) stores them once. Two images built
FROM debian:12share that base layer's bytes — one copy on disk, one upload over the wire (Ch. 4). - Tamper-evidence. Change one byte of a layer and its digest changes completely; a manifest that pins layer digests therefore can't be silently altered (Ch. 3, content trust).
- Cache validity. A build step's cache key is the digest of its inputs, which is why
changing
COPY ./appbusts onlylayer2and reuseslayer0/layer1.
In the lab, this is the very first function:
def digest(data: bytes) -> str:
return "sha256:" + hashlib.sha256(data).hexdigest()
Common misconception. "A bigger image is slower to pull." Not necessarily — pull time
is dominated by the layers you don't already have. A 2 GB image whose base you cached
pulls almost instantly; a 200 MB image with an all-new bottom layer does not. Layer
boundaries, not total size, decide pull cost. This is why you put rarely-changing things
(OS, dependencies) at the bottom of the Dockerfile and your fast-changing app at the top.
Chapter 3: Manifests and Tags — Immutable Content, Mutable Names
The manifest. You can't address an image by hashing all its layers concatenated —
that would re-hash gigabytes. Instead a manifest is a small JSON document that lists
the config descriptor and the layer descriptors (each a {mediaType, digest, size}):
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": { "digest": "sha256:aaa…", "size": 1472 },
"layers": [
{ "digest": "sha256:bbb…", "size": 30412763 }, // base layer
{ "digest": "sha256:ccc…", "size": 12044 } // your app layer
]
}
The manifest is itself content-addressed: its digest is the sha256 of its bytes. So an
image is identified by its manifest digest — a single 32-byte hash that transitively
commits to every layer and the config. That is what app@sha256:… refers to. In the lab:
def _manifest_digest(manifest) -> str:
canonical = "\n".join([manifest.config, *manifest.layers]).encode()
return "sha256:" + hashlib.sha256(canonical).hexdigest()
Multi-arch — the manifest list / image index. What does docker pull python:3.12
return on an ARM Mac vs an x86 server? A manifest list (OCI: image index): a manifest
whose entries are other manifests, one per os/arch. The client picks the one matching
its platform. (The lab models single-arch manifests; the extension adds the index.)
Tags — the mutable layer. A tag like :1.0 or :latest is not part of the
content; it is a mutable pointer: a name in the registry that maps to a manifest digest.
tag (mutable name) manifest digest (immutable content)
───────────────── ────────────────────────────────────
myrepo/app:latest ───────▶ sha256:1d4f… (today)
myrepo/app:latest ───────▶ sha256:9ab2… (after someone re-pushes :latest)
myrepo/app:1.4.2 ───────▶ sha256:1d4f… (by convention, never re-pushed)
This is the single most important operational fact in the chapter. A tag can be
repointed. :latest "changing under you" with nobody deploying means somebody re-pushed
the tag to a new digest. Therefore:
Pin production by digest, not tag.
app@sha256:1d4f…is a contract;app:latestis a suggestion. In the lab,resolve()accepts either, andtag()lets you repoint a name at a new digest — exactly the foot-gun, made visible.
Common misconception. "Immutable tags make a tag immutable." ACR's immutable tags feature (and policy) blocks re-pushing a tag — it doesn't change the data model; it just forbids the mutation. The underlying truth (tag = pointer, digest = content) is unchanged.
Chapter 4: OCI Distribution — Push, Pull, and Deduplication
A registry speaks the OCI distribution spec — a small HTTP API rooted at /v2/. The
two operations that matter:
Push (docker push myrepo/app:1.0):
- For each layer + the config, the client computes the digest and asks the registry "do
you already have
sha256:…?" (aHEADto/v2/myrepo/app/blobs/<digest>). - It uploads only the blobs the registry is missing. (Shared base already there → skipped. This is server-side dedup.)
- It uploads the small manifest and associates the tag.
Pull (docker pull myrepo/app:1.0) — the algorithm the lab's pull() implements:
1. resolve tag ──▶ manifest digest (GET /v2/<repo>/manifests/<tag>)
2. fetch manifest by digest (the small JSON listing blobs)
3. for each blob digest in the manifest:
if NOT in local cache: download it (GET /v2/<repo>/blobs/<digest>)
else: "Already exists" ← layer reuse
4. unpack the layers in order, apply the config
The lab makes this a one-liner — a set difference:
def pull(self, ref, *, client_has: set[str]) -> list[str]:
manifest = self._manifests[self.resolve(ref)]
return [b for b in manifest.blobs() if b not in client_has] # only the missing
client_has = { base } manifest.blobs() = [ config, base, app ]
download = blobs − client_has = [ config, app ] ← base skipped
Why this is the whole game. Deduplication is cross-image and cross-pull. The base
layer you pulled for service-a is reused when you pull service-b built on the same base
— the client downloads only service-b's config and top layer. At fleet scale this is the
difference between every node cold-pulling 900 MB and every node pulling 12 MB on top of a
shared cached base. The cache is keyed by digest, so reuse is automatic and safe.
Common misconception. "Pull deduplication is a Docker client trick." No — it is a
property of content addressing. Any OCI-conformant registry and client get it, because
the blob's identity is its hash. The lab proves it with blob_count(): two 2-layer
images that share a base store 5 unique blobs, not 6.
Chapter 5: Azure Container Registry — The Managed OCI Registry
What it is. ACR is a managed, private OCI registry — the same /v2/ data plane as
Chapter 4, run by Azure, fronted by Azure Resource Manager for the control plane (create the
registry, set SKU, configure replication) and by Entra/RBAC for auth. SKUs:
| SKU | Storage | Throughput | Geo-replication | Notable |
|---|---|---|---|---|
| Basic | low | low | no | dev/test |
| Standard | more | more | no | most prod |
| Premium | most | highest | yes | geo-replication, content trust, private link, CMK |
Authentication — the principal-level point. Never use the admin user / password in
production. A pod on AKS should pull with a managed identity (Phase 03) granted the
AcrPull role (Phase 04) — a token, scoped, rotated, revocable, no secret in a
manifest. AcrPush for CI. This is the control-plane/data-plane split again: RBAC
(AcrPull) is the control-plane grant; the actual blob GET is the data plane.
ACR Tasks — registry-side compute:
az acr build— build the image in the registry (no local Docker), push on success.- Triggered tasks — rebuild on a git commit, on a base-image update (a CVE patch to
debian:12rebuilds everythingFROMit), or on a schedule. This is the supply-chain spine: a base CVE fix propagates without a human.
Content trust / signing — Docker Content Trust (Notary v1) and the newer Notation / sigstore sign a manifest digest; a consumer verifies the signature before running. Because the digest commits to all content, a valid signature means "exactly these bytes, from this signer." (Lab extension models this with an HMAC over the digest.)
Soft-delete + GC — deleted manifests can be recovered within a retention window (soft-delete); storage is reclaimed by garbage collection of untagged manifests and unreferenced blobs (Ch. 6).
Chapter 6: Garbage Collection — Mark and Sweep from the Tags
The problem. CI pushes hundreds of builds a day. Most get a tag like build-4821 that
nobody references a week later. Storage grows forever unless something reclaims it. But
you cannot just "delete old blobs" — a blob might be a base layer that ten current
images still need. You need to delete exactly the blobs no live image references.
The algorithm — mark-and-sweep, rooted at the tags (what the lab's garbage_collect
implements, and what ACR's untagged-manifest GC does):
MARK a manifest is LIVE ⇔ some tag points at it.
every blob in a live manifest's closure (config + layers) is REACHABLE.
SWEEP delete every stored blob that is NOT reachable.
drop every manifest that NO tag keeps alive (untagged manifests).
reachable = set()
for manifest_digest in registry._tags.values(): # roots = the tags
reachable.update(registry._manifests[manifest_digest].blobs())
swept = {d for d in registry._blobs if d not in reachable} # the garbage
The crucial property interviewers probe. A shared layer survives as long as any
tagged image references it. Untagging one image reclaims only that image's exclusive
blobs (its config + unique top layer); the shared base stays because a sibling tag still
reaches it. The base is reclaimed only after the last referencing tag is gone. The lab
proves both halves (test_gc_shared_layer_survives_until_last_tag_gone).
tags: service-a → m_a, service-b → m_b (m_a, m_b share `base`)
untag service-a → GC reclaims top_a, cfg_a (a's exclusive blobs)
base SURVIVES (service-b still uses it)
untag service-b → GC now reclaims base too (last reference gone)
Why "we deleted 4 000 tags and reclaimed nothing" happens. Deleting a tag makes a manifest a candidate, but GC only runs as a separate sweep — and if even one tag (or a manifest-list entry, or a still-pulling client) still references the blobs, they stay. Reclaiming storage = untag and run GC, and the bytes freed = the exclusive closure of the untagged images, not the headline tag count.
Chapter 7: Geo-Replication — Copy Only What's Missing
Why. An AKS cluster in westeurope pulling images from an ACR in eastus pays
cross-region latency on every cold node start — and a region outage at the registry stalls
pod starts everywhere. Geo-replication (Premium ACR) keeps a replica of the registry's
content in each chosen region; a pull resolves to the nearest replica.
The mechanism — same content addressing, applied across regions. Replicating a
repository copies the small manifests always, but the large blobs are content-addressed, so
a region only stores each blob once and a replication into a warm region copies only
the blobs that region is missing. The lab's replicate is exactly this:
for d in manifest.blobs():
if not dst.has_blob(d): # copy ONLY what the destination lacks
dst._blobs[d] = src._blobs[d]
copied.append(d)
dst._manifests[manifest_digest] = manifest # the small manifest always
dst._tags[name] = manifest_digest # replicate the pointer
replicate service-a → cold west region: copies base + cfg_a + top_a + manifest
replicate service-b → base already in west → copies only cfg_b + top_b + manifest
So replicating a second image that shares a base with an already-replicated one is nearly free — the dedup that saved you on push (Ch. 4) saves you again on cross-region copy. This is why geo-replication's storage and egress cost scales with unique content, not with image count.
Chapter 8: AKS — The Control Plane and the Scheduler
What AKS is. Azure Kubernetes Service is a managed Kubernetes: Microsoft runs the
control plane (the API server, etcd, the scheduler, the controller-manager) and
you run node pools (VM scale sets of worker nodes). You pay for the nodes (and optionally
an SLA on the control plane); you never SSH into the API server.
┌──────────────── Microsoft-managed control plane ───────────────┐
kubectl ──▶│ API server ──▶ etcd (desired state) │
│ │ │
│ ▼ │
│ SCHEDULER ── decides which NODE each unscheduled Pod lands on │
│ controller-manager (Deployments→ReplicaSets→Pods, autoscaler) │
└───────────────────────────────┬────────────────────────────────┘
│ binding
┌───────────── customer node pools (VMSS) ─────────────┐
│ node aks-sys-0 [ kubelet | container runtime ] │
│ node aks-gpu-0 [ kubelet | container runtime ] taint=gpu
└──────────────────────────────────────────────────────┘
The scheduler is the component this lab models. Its job: for each pod with no node yet, pick a node. It runs in two phases:
- Filter (predicates) — eliminate nodes that can't run the pod:
PodFitsResources— node's remaining allocatable cpu and memory ≥ the pod's requests;TaintToleration— every taint on the node is tolerated by the pod;- (also
NodeAffinity,nodeSelector, volume limits, topology — beyond the lab).
- Score — among the survivors, rank (least-allocated to spread, or most-allocated to bin-pack tightly) and pick the best; then bind the pod to that node.
The lab implements the filter as a deterministic first-fit bin-pack:
for pod in sorted(pods, key=lambda p: p.name): # deterministic order
for node in nodes: # first-fit, list order
if not pod.tolerates(node): continue # TaintToleration
if pod.cpu_req <= rem.cpu and pod.mem_req <= rem.mem: # PodFitsResources
rem.cpu -= pod.cpu_req; rem.mem -= pod.mem_req # consume capacity
place pod on node; break
else: pod is Pending # fit no node → unschedulable
Taints and tolerations — the node-pool isolation mechanism. A taint on a node
(key=value:NoSchedule) repels pods unless the pod carries a matching toleration.
This is how you keep general workloads off your GPU pool, your spot pool, or your system
pool: taint the special nodes, and only the pods that opt in land there. In the lab a node
with taint gpu accepts only pods whose tolerations include gpu; a node with two taints
needs all of them tolerated.
Chapter 9: Requests, Limits, QoS, and Why a Pod is Pending
This is the chapter that ends the most support tickets. Two numbers per container, and they do different jobs:
| request | limit | |
|---|---|---|
| Used by | the scheduler (placement) | the kubelet (runtime enforcement) |
| Means | "guarantee me at least this" | "never let me exceed this" |
| CPU over it | (CPU can't be "over" request) | throttled (CFS quota) |
| Memory over it | — | OOM-killed |
| If unset | scheduler assumes 0 → overcommit risk | container can starve neighbours |
The scheduler places by requests, never by actual usage. This is the source of the
classic confusion: "the node's CPU graph shows 20% used, why is my pod Pending?" Because
the requests already committed on that node sum to its allocatable capacity — the pods
reserved the CPU even though they aren't using it right now. The scheduler honours the
reservation. The fix is right-sizing requests, not staring at the usage graph.
allocatable vs capacity. A node's raw capacity is not schedulable in full: the
kubelet reserves kube-reserved + system-reserved + an eviction threshold, leaving
allocatable — the number the scheduler actually packs against. (The lab takes
allocatable as given via Node.cpu/Node.mem.)
QoS classes (decided by the request/limit relationship — they set eviction order under node memory pressure):
- Guaranteed — requests == limits for every resource → evicted last.
- Burstable — requests < limits → evicted in the middle.
- BestEffort — no requests/limits → evicted first.
Why a pod is Pending — the complete answer. It maps 1:1 to the filter predicates, and
kubectl describe pod tells you which one:
0/3 nodes are available:
1 node(s) had untolerated taint {gpu: } ← TaintToleration (Ch. 8)
2 Insufficient cpu. ← PodFitsResources (requests > allocatable)
Every Pending is one of: not enough requestable cpu/mem left on any node; a taint
no node-matching pod tolerates; an unsatisfiable affinity/selector/topology; or no node
of the needed shape exists yet (→ the cluster autoscaler adds one, if the pool can grow).
The lab models the first two — the overwhelming majority of real cases — and proves them
deterministically.
Chapter 10: The Serverless Container Paths — ACA and ACI
AKS is powerful and operationally heavy — you own node pools, upgrades, autoscaling, the whole Kubernetes surface. For many workloads that is overkill. Azure offers two non-Kubernetes container paths, and a principal picks deliberately:
| AKS | Azure Container Apps (ACA) | Azure Container Instances (ACI) | |
|---|---|---|---|
| Model | full Kubernetes | serverless containers (Kubernetes hidden) | a single container group |
| Scaling | HPA + cluster autoscaler | scale-to-zero, KEDA event-driven | none (one shot) |
| You operate | nodes, upgrades, k8s | almost nothing | nothing |
| Best for | platforms, complex topologies, full k8s API | microservices, event/queue workers, HTTP apps | batch jobs, CI agents, quick tasks |
| Billing | per node-hour | per-request / per-active-second, $0 idle | per-second while running |
The dial (Phase 00 thinking). The axis is control/flexibility ↔ operability/cost. Choose AKS when you genuinely need the Kubernetes API, custom controllers, DaemonSets, or multi-team platform isolation. Choose ACA when you want containers + autoscaling + scale-to-zero without running a cluster — a bursty queue worker that's idle at night costs nothing. Choose ACI for a single short-lived container (a nightly batch, a build agent). The wrong default — "everything on AKS" — is how teams end up paying for, and being paged by, a cluster a Container App would have run for free at 3 a.m.
Lab Walkthrough Guidance
Lab 01 — OCI Registry Engine + AKS Scheduler, suggested order:
digest(data)(Ch. 2) —"sha256:" + hexdigest. Verify against the knownsha256("")constant; confirm one flipped bit changes it.Registry.put_blob/Manifest.blobs(Ch. 2–3) — store-by-digest with dedup (identical bytes → one blob). Theblob_count()is your dedup proof.put_manifest/_manifest_digest(Ch. 3) — content-address the manifest; re-pushing the same image must return the same digest and store nothing new.tag/untag/resolve(Ch. 3) — the mutable pointer;resolveaccepts a tag or a digest; re-tagging moves the pointer (the:latestfoot-gun).pull(ref, client_has=…)(Ch. 4) — the set difference: manifest's blobs minus the client's cache, config-first order. Cold cache downloads all; warm skips the base.garbage_collect(Ch. 6) — mark from the tags, sweep the rest. Prove the shared base survives an untag and is reclaimed only after the last tag is gone.replicate(Ch. 7) — copy the manifest + only the destination's missing blobs; the second shared-base image copies almost nothing.Node/Pod/schedule(Ch. 8–9) — first-fit bin-pack by requests under taints, decrementing capacity; a pod that fits nowhere isNone(Pending). Test determinism (sort by pod name → input order doesn't matter).
Success Criteria
You are ready for Phase 08 (CI/CD & secure supply chain) when you can, from memory:
- Draw the image model — layer → digest → manifest → tag — and explain what each is.
- Explain content addressing and the three properties it buys (dedup, tamper-evidence, cache validity).
- State why production pins a digest, not
:latest, and what "the tag changed under me" actually means. - Trace a
pullas a set difference and say why a warm-cache pull downloads only new layers. - Run garbage collection in your head: why deleting tags ≠ reclaiming storage, and why a shared layer survives until the last referencing tag is gone.
- Explain geo-replication as content-addressed copy-only-missing, and why replicating a shared-base image is nearly free.
- Describe the scheduler's filter step (
PodFitsResources+TaintToleration) and explain aPendingpod fromkubectl describeoutput. - Distinguish requests (scheduler/placement) from limits (kubelet/enforcement) and name the three QoS classes and their eviction order.
- Pick AKS vs ACA vs ACI for a workload and defend it with the operability/cost dial.
Interview Q&A
Q: What is a Docker image, precisely?
An ordered stack of read-only filesystem layers plus a config JSON, where every
layer and the config are named by the sha256 digest of their bytes. A manifest
lists those digests and is itself content-addressed, so an image is identified by a single
manifest digest. A tag is just a mutable name pointing at a manifest digest. Content
addressing is the whole point: it gives deduplication, tamper-evidence, and cache validity
for free.
Q: :latest worked yesterday and broke prod today and nobody deployed. How?
A tag is a mutable pointer, not content. Someone re-pushed :latest to a new manifest
digest — the name now resolves to different bytes. Your running pods kept the old digest;
the next pull (a new node, a restart) got the new one. The fix is to pin by digest
(app@sha256:…) in production manifests, and to enable immutable tags in ACR so the tag
can't be repushed. :latest is a convenience for humans, never a deployment contract.
Q: Two 900 MB images, but the second push only added 200 MB of storage. Why?
Content-addressed deduplication. The two images share lower layers (same base, same
dependency layers); those blobs already existed in the registry, so the push uploaded only
the blobs whose digests were missing — the differing top layers. The registry stores each
unique blob once regardless of how many images or tags reference it. Same reason a warm-cache
pull downloads only the new layers.
Q: We deleted 4 000 old tags and reclaimed no storage. Why? Deleting a tag doesn't delete blobs — it makes a manifest an untagged candidate. Storage is reclaimed by a separate garbage-collection sweep, and GC only deletes blobs that no live (tagged) manifest reaches. If your "old" images shared layers with current images, those layers stay. Reclaiming storage = untag the manifests and run GC, and the bytes you free are the exclusive closure of the untagged images, not a function of the tag count.
Q: A pod is Pending but the node CPU graph shows 20% utilization. Why won't it schedule?
The scheduler places by requests, not by live usage. The pods already on that node
reserved CPU via their requests; the sum of committed requests equals the node's
allocatable CPU even though actual usage is low. The new pod's request doesn't fit the
remaining requestable capacity, so it's Pending. kubectl describe pod will say
Insufficient cpu. The fix is right-sizing requests (or adding nodes), not looking at the
usage graph — and if it instead says had untolerated taint, the node pool is reserved
(GPU/spot/system) and the pod lacks the toleration.
Q: Requests vs limits — what's the difference, and what's a QoS class?
A request is the floor the scheduler guarantees and places by; a limit is the
ceiling the kubelet enforces at runtime — CPU over limit is throttled, memory over
limit is OOM-killed. The relationship sets the QoS class: requests == limits →
Guaranteed (evicted last under memory pressure); requests < limits → Burstable;
none set → BestEffort (evicted first). Guaranteed for latency-critical workloads,
BestEffort only for truly disposable ones.
Q: AKS, ACA, or ACI — how do you choose? By the operability/cost dial. AKS when I need the full Kubernetes API — custom controllers, DaemonSets, complex topology, multi-team platform isolation — and accept owning node pools and upgrades. ACA (Container Apps) for HTTP services and event/queue workers that want autoscaling and scale-to-zero without me running a cluster — idle costs nothing. ACI for a single short-lived container, like a nightly batch or a CI agent. The anti-pattern is defaulting everything to AKS and paying (in money and pages) for a cluster a Container App would run for free overnight.
Q: How should a pod authenticate to ACR to pull its image?
With a managed identity (Entra, Phase 03) granted the AcrPull RBAC role (Phase 04)
— a scoped, rotating, revocable token, no secret in a manifest. Never the registry admin
user/password in production. This is the control-plane/data-plane split: AcrPull is the
control-plane grant; the blob GET is the data plane. Pair it with digest-pinned
images, content trust/signing, and CVE scanning for a defensible supply chain.
References
- OCI Image Specification — image manifest, config, layers, descriptors, digests: https://github.com/opencontainers/image-spec
- OCI Distribution Specification — the
/v2/push/pull API and content addressing: https://github.com/opencontainers/distribution-spec - OCI Image Index (manifest list) — multi-arch images: https://github.com/opencontainers/image-spec/blob/main/image-index.md
- Azure Container Registry docs — concepts, geo-replication, ACR Tasks, content trust, soft-delete, GC: https://learn.microsoft.com/azure/container-registry/
- ACR authentication with managed identity /
AcrPull: https://learn.microsoft.com/azure/container-registry/container-registry-authentication - Azure Kubernetes Service docs — control plane, node pools, taints, autoscaling: https://learn.microsoft.com/azure/aks/
- Kubernetes Scheduler — filter/score, predicates, taints/tolerations: https://kubernetes.io/docs/concepts/scheduling-eviction/kube-scheduler/
- Kubernetes — Managing Resources (requests/limits) & QoS classes: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ and https://kubernetes.io/docs/concepts/workloads/pods/pod-qos/
- Azure Container Apps and Azure Container Instances docs (the serverless paths): https://learn.microsoft.com/azure/container-apps/ and https://learn.microsoft.com/azure/container-instances/
- The track's own CHEATSHEET.md and GLOSSARY.md.