Namespaces

Virtual clusters, and cross-namespace DNS names.

Beginner8 min · lesson 10 of 65
In plain terms
A namespace is an apartment inside a building. Your “kitchen” and your neighbor’s “kitchen” don’t clash, and the building sets separate rules and a separate budget for each apartment.

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.

list cluster-scoped resource kinds
kubectl api-resources --namespaced=false
output
NAME SHORTNAMES APIVERSION NAMESPACED KIND
componentstatuses cs v1 false ComponentStatus
namespaces ns v1 false Namespace
nodes no v1 false Node
persistentvolumes pv v1 false PersistentVolume
clusterrolebindings rbac.authorization.k8s.io/v1 false ClusterRoleBinding
clusterroles rbac.authorization.k8s.io/v1 false ClusterRole
storageclasses 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.

create and switch
kubectl create namespace team-a
kubectl config set-context --current --namespace=team-a
output
namespace/team-a created
Context "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.

quota.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-a-quota
namespace: team-a
spec:
hard:
requests.cpu: "10"
requests.memory: 20Gi
pods: "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.

verify usage vs cap
kubectl describe resourcequota team-a-quota -n team-a
output
Name: team-a-quota
Namespace: team-a
Resource Used Hard
-------- ---- ----
pods 3 50
requests.cpu 600m 10
requests.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.

Diagram
Cluster-scoped (no namespace)
Node
worker machine
PersistentVolume
cluster storage
StorageClass
cluster-wide
ClusterRole
cluster access rules
Namespace: team-a
Deployment web
scoped name
Service api
api.team-a.svc
ResourceQuota
CPU/mem cap
RoleBinding
scoped access rules
Namespace: team-b
Deployment web
same name, no clash
Service db
db.team-b.svc
Secret
scoped name

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.

resolve a service from another namespace
kubectl run netcheck --image=busybox:1.36 --rm -it --restart=Never -- \
nslookup db.team-b.svc.cluster.local
output
Server: 10.96.0.10
Address: 10.96.0.10:53
Name: db.team-b.svc.cluster.local
Address: 10.100.42.7
pod "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.

now open a real connection to it
kubectl run netcheck --image=busybox:1.36 --rm -it --restart=Never -- \
wget --spider --timeout=3 db.team-b
output
Connecting to db.team-b (10.100.42.7:80)
remote file exists
pod "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.

The namespace stuck in Terminating
Delete a namespace and it should vanish in seconds. Sometimes it just sits there in 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.

terminal
$ 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.

Quick check
01A namespace runs fine until you apply a ResourceQuota that sets 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?
Incorrect — Usage sits far below 10 CPUs, and the error is about a missing field, not exhausted capacity. Raising the cap changes nothing.
Correct — Once a quota constrains requests.cpu, admission requires every Pod to declare a CPU request. A LimitRange injects a default so existing manifests keep working untouched.
Incorrect — The requirement follows the ResourceQuota, not the age of the namespace. A fresh namespace with the same quota behaves identically.
Incorrect — RBAC controls who may create objects, not whether a Pod satisfies the quota. The request is still rejected for the missing request field.
02From a pod in team-a, an nslookup of db.team-b.svc.cluster.local resolves and the pod can reach that Service with no policy in the way. What does this demonstrate about namespaces?
Incorrect — Cross-namespace DNS and pod-network reachability have nothing to do with who created the namespaces.
Incorrect — Namespaces do not encrypt anything; this is about reachability, not encryption.
Correct — the flat pod network lets any pod reach a Service in another namespace unless a NetworkPolicy restricts it, so a namespace is a surface to bolt controls onto, not a boundary by itself.
Incorrect — No misconfiguration is required; default cluster networking already permits this reach.
03You delete a namespace and it sits in Terminating indefinitely. What is the right first move?
Incorrect — Force-removing the finalizer can orphan objects meant to be deleted with the namespace, leaving live workloads untracked; it is a last resort, not a first move.
Correct — a stuck termination is usually a dead APIService failing resource discovery or a custom resource whose controller is gone, and those commands name the actual blocker so you fix the cause instead of orphaning objects.
Incorrect — The controller is already retrying; restarting the API server does not clear the resource that refuses to be cleaned up.
Incorrect — You cannot recreate a namespace still in Terminating, and it would not address the blocking resource anyway.

Related