The Exam Blueprint · D1 · Platform Architecture & Infrastructure · 15%

Platform Architecture & Infrastructure

Before anyone paves a golden path, someone lays the ground — this ground: how the platform hands out compute, wires up networking, persists storage, shares one substrate across teams without collisions (multi-tenancy), scales with load (autoscaling), and keeps the cost honest. Get the bedrock right and every later lesson stands straight; get it wrong and you firefight for a year.

☺ Explain it like I’m 10

Your platform is a giant shared apartment building. Compute is the electricity and floor space; networking, the hallways and doors deciding who may visit whom; storage, each family’s locked closet that survives room changes. Multi-tenancy is the house rules for sharing without fighting over hot water. Autoscaling adds rooms when people arrive, removes them when they leave. Cost is the caretaker reading meters so nobody leaves the lights on.

🦉🦥Your hosts for this topic: Professor Owl & Sol the Sloth — Owl draws the blueprint for compute, networking, storage and tenancy, and Sol reads the toll-meter, right-sizing everything so the platform is fast and affordable.

Three raw resources, one shared bedrock

Strip a platform to the metal and it hands out three primitives: compute (CPU and memory for containers), networking (wires between containers and the world), and storage (durable disks that outlive any pod). Kubernetes — the substrate we call Nimbus — is a good scheduler and API over the three. The first competency: lay them out efficiently, resiliently, and safely to share.

The other two competencies build on top: cost management (right-sizing so you don’t pay for idle) and multi-tenancy (packing many teams on without harming each other). Professor Owl’s reference architecture assumes this layer is solid, so we start here.

◆ Key idea

Capacity is a shared, finite pool, not a personal sandbox. Almost every best practice here is one rule in different hats: declare what each workload needs, cap what it can take, and make the platform pack and scale that honestly.

Compute best practices

☺ Like you’re 10: You tell the platform “my app needs about this much,” and it finds room — like reserving a seat so you’re not left standing.

Compute placement turns on two numbers per container. The request is what the scheduler reserves — it packs pods so their requests never exceed a node’s allocatable capacity. The limit is the runtime ceiling. Their gap decides scheduling, throttling, eviction, cost, and autoscaling — the domain’s most consequential dial.

Requests, limits, and the two kinds of resource

Past the limit, CPU and memory diverge. CPU is compressible: over the limit the container is merely throttled, not killed. Memory is incompressible: over the limit it’s OOMKilled and restarted. Hence a senior guideline: set memory requests equal to limits, but go easy on CPU limits, which needlessly throttle latency-sensitive services.

What the scheduler packs = the SUM OF REQUESTS Node · allocatable = 8 CPU pod A · 2 pod B · 3 C · 1 2 free (headroom) At runtime, a container may burst above its request → up to its LIMIT request reserved floor limit burst ceiling CPU over limit → throttled  ·  memory over limit → OOMKilled

QoS classes — who gets evicted first

☺ Like you’re 10: When the building runs low on power, families who promised exactly what they’d use keep their lights on; those who promised nothing get switched off first.

From those requests and limits, Kubernetes derives a Quality of Service class per pod, deciding who’s evicted under memory pressure. You don’t set it — you earn it:

QoS classHow a pod qualifiesUnder node pressure
GuaranteedEvery container sets CPU and memory, and limits equal requests for both.Evicted last. Give this to databases and critical singletons.
BurstableAt least one container has a request or limit, but the pod isn’t Guaranteed.Evicted after BestEffort, worst-offenders (usage above request) first.
BestEffortNo requests or limits anywhere in the pod.Evicted first. Fine for throwaway batch; never for anything that matters.
⚠ Watch out

Pods with no requests are BestEffort — the first the kubelet kills under pressure, and rejected outright in any namespace whose ResourceQuota constrains that resource, because the quota system refuses a pod that omits it. Make requests mandatory: a LimitRange (below) supplies defaults, and admission policy rejects pods that omit them.

Steering pods onto the right nodes

