Namespaces
Virtual clusters, and cross-namespace DNS names.
Two teams share one cluster. Both want to ship a Deployment named web. Both want a Service called api. Neither wants to rename anything just to stay out of the other's way. On a flat cluster, that's a fight nobody wins. Namespaces are how Kubernetes lets both teams get what they want at the same time.
Think of a namespace as a street address for your objects. A city can have a hundred houses numbered 12 and the mail still shows up, because '12' only has to be unique on its own street. The namespace is that street. A Deployment named web in the team-a namespace and a different web over in team-b are two separate objects that never collide. Most things you create live inside one namespace: Pods (the smallest thing you can deploy, one or more containers that run together), Services, Deployments, ConfigMaps, Secrets. Each name only has to be unique within its own namespace.
Namespaced things, and things that aren't
Not everything lives on a street. Some objects are cluster property. Nodes (the physical or virtual machines that actually run your workloads), PersistentVolumes (chunks of storage the cluster hands out on request), StorageClasses, and ClusterRoles all sit outside every namespace, because they describe the whole cluster instead of one tenant. Putting a Node inside a namespace would make about as much sense as giving the city power grid a house number. You can ask the API server (the control-plane process that every kubectl command talks to) which resources are which.
kubectl api-resources --namespaced=false
NAME SHORTNAMES APIVERSION NAMESPACED KINDcomponentstatuses cs v1 false ComponentStatusnamespaces ns v1 false Namespacenodes no v1 false Nodepersistentvolumes pv v1 false PersistentVolumeclusterrolebindings rbac.authorization.k8s.io/v1 false ClusterRoleBindingclusterroles rbac.authorization.k8s.io/v1 false ClusterRolestorageclasses sc storage.k8s.io/v1 false StorageClass
Swap the flag to --namespaced=true and you get the long list back: pods, services, deployments, configmaps, secrets, roles, and everything else. Don't bother memorizing that one. Learn the short cluster-scoped set instead. Knowing up front that a Node can't be namespaced saves you from a confusing error later, when you try to scope something that was never scopeable to begin with.
Make one, then make it your default
Creating a namespace is one command. The second command is the quality-of-life move most people skip: point your current kubectl context at that namespace so you stop typing -n team-a on every single command.
kubectl create namespace team-akubectl config set-context --current --namespace=team-a
namespace/team-a createdContext "kind-lab" modified.
After that, kubectl get pods means 'pods in team-a' until you switch back. That default lives in your kubeconfig, the local file kubectl reads to figure out where it's connecting and as whom, not in the cluster itself. So it only changes things for you, never for a teammate on their own machine. Check what you're pointed at any time with kubectl config view --minify and read the namespace: line.
Governance: quotas and the door they're checked at
A namespace is more than a naming trick. It's the unit you hang budgets and rules on. A ResourceQuota is a spending cap for one namespace: a ceiling on total CPU, total memory, and how many of each kind of object it's allowed to hold. Now the part people get wrong. That cap isn't enforced by some background job that sweeps through later and tidies up after the fact. It's checked at the door. An admission controller, the code that inspects every create and update request before it's written to etcd (the cluster's key-value database, the single source of truth for everything that exists), does the checking. If a new Pod would push the namespace past its quota, the API server rejects the request right then, and the Pod never gets created.
apiVersion: v1kind: ResourceQuotametadata:name: team-a-quotanamespace: team-aspec:hard:requests.cpu: "10"requests.memory: 20Gipods: "50"
Apply it with kubectl apply -f quota.yaml, then read it back. The describe output is a live meter, used versus allowed, side by side. That's exactly the screen you pull up when deploys suddenly start getting rejected and you need to know whether you've actually hit the ceiling or something else is wrong.
kubectl describe resourcequota team-a-quota -n team-a
Name: team-a-quotaNamespace: team-aResource Used Hard-------- ---- ----pods 3 50requests.cpu 600m 10requests.memory 1536Mi 20Gi
One catch surprises everyone the first time. The moment a quota sets requests.cpu, every new Pod in that namespace has to declare its own CPU request, or admission turns it away. A LimitRange fixes that by injecting a default request into any Pod that forgot to set one. That's why the two objects almost always ship together: the quota sets the ceiling, and the LimitRange makes sure ordinary Pods still fit through the door.
Talking across namespaces
Split your workloads across namespaces and they still have to find each other. Kubernetes handles that with DNS, the same name-to-address lookup the internet uses, and it gives every Service a name built from its namespace. Inside its own namespace, the short name db is enough. From another namespace, you add the namespace on the end: db.team-b. That two-part name is all you ever need, because every Pod's resolver is already set up to try svc.cluster.local on the end for you. The longer db.team-b.svc.cluster.local is the same address written out in full, the way you might put the postcode and the country on a letter that is only going across town. Both arrive. Real manifests use both, so read db.team-b as complete rather than as something half-typed. You can watch this from a throwaway Pod that runs one command and then deletes itself. Start with the name.
kubectl run netcheck --image=busybox:1.36 --rm -it --restart=Never -- \nslookup db.team-b.svc.cluster.local
Server: 10.96.0.10Address: 10.96.0.10:53Name: db.team-b.svc.cluster.localAddress: 10.100.42.7pod "netcheck" deleted
That is DNS doing its job, and nothing else. Read the Server line: the only thing this Pod actually talked to was the cluster DNS service at 10.96.0.10, which runs over in kube-system. It handed back an address that belongs to a Service in team-b, but not one packet has gone to team-b yet. A name resolving and a connection working are two different questions, and taking the first as proof of the second is the most common way people misread a blocked path. So ask the second question on its own.
kubectl run netcheck --image=busybox:1.36 --rm -it --restart=Never -- \wget --spider --timeout=3 db.team-b
Connecting to db.team-b (10.100.42.7:80)remote file existspod "netcheck" deleted
remote file exists is the line that counts: the connection opened. Same address as the lookup returned, reached with the short two-part name. Now you have proof of both things, and you know which command proved which. Which brings us to the thing administrators get wrong more than anything else.
What a namespace does not isolate
A namespace is an organizing boundary and an access-control boundary. By default, it is not a network boundary, and it is not a security boundary. The wget just proved the first half: a Pod in team-a opened a connection to a Service in team-b straight over the pod network, with no policy in the way. Put a default-deny NetworkPolicy in team-b and the nslookup would still print exactly what it printed above, because the DNS server sits in a third namespace, while that wget timed out. That is precisely why you run the two commands separately. The second half is the kernel. Two Pods from two different namespaces can land on the same Node and share that one machine's single Linux kernel. A container that breaks out of its sandbox doesn't care which namespace label was stuck on its Pod.
So think of a namespace as the surface you bolt real controls onto, not a control by itself. NetworkPolicies do the actual network segmentation; start with default-deny, then open only the paths you truly need. Role-Based Access Control (RBAC), Kubernetes' permission system, decides who can do what inside the namespace through scoped RoleBindings. ResourceQuotas cap capacity. Pod Security Admission enforces the Pod Security Standards, constraining what a Pod is even allowed to run as, and it's what you use now that the old PodSecurityPolicy has been removed. Assume a single namespace label will contain a compromised workload and you've just described how attackers walk sideways into everything else.
Terminating, forever. The namespace controller can't finish because something it's trying to clean up refuses to go, usually a custom resource whose controller is long gone, or a broken APIService (a plug-in that extends the Kubernetes API with extra resource types) that makes resource discovery fail. Don't reach for the force-remove-the-finalizer trick first. Run kubectl get apiservices | grep -v True to spot a dead add-on API, and kubectl get namespace <ns> -o yaml to read .status.conditions, which names the exact resource that's blocking cleanup. Editing the finalizer out by hand can orphan objects that were meant to be deleted along with the namespace, leaving live workloads with no owner and nothing tracking them.ResourceQuotas and LimitRanges live at namespace scope. Empty namespaces still need policy if you care about noisy neighbors.
kube-system and default are not places for app experiments. Keep lab junk in disposable namespaces.
Deleting a namespace cascades. Confirm you are not holding shared Secrets or PVCs the next team still needs.
Try this
Create two namespaces, put a Service in one, then from the other resolve its name and open a real connection to it. Run both probes, because they prove different things: one shows DNS crossing the boundary, the other shows the network crossing it. Save the quota for last, and the comment in the block explains why. Feel the boundary: isolation of names, not a hard security wall by itself.
$ kubectl api-resources --namespaced=false$ kubectl create namespace team-a$ kubectl config set-context --current --namespace=team-a# give team-b something to look up and connect to# (nginx stands in for the database, so the Service listens on 80)$ kubectl create namespace team-b$ kubectl create deployment db --image=nginx:1.27 -n team-b$ kubectl expose deployment db --port=80 -n team-b$ kubectl rollout status deployment/db -n team-b# probe from team-a: first the name, then the connection$ kubectl run netcheck --image=busybox:1.36 --rm -it --restart=Never -- \nslookup db.team-b$ kubectl run netcheck --image=busybox:1.36 --rm -it --restart=Never -- \wget --spider --timeout=3 db.team-b# quota last: once it lands, any Pod without a CPU request is refused,# so the netcheck probes above stop working until you add a LimitRange$ kubectl apply -f quota.yaml$ kubectl describe resourcequota team-a-quota -n team-a
Takeaway
Namespaces partition names and defaults. DNS crosses them with dotted names. RBAC and NetworkPolicy make the boundary real.
requests.cpu: "10". Existing Pods keep running, but every new Deployment's Pods now fail to create with a 'must specify requests.cpu' error. What actually fixes it?