K8s Basics #7: Namespaces and Labels — Organizing the Cluster

13 min read

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.

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.

See objects across all namespaces
kubectl get all -A
Example output — excerpt
NAMESPACE     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          1h

kube-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.

List namespaces
kubectl get ns
Example output
NAME              STATUS   AGE
default           Active   3d
kube-node-lease   Active   3d
kube-public       Active   3d
kube-system       Active   3d

Four system namespaces are always there by default — default, kube-system, kube-public, kube-node-lease. One line each:

  • default — where objects without -n end 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 web Service 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-a namespace, nothing else.”
  • A unit for resource quotasResourceQuota and LimitRange set 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:

ScopeExamples
Namespace-scopedPod, Deployment, ReplicaSet, Service, ConfigMap, Secret, Job, Ingress
Cluster-scopedNode, 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:

List namespace-scoped objects
kubectl api-resources --namespaced=true
List cluster-scoped objects
kubectl api-resources --namespaced=false

When you’re operating and unsure whether a -n is needed for an object, this gives the answer immediately.

Creating a Namespace #

Imperative one-liner:

Imperatively
kubectl create namespace dev
Example output
namespace/dev created

To leave intent in git, write a manifest:

dev-ns.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: dev
  labels:
    env: dev
apply
kubectl apply -f dev-ns.yaml

apiVersion 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: dev directly in the object’s metadata.
  • As a command flag — pass -n dev like kubectl apply -f web.yaml -n dev.
With metadata.namespace
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.

Pods in the dev namespace
kubectl get pods -n dev
All objects in kube-system
kubectl get all -n kube-system

-A looks across all namespaces at once.

All namespaces in one shot
kubectl get pods -A

Typing -n every time is annoying. Change the current context’s default namespace and subsequent commands without -n go to that namespace.

Switch the default namespace
kubectl config set-context --current --namespace=dev
Verify the current context
kubectl 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.

Using kubens
kubens                  # current namespace + candidates
kubens dev              # switch to dev
kubens -                # back to the previous namespace

kubectx 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.

Inside the same namespace
http://web/api          # the Service called web, by short name

To call a Service in another namespace, append the namespace.

A Service in another namespace
http://web.prod/api                     # short
http://web.prod.svc.cluster.local/api   # FQDN

Every 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.

LabelAnnotation
Used by K8s for matchingYes (selector)No
Length / contentShort, meaningful key-value (a few dozen chars)Arbitrary — long is fine, JSON / base64 OK
UseClassify / select objectsNotes left by tools or operators
Example keysapp=web, env=prod, tier=backendprometheus.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.

Labels and annotations side by side in metadata
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/.

KeyMeaningExample values
app.kubernetes.io/nameApp namenginx, web, kafka
app.kubernetes.io/instanceThis deployment instance’s identifierweb-prod, kafka-shop
app.kubernetes.io/versionVersion1.27, 2.4.1
app.kubernetes.io/componentRolefrontend, backend, database
app.kubernetes.io/part-ofParent systemshop-platform, analytics
app.kubernetes.io/managed-byTool that manages itHelm, 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.

A Deployment with the standard labels
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: 80

Inside 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.

Equality matching
kubectl get pods -l app=web
kubectl get pods -l env=prod,tier=backend         # AND

Comma-separated is AND. For OR, use the set form.

Set expressions
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 label

Across multiple object kinds:

Across kinds with one label selector
kubectl get deploy,svc,cm -l app.kubernetes.io/instance=web-prod
Bulk delete by label — dangerous
kubectl delete pods -l env=dev

That 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.

prod namespace — Deployment + Service
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: 80

dev 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.

Delete the namespace
kubectl delete ns dev
Example output
namespace "dev" deleted

What 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.

Reset the default namespace
kubectl config set-context --current --namespace=default

If 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.

  • #1Why K8s. The ceiling of single-host containers, what an orchestrator solves, the control plane / worker picture.
  • #2Local clusters. Use cases and differences across minikube / kind / Docker Desktop, kubectl contexts.
  • #3First Pod. The manifest spine (apiVersion / kind / metadata / spec), kubectl apply / get / describe / logs / exec.
  • #4Deployment and ReplicaSet. Declarative deploys, rolling updates, kubectl rollout, selector immutability.
  • #5Service. The three steps ClusterIP / NodePort / LoadBalancer, cluster-internal DNS.
  • #6ConfigMap 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.

TopicDescription
StatefulSet / DaemonSet / Job / CronJobThe other controllers besides Deployment. Databases, node agents, one-shot batches, scheduled batches.
PV / PVC / StorageClassPersistent data. The disk model that survives a Pod’s death.
Ingress + Ingress ControllerConcentrating external entry points in one place. Controllers like nginx / Traefik / GKE Ingress.
resources.requests / limitsA Pod’s CPU / memory requests and limits. The basis of scheduling and OOM.
Health checkliveness / readiness / startup probes. How K8s decides a container is alive.
HPA / VPA / Cluster AutoscalerAuto-adjusting Pod count, Pod resources, and node count to load.
RBAC / NetworkPolicy / ResourceQuotaThe 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.

X