Not all compute is equal — general, memory-heavy, GPU, and cheap interruptible spot nodes. Group them into node pools (managed node groups on EKS, node pools on GKE/AKS), then steer workloads with four tools:

☺ Like you’re 10: Taints say “stay off my lawn unless invited,” affinity “sit near your friends,” and spread rules “don’t all pile onto one bench, so if one breaks most of you are fine.”

PriorityClass and bin-packing

When capacity is tight, two more levers decide what runs. A PriorityClass assigns an integer priority; if a high-priority pod can’t schedule, the scheduler may preempt (evict) lower-priority pods — how you guarantee platform-critical components outrank a batch job.

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: platform-critical
value: 1000000            # higher wins; may preempt lower-priority pods
globalDefault: false
description: "Ingress, DNS, mesh, and other must-never-die platform add-ons."
---
# A batch job that yields under pressure instead of preempting others:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: best-effort-batch
value: 100
preemptionPolicy: Never   # will wait for room, never evict anyone
globalDefault: false

Finally, bin-packing is the cost-critical choice of how tightly to pack pods. The default scheduler spreads them out (LeastAllocated) — resilient, but nodes sit half-empty. MostAllocated scoring, or Karpenter (which bin-packs natively), packs onto fewer, fuller nodes the autoscaler can delete. Tight packing saves money; loose buys headroom — pick on purpose.

Networking best practices

☺ Like you’re 10: Every app gets its own unchanging phone number, a receptionist who forwards calls, and locked doors so only the right apps can call.

Kubernetes gives every pod its own IP, but pods are ephemeral, so you rarely talk to one directly — networking is about stable names, controlled doors, and who may reach whom.

The CNI: the wiring under everything

A CNI (Container Network Interface) plugin gives pods their IPs and decides whether NetworkPolicy is enforced at all. Platform-grade choices: Cilium (eBPF, deep policy, load-balancing, Hubble; can replace kube-proxy) and Calico (mature policy, flexible networking). A bare overlay like Flannel moves packets but ignores NetworkPolicy — pick a policy-enforcing CNI or your isolation is a silent no-op.

North-south vs east-west, and how traffic gets in

East-west is service-to-service traffic inside the cluster; north-south crosses the boundary. Inside, a Service (usually ClusterIP) is a stable virtual IP and DNS name load-balancing across a Deployment’s healthy pods; a headless Service (clusterIP: None) gives each StatefulSet pod its own record. CoreDNS resolves service.namespace.svc.cluster.local, so apps find each other by name, not IP.

For north-south HTTP, an Ingress plus a controller routes outside requests to Services — but it shows its age: features hide in controller-specific annotations, and it blurs ownership. Its successor, the Gateway API, is role-oriented: infra owns the GatewayClass, cluster operators the Gateway (listener), app teams their HTTPRoutes. More expressive (header routing, traffic splitting, multi-protocol) and portable — the clean self-service boundary a platform wants.

NeedReach forWhy
Stable in-cluster address (east-west)Service (ClusterIP) + CoreDNSOne name, auto load-balanced across healthy pods.
Simple HTTP entry (north-south)Ingress + controllerUbiquitous; fine for basic host/path routing.
Expressive, multi-team entryGateway API (Gateway/HTTPRoute)Role separation, traffic splitting, multi-protocol — self-service-friendly.
Lock down who talks to whomNetworkPolicy (policy-CNI)Default-deny east-west; least-privilege between tenants.

Default-deny: the posture that scales

By default, every pod can reach every other. The best posture is default-deny: a NetworkPolicy blocking all ingress and egress in a namespace, then narrow “allow” rules for the traffic you want (including DNS, always forgotten). That’s the network half of zero-trust; the identity half — mTLS via a service mesh — lives in Security & Policy.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: payments
spec:
  podSelector: {}                 # selects EVERY pod in the namespace
  policyTypes: [Ingress, Egress]  # with no rules below, nothing is allowed
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-in-namespace-and-dns
  namespace: payments
