Platform Engineering on Kubernetes
Every deep-dive on this course so far has assumed one operator running kubectl against one cluster. That assumption holds for a homelab and starts to strain the moment a cluster serves more than a couple of product teams — because Kubernetes itself has no opinion on how a developer asks for a new namespace, a database, or a running service; it only has an API you can extend. This page is the Kubernetes-side half of a two-course story: it stays inside kubectl, namespaces, CRDs, and RBAC to show what actually changes on a cluster once a platform team stands up self-service on top of it — the guardrails a namespace needs before it's safe to hand to a team, the CRD-and-operator mechanism that turns eight raw manifests into one declarative request, and the ladder from namespace isolation up to a dedicated cluster. The sibling Platform Engineering course is where the organizational case, the cognitive-load argument, and the CNPE-certified skill set live in full; this page only covers the half of it built out of Kubernetes primitives you already know.
Think of a freshly built apartment building. Kubernetes is the building itself — pipes already run to every unit, wiring already in every wall, an elevator that already works. That's real infrastructure, but nobody's actually living there yet, and nothing stops a new tenant from repainting a hallway or running a space heater that trips the whole building's breaker. A platform team is the building's management office: before handing over a key, they set a water and power allowance for each unit, install a lock that only that tenant's key opens, and offer a pre-approved "starter apartment" package — furniture, paint, everything already chosen — so most new tenants take the package instead of designing their unit from scratch, wiring diagram and all.
Why this page goes past the exam blueprint
☺ Like you're 10: None of KCNA, CKAD, CKA, or CKS ever asks whether a cluster has a working way for a new team to self-serve a namespace — that's a real production problem the exams simply don't reach.
Every blueprint page and most deep-dives in this course quietly assume one operator running kubectl by hand against one cluster, and that assumption is fine for a homelab or a five-person startup. It breaks the moment a cluster is shared by more than a couple of product teams: someone has to decide who gets a namespace and how big, what guardrails come with it by default, and how a developer asks for a running service without either filing a ticket or being handed cluster-admin "just to get it working." Kubernetes itself answers none of these questions — it gives you the primitives (Namespace, RBAC, ResourceQuota, CustomResourceDefinition) and stays silent on how a platform team should assemble them into something a developer can self-serve. Filling that silence in is what this page walks through, staying entirely inside objects you already know from earlier in this course.
This is deliberately the narrow, Kubernetes-mechanics half of a much bigger topic. The organizational case for why a platform team exists at all — the cognitive-load argument, Team Topologies, the Internal Developer Platform vs. Internal Developer Portal distinction, and the full CNCF Platform Engineering (CNPE) certification path — belongs to the sibling Platform Engineering course, starting at What & Why We Platform; its own deep dive, Kubernetes as the Platform Substrate, covers the control-plane internals this page assumes you already have from Control Plane Internals. This page won't re-derive any of that — treat it as the bridge from "I know Kubernetes" to "I understand what a platform team builds on top of it." And if your own certification path runs through the full CNCF ladder rather than just Kubernetes' own exams, the Golden Astronaut course covers the other nine CNCF projects and the LFCS on the way to Golden Kubestronaut.
Kubernetes is a substrate, not a platform
☺ Like you're 10: Kubernetes hands you the plumbing and wiring for every apartment in the building — it has no opinion on how a new tenant actually gets a key.
It's worth stating plainly, because it's easy to blur: Kubernetes ships scheduling, self-healing, service discovery, and rollout primitives, and it is not, by itself, an internal developer platform. A Deployment, a Service, and a NetworkPolicy are building blocks a platform is built out of, not a platform on their own — nothing about a fresh cluster tells a new engineer which namespace is theirs, what resource footprint is reasonable, or which twelve fields of YAML they can safely ignore. What makes Kubernetes the substrate almost every real platform still chooses, rather than a decorative layer nobody needed, is that its API is genuinely extensible: a CustomResourceDefinition lets a platform team register their own noun — WebService, DatabaseClaim, Environment — as a first-class object in the same API server that already runs Pods and Deployments, and an operator reconciling that CRD is exactly the same reflector-informer-workqueue machinery Control Plane Internals walked through for kube-scheduler and kube-controller-manager. A platform team's custom API isn't a wrapper bolted on the side; it inherits kubectl get, RBAC, admission control, and kubectl explain for free, the moment it's registered.
A golden path built on a CRD isn't a script that happens to call the Kubernetes API — it is the Kubernetes API, extended. That's the whole reason platform teams keep reaching for Kubernetes instead of writing a bespoke provisioning service: they inherit watch, RBAC, versioning, and a reconcile loop as free behavior, not something they have to build from scratch.
Namespaces: the unit self-service is built on
☺ Like you're 10: A namespace is a name tag on a folder, not a locked door — the lock is four separate objects you have to add yourself.
A Namespace is the natural unit to hand a team, but a bare namespace is a filing convenience, not an isolation boundary — its Pods still share every node's kernel, and without anything else applied, workloads inside it can consume unlimited CPU and memory, reach every Pod in every other namespace, and be edited by anyone holding any RoleBinding anywhere in the cluster. A namespace becomes safe to hand to a team, self-service, only once it's paired with the guardrails that make its edges real:
- A ResourceQuota caps the namespace's aggregate footprint — total CPU/memory requests and limits, object counts, even whether it may request its own cloud LoadBalancer — so one team's runaway HorizontalPodAutoscaler can't starve every other tenant on the cluster of schedulable capacity.
- A LimitRange sets per-container defaults and min/max bounds, so a developer who forgets
resources:entirely — the single most common mistake covered in Scheduling & Resource Management — gets a sane default injected instead of an unbounded container or a Pod rejected outright. - A RoleBinding to a namespace-scoped Role, or to a built-in ClusterRole such as
edit, grants the team's identity real permissions inside their own namespace and nothing outside it — the mechanics live in RBAC & Admission Control. - A default-deny NetworkPolicy stops the namespace from being reachable by every other Pod in the cluster by default, so cross-namespace traffic becomes an explicit, reviewable allow rule instead of the accidental default.
Bundle those four with a Pod Security Admission label and you have a template a platform team applies once per new tenant — by hand the first time, then through the same GitOps pipeline that ships everything else, so "provision a namespace" becomes a pull request instead of a ticket:
apiVersion: v1
kind: Namespace
metadata:
name: team-checkout
labels:
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/warn: restricted
platform.acme.io/owner: team-checkout
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-checkout-quota
namespace: team-checkout
spec:
hard:
requests.cpu: "20"
requests.memory: 40Gi
limits.cpu: "40"
limits.memory: 80Gi
pods: "100"
services.loadbalancers: "0" # no direct cloud LBs — traffic goes through the shared ingress
---
apiVersion: v1
kind: LimitRange
metadata:
name: team-checkout-defaults
namespace: team-checkout
spec:
limits:
- type: Container
default: { cpu: 250m, memory: 256Mi } # injected if a container omits limits
defaultRequest: { cpu: 100m, memory: 128Mi } # injected if a container omits requests
max: { cpu: "2", memory: 4Gi }
min: { cpu: 10m, memory: 16Mi }
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: team-checkout-edit
namespace: team-checkout
subjects:
- kind: Group
name: team-checkout-devs
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: edit # built-in ClusterRole, scoped to this namespace by the binding
apiGroup: rbac.authorization.k8s.io
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: team-checkout
spec:
podSelector: {}
policyTypes: [Ingress]
# no ingress rules listed = deny by default; the team adds its own allow rules on top"Just give the team a namespace" without the four guardrails above is the anti-pattern that looks like self-service and behaves like a shared blast radius — one team's misconfigured Deployment can page every other team on the cluster. A namespace with no ResourceQuota isn't a smaller cluster; it's the same cluster with an extra label on part of it.
On any cluster you can reach — a kind cluster is enough — apply the namespace and ResourceQuota above, then request more CPU than the quota has left: kubectl run hog --image=nginx --requests=cpu=100 -n team-checkout is rejected immediately, at admission time, before it ever reaches the scheduler. That rejection message is the guardrail doing exactly its job, not a bug to route around.
Golden paths: turning eight manifests into one request
☺ Like you're 10: Instead of handing a new tenant a stack of forms for wiring, plumbing, and the fire code, you hand them one signed lease — and the building fills the forms in itself.
A guarded namespace solves who can touch what; it says nothing about how much a developer still has to write to actually ship something. Requesting one small service, hand-written, is realistically six to eight separate objects — Deployment, Service, Ingress, NetworkPolicy, resource requests sized against the ResourceQuota, and often a PodDisruptionBudget and a HorizontalPodAutoscaler on top — each one a place to get a probe path wrong, forget runAsNonRoot, or copy stale resource limits from the last team's example. Kubernetes gives platform teams two ways to pave over that, at two different depths.
The lighter-weight option is templating: a Helm chart or a Kustomize base with per-team overlays still produces the same six-to-eight raw objects, but a developer fills in a handful of values instead of writing YAML from a blank file — fewer places to get it wrong, though every object still lands in the cluster as something a human templated, not something the cluster itself decided to create. The deeper option — the one that actually turns the request into a single declarative object, the way Operators & Custom Resource Definitions covers building — is a CRD purpose-built for the golden path itself:
apiVersion: platform.acme.io/v1alpha1
kind: WebService
metadata:
name: checkout
namespace: team-checkout
spec:
image: registry.acme.io/checkout:1.4.2
port: 8080
replicas: 3
route:
host: checkout.acme.io
resources:
profile: small # platform-defined presets, not raw cpu/memory numbers to guess atA developer applies that one document; a platform-owned operator, watching that CRD exactly the way Control Plane Internals described kube-controller-manager watching Deployments, expands it into the Deployment, Service, Ingress, NetworkPolicy, and PodDisruptionBudget the team would otherwise have hand-written — each generated object labeled back to the WebService that produced it, so nothing sitting in the namespace is unowned:
$ kubectl apply -f webservice.yaml webservice.platform.acme.io/checkout created $ kubectl get webservice checkout -n team-checkout NAME READY URL checkout True https://checkout.acme.io $ kubectl get deploy,svc,ingress,networkpolicy -n team-checkout -l platform.acme.io/owner=checkout # every object below was generated by the WebService controller, not hand-written
Which depth is right depends entirely on how many teams are asking for the same shape of thing. A one-off internal tool doesn't earn an operator; a request pattern forty teams repeat every month earns exactly one, because every hour spent generalizing it is repaid forty times over. Platform Engineering's own Platform APIs & CRDs and Self-Service go deep on designing that CRD's schema, its defaults, and its validation — this page only needed to show that the mechanism is the exact same reconcile loop the rest of this course already taught you to read.
Multi-tenancy: how far self-service goes before you need a new cluster
☺ Like you're 10: Roommates who trust each other share one apartment with house rules; roommates who don't get their own separate apartments — the platform team decides which situation each team is actually in.
Multi-Cluster & Fleet Management already laid out the ladder from a well-configured namespace, to a virtual cluster giving a team its own API server on a shared host, to a genuinely separate cluster, to a separate region entirely — and the cost tax that climbs with every rung. What that page didn't dwell on is who decides, and a platform team answers that question constantly: every new tenant lands somewhere on this ladder, and picking the rung is a trust decision, not a convenience one. A namespace with the guardrails from earlier on this page is the right default for teams that trust each other and share a compliance boundary — most internal product teams, most of the time. Escalating a rung is warranted by a genuine driver: a regulatory boundary that legally can't share infrastructure, a tenant running arbitrary or untrusted code, or a workload whose noisy-neighbor scheduling pressure keeps degrading everyone else sharing the node pool. Wanting a nicer dashboard, or not wanting to ask the platform team for a ResourceQuota bump, is not a driver — it's the over-provisioning half of the same anti-pattern Anti-Patterns & Pitfalls already named for reaching for Kubernetes when a namespace, or even a single managed service, would have done.
"A team building an internal 'run arbitrary code a customer uploaded' feature asked for a bigger ResourceQuota on their existing namespace. Wrong rung entirely — the request wasn't for more capacity, it was to run genuinely untrusted code next to seventeen other teams sharing the same kernel. We moved them to their own cluster, not because the ladder says 'untrusted code' in bold anywhere, but because the actual question — could a bug in one tenant's Pod escalate into another tenant's data — had a different answer for them than for anyone else on that host. Read the request behind the request, not just the YAML in front of you."
Why platform teams exist, in Kubernetes terms
☺ Like you're 10: The building's own plumbing being excellent doesn't stop a new tenant from flooding their unit — someone still has to own the rules and the starter package.
Put the last three sections together and the shape of a platform team's actual Kubernetes-facing job appears without needing any outside theory: they own the namespace template that makes a tenant safe to onboard, they own the CRDs and operators that turn a repeated request into one declarative object, and they own the ladder decisions that keep a trusted-tenant assumption from quietly becoming false. None of that requires Kubernetes to change — every mechanism above is a primitive this course already covered on its own terms. What changes is who is accountable for assembling those primitives into something a developer never has to think about twice.
That's also exactly where this page stops and hands off. Why this role exists at all — the cognitive-load argument for shrinking a developer's YAML surface area to near zero, the Team Topologies vocabulary for what a platform team is versus a stream-aligned team, and the "platform as a product, not a ticket queue" framing that keeps a golden path from becoming a gate — is argued in full, from first principles, in Platform Engineering's own What & Why We Platform. This page showed you the Kubernetes half; that page is the other half.
Benny the Beaver: Team Orbit wants a new service live by Friday. I could just hand them the Deployment YAML from the last team and let them edit it.
Gizmo the Gremlin: Faster idea — skip the ResourceQuota, skip the NetworkPolicy, they're a good team, they won't break anything. And just give them edit on the whole cluster while you're at it, saves you writing a RoleBinding every time.
Timmy the Turtle: A "good team" still ships a bug. The quota isn't about trusting Orbit — it's about every other namespace on this cluster not depending on Orbit never making a mistake.
Professor Owl: And hand-copied YAML drifts the moment two teams "edit" it slightly differently. A CRD doesn't drift — the operator regenerates the same objects from the same schema, every single time.
Foxy: So what does Orbit actually type on Friday?
Benny the Beaver: One WebService, twelve lines, into a namespace that already has its guardrails applied. Everything Gizmo wanted to skip is still there — Orbit just never has to look at it.
1. Why is a bare namespace, with nothing else applied, not actually an isolation boundary — and what four objects turn it into one? 2. What's the practical difference between templating a golden path with Helm or Kustomize and building it as a CRD reconciled by an operator? 3. Name two genuine drivers for moving a tenant up the multi-tenancy ladder from a namespace to a separate cluster, and one thing that sounds like a driver but isn't. 4. Why does a platform team's own CRD get RBAC, watch, and kubectl get for free, instead of the platform team having to build those from scratch? 5. In one sentence, what does this page mean by "Kubernetes is a substrate, not a platform"? 6. Where does the organizational case for why platform teams exist — cognitive load, Team Topologies — actually live, and why doesn't this page repeat it?
Check your answers
- Its Pods still share every node's kernel and, without more applied, face no cap on resource consumption, no restriction on cross-namespace network traffic, and no scoped-down permissions. A ResourceQuota, a LimitRange, a RoleBinding, and a default-deny NetworkPolicy are what turn it from a filing convenience into a real boundary.
- Templating (Helm/Kustomize) still produces the same raw objects, just filled in from fewer inputs by a human — a developer still applies six-to-eight hand-templated manifests. A CRD-and-operator golden path replaces those manifests with one declarative object; a platform-owned controller generates the rest server-side, so nothing is hand-typed at all.
- Genuine drivers: a regulatory/compliance boundary that legally can't share infrastructure, or a tenant running arbitrary/untrusted code (or, per the multi-cluster ladder, workloads whose noisy-neighbor scheduling pressure degrades other tenants). Not a driver: wanting a nicer dashboard or convenience rather than an actual trust or isolation problem.
- Because a CustomResourceDefinition registers the platform team's own object as a first-class citizen of the same Kubernetes API server that already runs Pods and Deployments — it automatically inherits the API machinery (watch, RBAC, admission, versioning, kubectl compatibility) rather than the platform team reimplementing any of that themselves.
- Kubernetes supplies powerful primitives and an extensible API, but no opinion on how a developer should self-serve a namespace, a service, or a database — a platform team has to assemble those primitives into that self-service experience themselves.
- It lives in Platform Engineering's own foundations, starting at "What & Why We Platform." This page doesn't repeat it because that argument isn't Kubernetes-specific — it's organizational and applies whether the platform sits on Kubernetes or something else entirely, so it belongs to the course built around that case, not this one.