K8s Basics #7: Namespaces and Labels — Organizing the Cluster
The final post of the K8s Basics series. We pin down why all our objects so far ended up in the default namespace, what Namespace actually separates, and how labels and selectors group and find objects. We close with a recap of the seven posts and a preview of the next track — K8s Intermediate.
This series is K8s Basics, 7 posts.
- #1 What is Kubernetes — why do we need a container orchestrator?
- #2 Local environments — minikube / kind / Docker Desktop k8s
- #3 kubectl and your first Pod
- #4 Deployment and ReplicaSet — declarative deploys and rolling updates
- #5 Service — ClusterIP / NodePort / LoadBalancer
- #6 ConfigMap and Secret — splitting out configuration
- #7 Namespaces and labels — organizing the cluster ← this post
The limit of the default namespace #
Add -A to list every object in the cluster, and a lot of things we didn’t create show up.
kubectl get all -ANAMESPACE NAME READY STATUS RESTARTS AGE
kube-system pod/coredns-5d78c9869d-2xvlw 1/1 Running 0 3d
kube-system pod/etcd-minikube 1/1 Running 0 3d
kube-system pod/kube-apiserver-minikube 1/1 Running 0 3d
kube-system pod/kube-controller-manager-minikube 1/1 Running 0 3d
kube-system pod/kube-proxy-7gbdn 1/1 Running 0 3d
kube-system pod/kube-scheduler-minikube 1/1 Running 0 3d
default pod/web-7f8b6c8f7d-abcde 1/1 Running 0 1hkube-system is where the control plane components K8s uses to run itself live. coredns (cluster DNS), etcd, apiserver, controller-manager, scheduler, kube-proxy — the components we drew in #1 all live here. We’ve been seeing this isolated space since #2 without naming it.
kubectl get nsNAME STATUS AGE
default Active 3d
kube-node-lease Active 3d
kube-public Active 3d
kube-system Active 3dFour system namespaces are always there by default — default, kube-system, kube-public, kube-node-lease. One line each:
default— where objects without-nend up. Every object from #1–#6 of this series gathered here.kube-system— where K8s control plane components live. End users don’t touch it directly.kube-public— for objects readable without auth. Almost never used in practice; reserved for cluster info.kube-node-lease— split out in 1.13 to make node heartbeats efficient. Operators don’t touch it directly.
While the system manages itself, problems emerge the moment you try to run dev / staging / prod in the same cluster, share the cluster across teams A and B, or split apps into separate groups. The same Service name has to exist per environment, permissions need separation per environment, and a problem in one environment shouldn’t bleed into another. Putting everything in default breaks down quickly.
What Namespace solves #
Namespace, in one line, is a virtual cluster inside a cluster. The same shape as Linux user accounts or git branches — a logical partition on top of shared physical resources. Four key things it solves:
- Name space separation — objects with the same name can sit in different namespaces. A
webService can exist independently in dev and prod without colliding. - A unit for RBAC — permissions can be granted per namespace. The base unit for policies like “team A can read and write only inside
team-anamespace, nothing else.” - A unit for resource quotas —
ResourceQuotaandLimitRangeset ceilings on CPU / memory / object count per namespace. Stops dev from eating prod’s resources. - A unit for NetworkPolicy — policies that allow or block traffic between namespaces. By default everything talks to everything; NetworkPolicy is what tightens it.
One thing worth pinning down clearly — a Namespace by itself is not a security boundary. It’s just a logical partition that splits object names. Real isolation comes from the latter three above (RBAC, ResourceQuota, NetworkPolicy). Create only a Namespace without RBAC or NetworkPolicy and an authorized user can see and touch any namespace’s objects, and Pods can talk to each other freely. This series stays at the manifest shape; the depth of RBAC / NetworkPolicy / ResourceQuota is a topic for K8s Intermediate.
Cluster-scoped vs namespace-scoped #
Objects come in two flavors. Namespace-scoped objects belong to some namespace; cluster-scoped objects exist once across the whole cluster, no namespace involved. The objects we’ve seen, sorted:
| Scope | Examples |
|---|---|
| Namespace-scoped | Pod, Deployment, ReplicaSet, Service, ConfigMap, Secret, Job, Ingress |
| Cluster-scoped | Node, PersistentVolume, Namespace itself, ClusterRole, StorageClass |
Node not being namespace-scoped is intuitive — a node is a physical or virtual machine and a cluster-level resource, not something belonging to an environment. You can also check via command which side an object falls on:
kubectl api-resources --namespaced=truekubectl api-resources --namespaced=falseWhen you’re operating and unsure whether a -n is needed for an object, this gives the answer immediately.
Creating a Namespace #
Imperative one-liner:
kubectl create namespace devnamespace/dev createdTo leave intent in git, write a manifest:
apiVersion: v1
kind: Namespace
metadata:
name: dev
labels:
env: devkubectl apply -f dev-ns.yamlapiVersion is the core group’s v1, same as ConfigMap / Secret. Namespace itself is cluster-scoped, so its metadata doesn’t have a namespace: field (and can’t have one).
Two ways to put an object in a specific namespace:
- In the manifest — write
metadata.namespace: devdirectly in the object’s metadata. - As a command flag — pass
-n devlikekubectl apply -f web.yaml -n dev.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: dev
labels:
app: web
spec:
# ... same as [#4](/en/posts/k8s-basics-4)If both are present, the manifest takes precedence (the flag only applies when the manifest has no namespace field). To reduce confusion, pick just one. From the “leave intent in git” angle, writing it in the manifest is friendlier to whoever comes next — flags only live in shell history, so without that anyone looking at the manifest is left wondering “where does this go?”
Browsing per namespace #
Use -n to look at one namespace’s objects.
kubectl get pods -n devkubectl get all -n kube-system-A looks across all namespaces at once.
kubectl get pods -ATyping -n every time is annoying. Change the current context’s default namespace and subsequent commands without -n go to that namespace.
kubectl config set-context --current --namespace=devkubectl config view --minify | grep namespace:This writes the default namespace into the current context in ~/.kube/config. After that, kubectl get pods alone returns dev’s Pods.
kubens — switch namespaces in one line #
The command above is verbose, so almost every operator uses kubens (the partner to kubectx). One line to switch namespaces.
kubens # current namespace + candidates
kubens dev # switch to dev
kubens - # back to the previous namespacekubectx switches clusters (contexts); kubens switches namespaces. They ship together — install kubectx via Homebrew / apt / scoop and kubens comes along. For anyone working with K8s daily, this is essentially mandatory ergonomics.
How objects in different namespaces call each other #
Time to revisit the flow from the Service section in #5. Pods in the same namespace called Services by their short name.
http://web/api # the Service called web, by short nameTo call a Service in another namespace, append the namespace.
http://web.prod/api # short
http://web.prod.svc.cluster.local/api # FQDNEvery Service in a K8s cluster resolves at <service>.<namespace>.svc.cluster.local. Even with the short web.prod, cluster DNS auto-completes the rest. One-line takeaway — DNS is the bridge across namespaces. Namespaces are partitions for object names; DNS is the lane that lets you call across them.
Labels vs annotations #
Pivot to the other axis of organizing the cluster — labels. Labels have been with us since #4’s selector — a Deployment’s spec.selector.matchLabels, a Pod template’s metadata.labels, a Service’s spec.selector all use labels to group and pick objects.
Often confused with labels are annotations. Both sit under metadata as key-value pairs, but their roles are different.
| Label | Annotation | |
|---|---|---|
| Used by K8s for matching | Yes (selector) | No |
| Length / content | Short, meaningful key-value (a few dozen chars) | Arbitrary — long is fine, JSON / base64 OK |
| Use | Classify / select objects | Notes left by tools or operators |
| Example keys | app=web, env=prod, tier=backend | prometheus.io/scrape: "true", kubectl.kubernetes.io/last-applied-configuration |
The short version — labels are search keys; annotations are sticky notes. What K8s controllers (Deployment, Service, NetworkPolicy) look at to decide which objects they manage is labels. What external tools (Prometheus, Helm, ArgoCD, ingress controllers) and operators stick onto an object as metadata is annotations.
The constraints on keys and values differ slightly. Labels are tighter because they’re used for selectors — keys are ASCII alphanumeric plus -, _, .; values short strings (both within a few dozen chars). Annotations have those constraints relaxed, so arbitrary text or JSON can sit in them. The kubectl.kubernetes.io/last-applied-configuration annotation holding an entire manifest as JSON is one example.
metadata:
name: web
labels:
app: web
env: prod
tier: backend
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
kubernetes.io/change-cause: "bump nginx 1.27 -> 1.28"Standard label conventions — app.kubernetes.io/* #
You can attach any keys you want to labels (app=web, etc.), but the K8s community recommends a standard set. Six keys with the prefix app.kubernetes.io/.
| Key | Meaning | Example values |
|---|---|---|
app.kubernetes.io/name | App name | nginx, web, kafka |
app.kubernetes.io/instance | This deployment instance’s identifier | web-prod, kafka-shop |
app.kubernetes.io/version | Version | 1.27, 2.4.1 |
app.kubernetes.io/component | Role | frontend, backend, database |
app.kubernetes.io/part-of | Parent system | shop-platform, analytics |
app.kubernetes.io/managed-by | Tool that manages it | Helm, argocd, kubectl |
These keys exist because operational tools and dashboards recognize them as standard. Lens, k9s, Datadog, Helm — they all look at these keys to group objects in their views. Compatibility is clearly better than custom-only keys. Mixing custom keys (e.g., env, team) with the standard set is normal and recommended.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: prod
labels:
app.kubernetes.io/name: nginx
app.kubernetes.io/instance: web-prod
app.kubernetes.io/version: "1.27"
app.kubernetes.io/component: frontend
app.kubernetes.io/part-of: shop-platform
app.kubernetes.io/managed-by: kubectl
env: prod
spec:
replicas: 3
selector:
matchLabels:
app.kubernetes.io/name: nginx
app.kubernetes.io/instance: web-prod
template:
metadata:
labels:
app.kubernetes.io/name: nginx
app.kubernetes.io/instance: web-prod
app.kubernetes.io/version: "1.27"
app.kubernetes.io/component: frontend
env: prod
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80Inside the selector’s matchLabels and the Pod template’s labels, it’s safer to include only the labels that don’t change. Putting version in the selector means you’d have to update the selector when the version bumps — and that’s the selector immutability trap from #4 waiting for you.
Picking objects by label — kubectl -l #
If labels are attached, kubectl’s -l flag picks objects by them. Selector syntax goes from simple equality to set expressions.
kubectl get pods -l app=web
kubectl get pods -l env=prod,tier=backend # ANDComma-separated is AND. For OR, use the set form.
kubectl get pods -l 'env in (dev,staging)'
kubectl get pods -l 'env notin (prod)'
kubectl get pods -l 'tier' # has the tier label
kubectl get pods -l '!debug' # doesn't have the debug labelAcross multiple object kinds:
kubectl get deploy,svc,cm -l app.kubernetes.io/instance=web-prodkubectl delete pods -l env=devThat last command is powerful — it deletes every matching Pod at once. People have caused production incidents by mistyping a label and deleting more than intended. Before bulk delete, run get with the same selector first to confirm the targets are what you expect.
The -l syntax matters because the same syntax is used for Service’s spec.selector, Deployment’s spec.selector.matchLabels, NetworkPolicy’s podSelector, ResourceQuota’s scopeSelector — almost every object matching across K8s. Get labels right once and the selectors layered on top read naturally.
One ops snapshot — putting it all together #
Stack everything we’ve built so far into one manifest set, and the standard shape of an ops cluster appears. Three namespaces — dev / staging / prod — each with the same Deployment / Service / ConfigMap / Secret names, with labels marking environment and version.
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: prod
labels:
app.kubernetes.io/name: nginx
app.kubernetes.io/instance: web-prod
app.kubernetes.io/version: "1.27"
app.kubernetes.io/component: frontend
app.kubernetes.io/part-of: shop-platform
env: prod
spec:
replicas: 3
selector:
matchLabels:
app.kubernetes.io/name: nginx
app.kubernetes.io/instance: web-prod
template:
metadata:
labels:
app.kubernetes.io/name: nginx
app.kubernetes.io/instance: web-prod
app.kubernetes.io/component: frontend
env: prod
spec:
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
envFrom:
- configMapRef:
name: web-config
- secretRef:
name: db-secret
---
apiVersion: v1
kind: Service
metadata:
name: web
namespace: prod
labels:
app.kubernetes.io/name: nginx
app.kubernetes.io/instance: web-prod
env: prod
spec:
selector:
app.kubernetes.io/name: nginx
app.kubernetes.io/instance: web-prod
ports:
- port: 80
targetPort: 80dev and staging have nearly the same manifest — only metadata.namespace, instance, version, and the env label differ per environment; everything else stays. This shape is the foundation of a cluster where objects are cleanly separated by environment within a single cluster.
When you have to apply nearly-the-same manifest with small differences across environments, doing it by hand each time is error-prone. That’s where Helm (templates + values) and Kustomize (base + overlays) come in. Both are out of scope for this series; they’ll come up in K8s Advanced / In Practice.
Clean up #
Tear down the dev namespace from this post.
kubectl delete ns devnamespace "dev" deletedWhat makes that command dangerous is that everything inside that namespace disappears with it. Deployments, Services, Pods, ConfigMaps, Secrets, PVCs — all deleted asynchronously. In a production cluster, deleting a namespace is essentially a last-resort move — once executed, data inside can be lost, and depending on the PV reclaimPolicy, even the disk underneath might go.
kubectl config set-context --current --namespace=defaultIf you have kubens installed, it’s kubens default — one line.
Series recap — what these 7 posts gave you #
A recap of the 7 posts to close the series.
- #1 — Why K8s. The ceiling of single-host containers, what an orchestrator solves, the control plane / worker picture.
- #2 — Local clusters. Use cases and differences across minikube / kind / Docker Desktop, kubectl contexts.
- #3 — First Pod. The manifest spine (
apiVersion / kind / metadata / spec),kubectl apply/get/describe/logs/exec. - #4 — Deployment and ReplicaSet. Declarative deploys, rolling updates,
kubectl rollout, selector immutability. - #5 — Service. The three steps ClusterIP / NodePort / LoadBalancer, cluster-internal DNS.
- #6 — ConfigMap and Secret. 12-factor’s “store config in the environment,” three injection methods env / envFrom / volume.
- #7 Namespaces and labels — organizing the cluster ← this post.
By this point, you can read and write a K8s manifest you’ve never seen before and tell what it means. Open up any company cluster’s manifest directory and the object kinds and field names won’t feel foreign. The deeper topics layered on top are the next track.
Next — K8s Intermediate #
The topics intentionally postponed in this series are the outline of K8s Intermediate’s 7 posts.
| Topic | Description |
|---|---|
| StatefulSet / DaemonSet / Job / CronJob | The other controllers besides Deployment. Databases, node agents, one-shot batches, scheduled batches. |
| PV / PVC / StorageClass | Persistent data. The disk model that survives a Pod’s death. |
| Ingress + Ingress Controller | Concentrating external entry points in one place. Controllers like nginx / Traefik / GKE Ingress. |
| resources.requests / limits | A Pod’s CPU / memory requests and limits. The basis of scheduling and OOM. |
| Health check | liveness / readiness / startup probes. How K8s decides a container is alive. |
| HPA / VPA / Cluster Autoscaler | Auto-adjusting Pod count, Pod resources, and node count to load. |
| RBAC / NetworkPolicy / ResourceQuota | The depth of security and resource policy we touched briefly here. |
After working through those seven, you’ll be one step closer to writing your own manifests in a company cluster’s manifest directory. The tracks after that — K8s Advanced and K8s Practice — cover Helm, Kustomize, GitOps (ArgoCD / Flux), observability (Prometheus / Grafana / Loki), and cluster operations (upgrades, backups, multi-cluster) in earnest.
Wrap #
The Basics series — 7 posts on the fundamentals of K8s. From a single host to bringing up a cluster, writing your first manifest, keeping multiple Pods alive reliably, exposing them externally, and pulling config and secrets out of the body. If you can understand which objects a manifest produces inside a cluster and how those objects connect, this series has done its job. K8s Intermediate picks up the deeper topics on top of this foundation, one by one.