spec:
  podSelector: {}
  policyTypes: [Ingress, Egress]
  ingress:
    - from: [{ podSelector: {} }]           # only pods in this namespace
  egress:
    - to: [{ podSelector: {} }]             # to pods in this namespace
    - to:                                   # plus DNS to CoreDNS in kube-system
        - namespaceSelector:
            matchLabels: { kubernetes.io/metadata.name: kube-system }
      ports:
        - { protocol: UDP, port: 53 }
        - { protocol: TCP, port: 53 }
🦆 Dot’s-eye view

“I don’t want to think about IPs, subnets, or firewalls. I want to call http://orders and reach the orders service — not accidentally hit the payments database I’ve no business touching. A name that just works, plus a default-deny that keeps me in my lane — invisible until I bump into it.”

Storage best practices

☺ Like you’re 10: A pod is like a hotel room — check out and it’s wiped clean. Storage is your suitcase that follows you room to room and keeps your stuff.

Containers are ephemeral; local disk vanishes on restart. Anything that must survive — a database, a queue, uploaded files — needs a real volume. Kubernetes handles this through the CSI (Container Storage Interface), a plugin standard so cloud disks, NFS, and SAN all speak one API.

StorageClass, PV/PVC, and dynamic provisioning

Three objects carry the model. A StorageClass describes a kind of storage (fast SSD, cheap HDD, replicated), naming a CSI provisioner. A PersistentVolumeClaim (PVC) is a developer’s request: “20 Gi of fast-ssd.” The platform dynamically provisions: the CSI driver makes a disk and binds it as a PersistentVolume (PV), no Ops ticket. Two settings matter: reclaimPolicy: Retain keeps data if the claim is deleted; volumeBindingMode: WaitForFirstConsumer waits for the pod so the disk lands in its zone.

Access modes, StatefulSets, and databases on the platform

A PVC also declares an access mode, stricter than expected: ReadWriteOnce (RWO — one node; the norm for cloud block disks), ReadWriteOncePod (RWOP — one pod), ReadOnlyMany (ROX), and ReadWriteMany (RWX — many nodes at once, needing a shared filesystem like NFS/EFS, not a block disk). Asking for RWX when your store only supports RWO is a classic day-one stumble.

Stateful workloads also need stable identity, which a StatefulSet gives: ordered, stably-named pods (db-0, db-1…), each with its own PVC via volumeClaimTemplates, and ordered rollouts — what a clustered database expects. Run databases on-platform with an operator (CloudNativePG, Zalando Postgres) automating failover, backups, and restores, plus zone anti-affinity so one failure can’t take the quorum. Or provision a managed cloud database via Crossplane as a one-click self-service claim — trading control and cost for less ops burden.

◆ Key idea

Match the storage to the workload’s truth: block disk + StatefulSet + RWO for a single-writer database; shared filesystem + RWX for many pods writing one tree; no PVC for anything you can rebuild — keep it stateless, and let stateful things be few and operator-managed.

Multi-tenancy: sharing the road safely

☺ Like you’re 10: Lots of teams share one playground. Multi-tenancy is the rules and fences that let them all play without hogging the swings or wrecking a sandcastle.

The core question of multi-tenancy: how do you put many teams (“tenants”) on shared infrastructure — cheap and consistent — without one starving, snooping on, or crashing another? It turns first on how much you trust them.

Soft vs hard multi-tenancy

Soft multi-tenancy assumes tenants are trusted but clumsy — teams in one company. They won’t attack each other, so namespace isolation (quotas, policies, RBAC) prevents accidents. Hard multi-tenancy assumes they’re untrusted or hostile — a SaaS running arbitrary customer code. Now namespaces aren’t a security boundary: all pods share one Linux kernel, so a container escape crosses them, needing stronger walls — separate clusters, or sandboxed runtimes like gVisor or Kata Containers.

The three models

