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 pod Pending. By the end you can answer "why did :latest change 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

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:

  1. an ordered stack of read-only layers, each layer a tarball of filesystem changes (files added/modified/deleted) contributed by one Dockerfile instruction;
  2. a config — a small JSON document with the entrypoint, env, working dir, exposed ports, and the ordered list of layer digests (the rootfs.diff_ids and history).
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:12 share 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 ./app busts only layer2 and reuses layer0/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:latest is a suggestion. In the lab, resolve() accepts either, and tag() 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):

  1. For each layer + the config, the client computes the digest and asks the registry "do you already have sha256:…?" (a HEAD to /v2/myrepo/app/blobs/<digest>).
  2. It uploads only the blobs the registry is missing. (Shared base already there → skipped. This is server-side dedup.)
  3. 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:

SKUStorageThroughputGeo-replicationNotable
Basiclowlownodev/test
Standardmoremorenomost prod
Premiummosthighestyesgeo-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:12 rebuilds everything FROM it), 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:

  1. 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).
  2. 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:

requestlimit
Used bythe 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 itOOM-killed
If unsetscheduler assumes 0 → overcommit riskcontainer 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:

AKSAzure Container Apps (ACA)Azure Container Instances (ACI)
Modelfull Kubernetesserverless containers (Kubernetes hidden)a single container group
ScalingHPA + cluster autoscalerscale-to-zero, KEDA event-drivennone (one shot)
You operatenodes, upgrades, k8salmost nothingnothing
Best forplatforms, complex topologies, full k8s APImicroservices, event/queue workers, HTTP appsbatch jobs, CI agents, quick tasks
Billingper node-hourper-request / per-active-second, $0 idleper-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:

  1. digest(data) (Ch. 2) — "sha256:" + hexdigest. Verify against the known sha256("") constant; confirm one flipped bit changes it.
  2. Registry.put_blob / Manifest.blobs (Ch. 2–3) — store-by-digest with dedup (identical bytes → one blob). The blob_count() is your dedup proof.
  3. put_manifest / _manifest_digest (Ch. 3) — content-address the manifest; re-pushing the same image must return the same digest and store nothing new.
  4. tag / untag / resolve (Ch. 3) — the mutable pointer; resolve accepts a tag or a digest; re-tagging moves the pointer (the :latest foot-gun).
  5. 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.
  6. 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.
  7. replicate (Ch. 7) — copy the manifest + only the destination's missing blobs; the second shared-base image copies almost nothing.
  8. Node / Pod / schedule (Ch. 8–9) — first-fit bin-pack by requests under taints, decrementing capacity; a pod that fits nowhere is None (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:

  1. Draw the image model — layer → digest → manifest → tag — and explain what each is.
  2. Explain content addressing and the three properties it buys (dedup, tamper-evidence, cache validity).
  3. State why production pins a digest, not :latest, and what "the tag changed under me" actually means.
  4. Trace a pull as a set difference and say why a warm-cache pull downloads only new layers.
  5. Run garbage collection in your head: why deleting tags ≠ reclaiming storage, and why a shared layer survives until the last referencing tag is gone.
  6. Explain geo-replication as content-addressed copy-only-missing, and why replicating a shared-base image is nearly free.
  7. Describe the scheduler's filter step (PodFitsResources + TaintToleration) and explain a Pending pod from kubectl describe output.
  8. Distinguish requests (scheduler/placement) from limits (kubelet/enforcement) and name the three QoS classes and their eviction order.
  9. 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 == limitsGuaranteed (evicted last under memory pressure); requests < limitsBurstable; 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.