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.
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.
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.
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.
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 class | How a pod qualifies | Under node pressure |
|---|---|---|
| Guaranteed | Every container sets CPU and memory, and limits equal requests for both. | Evicted last. Give this to databases and critical singletons. |
| Burstable | At least one container has a request or limit, but the pod isn’t Guaranteed. | Evicted after BestEffort, worst-offenders (usage above request) first. |
| BestEffort | No requests or limits anywhere in the pod. | Evicted first. Fine for throwaway batch; never for anything that matters. |
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:
- Taints & tolerations — a taint repels every pod without a matching toleration: reserve GPU nodes for GPU jobs, keep normal workloads off spot unless they opt in. Effects:
NoSchedule,PreferNoSchedule,NoExecute. - Node affinity — pulls a pod toward nodes with certain labels (e.g. the well-known
topology.kubernetes.io/zoneornode.kubernetes.io/instance-type), as a hardrequired…or softpreferred…rule. - Pod affinity / anti-affinity — places a pod relative to other pods: co-locate a cache next to its app (affinity), or force replicas apart so one node dying doesn’t take them all (anti-affinity).
- Topology spread constraints — the modern, precise way to spread replicas evenly across zones or nodes: set a
maxSkew, atopologyKey, and awhenUnsatisfiableof hardDoNotScheduleor softScheduleAnyway.
☺ 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.
| Need | Reach for | Why |
|---|---|---|
| Stable in-cluster address (east-west) | Service (ClusterIP) + CoreDNS | One name, auto load-balanced across healthy pods. |
| Simple HTTP entry (north-south) | Ingress + controller | Ubiquitous; fine for basic host/path routing. |
| Expressive, multi-team entry | Gateway API (Gateway/HTTPRoute) | Role separation, traffic splitting, multi-protocol — self-service-friendly. |
| Lock down who talks to whom | NetworkPolicy (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 }“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.
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
| Model | Isolation | Density / cost | Tenant gets | Best when… |
|---|---|---|---|---|
| Namespace-per-tenant | Soft (shared kernel & API server) | Highest density, lowest cost | A namespace, scoped RBAC, quotas | Trusted internal teams; you want maximum sharing. |
| Virtual clusters (vCluster) | Medium (own API server, shared nodes) | Good density, moderate cost | A “real-feeling” cluster: own CRDs, cluster-scoped objects, K8s version | Teams need cluster-admin-like power or their own CRDs, but a full cluster each is wasteful. |
| Cluster-per-tenant | Hard (separate control plane) | Lowest density, highest cost | An entire dedicated cluster | Untrusted 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 thisWithout 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.
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 flappingNever 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.
| Tool | What it scales | Trigger | Watch out for |
|---|---|---|---|
| HPA | Pod count (out/in) | CPU %, memory, custom / external metric | Targets a % of the request; won’t go below minReplicas. |
| VPA | Pod size (requests/limits) | Observed usage over time | Recreates pods to apply; don’t pair with HPA on the same metric. |
| Cluster Autoscaler | Node count, per node group | Pending (unschedulable) pods; idle nodes | Bound to pre-defined groups; coarser bin-packing. |
| Karpenter | Node count, groupless | Pending pods; consolidation opportunities | Provisions & bin-packs right-sized/spot nodes; cloud-specific providers. |
| KEDA | Pods, incl. to zero | Events: queue depth, Kafka lag, cron, PromQL… | Drives an HPA underneath; great for bursty/queue work. |
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.
“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.”
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.
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.
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
- Request = what the scheduler reserves; limit = the runtime ceiling. Over the CPU limit: throttled (compressible); over the memory limit: OOMKilled and restarted (incompressible).
- BestEffort is evicted first — earned by setting no requests or limits. (Guaranteed — limits equal requests for CPU and memory — is evicted last.)
- Least → most isolated: namespace-per-tenant → vCluster → cluster-per-tenant. Go dedicated for untrusted workloads, a hard compliance boundary, blast-radius limits, or conflicting cluster-wide needs (e.g. different K8s versions).
- 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.
- More replicas → HPA; bigger pods (requests/limits) → VPA; more nodes → Cluster Autoscaler or Karpenter; scale-to-zero on a queue → KEDA.
- 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.