Namespace-per-tenant 1 shared control plane (API) ns A ns B ns C shared nodes cheapest · dense soft isolation quota · policy · RBAC Virtual clusters host cluster control plane vCluster Aown API vCluster Bown API workloads sync to shared nodes mid cost · mid density own CRDs & versions stronger than a namespace Cluster-per-tenant cluster Aown CP+nodes cluster Bown CP+nodes priciest · least dense hard isolation separate blast radius isolation & cost increase →   density decreases →
ModelIsolationDensity / costTenant getsBest when…
Namespace-per-tenantSoft (shared kernel & API server)Highest density, lowest costA namespace, scoped RBAC, quotasTrusted internal teams; you want maximum sharing.
Virtual clusters (vCluster)Medium (own API server, shared nodes)Good density, moderate costA “real-feeling” cluster: own CRDs, cluster-scoped objects, K8s versionTeams need cluster-admin-like power or their own CRDs, but a full cluster each is wasteful.
Cluster-per-tenantHard (separate control plane)Lowest density, highest costAn entire dedicated clusterUntrusted workloads, strict compliance, or hard blast-radius limits.

The isolation primitives

Within a shared cluster, four primitives do the fencing. A ResourceQuota caps a namespace’s total consumption (requests/limits, object counts, storage). A LimitRange sets per-container defaults and maximums — a default request so pods are countable by the quota, a cap so one can’t claim the namespace. NetworkPolicy walls off cross-tenant traffic; RBAC scopes each tenant to their namespace(s). Here’s a tenant’s guardrail pair:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: payments-quota
  namespace: payments
spec:
  hard:
    requests.cpu: "20"          # the team may reserve at most 20 cores…
    requests.memory: 40Gi
    limits.cpu: "40"            # …and burst to at most 40
    limits.memory: 80Gi
    persistentvolumeclaims: "15"
    pods: "150"                 # non-terminal pods in the namespace
---
apiVersion: v1
kind: LimitRange
metadata:
  name: payments-limits
  namespace: payments
spec:
  limits:
    - type: Container
      defaultRequest: { cpu: 100m, memory: 128Mi }  # applied if a pod omits requests
      default:        { cpu: 500m, memory: 512Mi }  # applied if a pod omits limits
      max:            { cpu: "4",  memory: 8Gi }     # no single container may exceed this
⚠ The noisy-neighbour trap

Without a ResourceQuota, one tenant can fill the shared nodes and starve everyone else — the classic noisy neighbour. A quota also makes requests mandatory, so ship it with a LimitRange (which supplies defaults) — otherwise day one brings a wall of confusing “failed quota” errors.

When to escalate to cluster isolation

Start cheap (namespaces) and escalate only when a requirement forces it. Reach for vClusters when tenants need their own CRDs, cluster-scoped resources, or admin-ish control but a full cluster each wastes money. Go dedicated for untrusted code, a hard compliance boundary (PCI, regulated data), a blast-radius limit, or conflicting cluster-wide needs (different Kubernetes versions, clashing webhooks). Every step buys isolation and costs density — make each a deliberate decision.

Autoscaling: growing and shrinking with the load

☺ Like you’re 10: The platform adds helpers when it’s slammed and sends them home when it’s quiet — so you never pay a crowd to stand around.

Autoscaling works on three independent axes; the exam wants you to know which tool moves which. You can add more pods, make each pod bigger, or add more nodes — triggered by CPU, custom metrics, or external events. Get the axes straight and the five tools fall into place.

SCALE PODS HPA — more replicas (out) on CPU / metric VPA — bigger requests (up) right-size, one pod HPA + VPA on the same metric fight — split the metric or run VPA in recommend-only. no room? SCALE NODES pending + new node Cluster Autoscaler — node groups Karpenter — groupless, bin-packs SCALE ON EVENTS queue 1,240 msg or → 0 KEDA — event-driven, scale to zero

Scaling pods — HPA and VPA (and why they fight)

The HorizontalPodAutoscaler (HPA) adds and removes replicas to hit a target metric — usually average CPU as a % of the request, also memory, custom, or external metrics. The VerticalPodAutoscaler (VPA) works the other axis: it watches real usage and adjusts each pod’s requests and limits, traditionally by recreating the pod. Use its recommender for “what should this pod actually request?”

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: checkout
  namespace: payments
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: checkout
  minReplicas: 3
  maxReplicas: 40
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70     # add pods when avg CPU > 70% of the REQUEST
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300  # wait 5 min before scaling in, to avoid flapping
⚠ HPA + VPA on the same metric = a tug-of-war

Never point HPA and VPA at the same resource metric. If both watch CPU, HPA adds replicas while VPA shrinks each pod’s CPU request — which shifts HPA’s math, so it adds more… they oscillate. Safe combos: run VPA in recommendation-only mode beside an HPA, or drive HPA off a different metric and let VPA own CPU/memory. Since HPA targets a percentage of the request, a sane request (which VPA finds) is a prerequisite for HPA behaving.

Scaling nodes — Cluster Autoscaler and Karpenter

Pods grow until the nodes fill up. The Cluster Autoscaler grows a pre-defined node group (an ASG/MIG) to fit pending (unschedulable) pods, shrinking it when idle. Karpenter is the newer, groupless approach: it reads the exact pending pods and provisions right-sized nodes from the cloud catalogue — cheapest shape that fits, spot where possible — then consolidates onto fewer nodes. Karpenter usually wins on utilisation; the Cluster Autoscaler is simpler and works anywhere node groups do.

Scaling on events — KEDA

☺ Like you’re 10: Some work shows up only sometimes, like letters in a mailbox. KEDA wakes workers only when letters arrive, then lets them nap (all the way to zero) when it’s empty.

KEDA (Kubernetes Event-Driven Autoscaling) scales on external events rather than CPU: queue depth (Kafka lag, RabbitMQ, SQS), a Prometheus query, a cron schedule, and more via its scalers. Its superpower is scale-to-zero: plain HPA can’t go below one replica, but KEDA parks an idle workload at zero and wakes it on the first event, driving an HPA for the busy range. For bursty or scheduled work.

ToolWhat it scalesTriggerWatch out for
HPAPod count (out/in)CPU %, memory, custom / external metricTargets a % of the request; won’t go below minReplicas.
VPAPod size (requests/limits)Observed usage over timeRecreates pods to apply; don’t pair with HPA on the same metric.
Cluster AutoscalerNode count, per node groupPending (unschedulable) pods; idle nodesBound to pre-defined groups; coarser bin-packing.
KarpenterNode count, grouplessPending pods; consolidation opportunitiesProvisions & bin-packs right-sized/spot nodes; cloud-specific providers.
KEDAPods, incl. to zeroEvents: queue depth, Kafka lag, cron, PromQL…Drives an HPA underneath; great for bursty/queue work.
◆ Key idea

Pod scaling and node scaling are partners. HPA/VPA/KEDA decide how much pod you need; the Cluster Autoscaler or Karpenter make sure there’s a node for it — the pod layer reacts in seconds, the node layer backfills right behind.

Cost management: reading the toll-meter

☺ Like you’re 10: Sol the Sloth reads every meter and asks, “Do you really need all this, or can we turn some off?” — so the bill matches what you use.

Every core and gigabyte is money, and the platform’s job is to make it visible and honest. This is the domain’s cost management competency — FinOps applied to Kubernetes, right-sizing and scaling so the bill tracks reality — and Sol runs it in three moves: see the cost, attribute it, cut the waste.

Making cost visible — OpenCost & Kubecost

You can’t optimise what you can’t see. OpenCost is the CNCF project that models Kubernetes spend, allocating real cloud prices to namespace, workload, or label — so “what does the payments team cost?” becomes a query, not a guess. Kubecost is the commercial product around it: richer UI, longer retention, savings recommendations. An opaque bill becomes per-team numbers to act on.

Showback vs chargeback

Once you can attribute cost, you choose how firmly to enforce it. Showback shows each team what they cost — pure visibility, no invoice — where almost everyone should start, since awareness alone changes behaviour. Chargeback bills each team’s budget for hard accountability and the sharpest right-sizing, but needs mature, trusted data — bill on wrong numbers and you lose trust fast. Move only once the allocation is credible.

☺ Like you’re 10: Showback puts everyone’s snack tab on the fridge to see; chargeback takes it out of their allowance.

Right-sizing, spot, and bin-packing — where the money hides

Most waste is the gap between what workloads request (and pay for) and what they use — reserve 8 cores, use 2, and 6 sit idle per replica. Closing it with VPA recommendations and OpenCost data is the single biggest lever. Two structural savings on top: run interruption-tolerant work (stateless, batch, CI) on spot instances at a fraction of on-demand, and enable bin-packing / consolidation so the autoscaler packs onto fewer nodes. Right-size, pack tight, run on spot — the FinOps trifecta.

🦆 Dot’s-eye view

“I over-requested because I feared OOMKills, and nobody told me it cost anything. The day the platform showed me ‘you reserve 8 cores and use 1.5 — $340/month idle,’ I fixed my requests in ten minutes. Make the waste visible to me and I’ll happily right-size — I just never had the number.”

🦥 Sol’s cost workshop · 15 min

On a cluster with real workloads, install OpenCost (or Kubecost’s free tier) and open the allocation view by namespace. Find your biggest offender by the gap between requested and used. Pick one Deployment, run the VPA recommender in updateMode: "Off" for a few hours, apply its suggested requests, and watch idle spend drop — the whole FinOps loop in miniature.

🎬 At the Platform Guild
🦊

Foxy: Everything’s slow and half our pods keep dying. Do we just… buy bigger nodes?

🦉

Professor Owl: First, the requests. Pods with none are BestEffort — evicted first when a node’s squeezed. Set honest requests and limits; give critical ones a PriorityClass.

🦥

Sol: And before we buy anything… OpenCost says we reserve forty cores and use nine. Not short on hardware — short on right-sizing.

👺

Gizmo: Ugh, so fiddly. Give every team their own cluster, set every limit to “unlimited.” Solved! 🤑

🐢

Timmy: A cluster per team for trusted teams? A fortune in idle control planes. Namespaces, quotas, default-deny first — real clusters only for untrusted or compliance-bound tenants.

🦥

Sol: Right-size the pods, quota the tenants, add a node scaler behind the pod scaler, batch on spot. Then the platform is fast and cheap. …I’ll get there. Eventually. 🐌

That’s the bedrock: honest compute, named and default-denied networking, persistent storage, safe tenancy, scaling on three axes, and an honest toll-meter. Professor Owl can now draw the full reference architecture on top — and everything from GitOps to observability stands on solid footing.

🐢 Timmy’s checkpoint

1. What does a pod’s request control versus its limit, and what happens over each for CPU vs memory? 2. Which QoS class is evicted first, and how does a pod land there? 3. Name the three multi-tenancy models least to most isolated, plus one reason to go dedicated. 4. What do a ResourceQuota and a LimitRange each do, and why ship them together? 5. Match scaler to axis: more replicas, bigger pods, more nodes, scale-to-zero on a queue. 6. Where does most cost waste hide, and how do showback and chargeback differ?

Check your answers
  1. Request = what the scheduler reserves; limit = the runtime ceiling. Over the CPU limit: throttled (compressible); over the memory limit: OOMKilled and restarted (incompressible).
  2. BestEffort is evicted first — earned by setting no requests or limits. (Guaranteed — limits equal requests for CPU and memory — is evicted last.)
  3. Least → most isolated: namespace-per-tenantvClustercluster-per-tenant. Go dedicated for untrusted workloads, a hard compliance boundary, blast-radius limits, or conflicting cluster-wide needs (e.g. different K8s versions).
  4. ResourceQuota caps a namespace’s aggregate consumption; LimitRange sets per-container defaults and maximums. Ship both: a quota makes requests mandatory, so LimitRange defaults keep pods from being rejected — and stop one pod hogging the namespace.
  5. More replicas → HPA; bigger pods (requests/limits) → VPA; more nodes → Cluster Autoscaler or Karpenter; scale-to-zero on a queue → KEDA.
  6. Most waste hides in the gap between requested (paid-for) and used — close it by right-sizing. Showback shows teams their cost; chargeback bills their budget.