Kubernetes Fundamentals

Namespaces & Labels

18 min Lesson 7 of 32

Namespaces & Labels

A fresh Kubernetes cluster feels clean. A production cluster three months later — running payment services, analytics pipelines, a monitoring stack, and two experimental features — is a different story. Without deliberate organization, every team shares the same flat namespace, labels are inconsistent, and kubectl get pods returns hundreds of results with no clear ownership. This lesson covers the two primary mechanisms Kubernetes provides to solve this: namespaces (logical partitions of the cluster) and labels with selectors (a flexible key-value tagging and query system that drives most of the control-plane's own logic).

Kubernetes Namespaces: Logical Cluster Partitioning

A Kubernetes namespace is a virtual sub-cluster. It scopes most API objects (Pods, Services, Deployments, ConfigMaps, Secrets, ServiceAccounts, ResourceQuotas) so that names only need to be unique within a namespace, not across the entire cluster. This allows multiple teams or environments to coexist on shared infrastructure without stepping on each other.

Every cluster ships with four built-in namespaces:

  • default — where your objects land when you do not specify a namespace. In production, deploying into default is an anti-pattern; it mixes everything together and makes RBAC and quota enforcement impossible to scope.
  • kube-system — reserved for cluster components (kube-dns, kube-proxy, metrics-server, CNI plugins). Never place application workloads here; accidental deletion of a component is catastrophic.
  • kube-public — publicly readable even by unauthenticated users. Only used for the cluster-info ConfigMap by bootstrapping tooling.
  • kube-node-lease — holds Lease objects used by the node heartbeat mechanism (Node lifecycle controller reads these to detect node failures). Do not touch it.
Key idea: Namespaces are a resource-management and access-control boundary, not a security boundary. Pods in different namespaces can communicate freely over the cluster network unless you explicitly block them with NetworkPolicies. Do not rely on namespace separation alone to isolate hostile workloads — add NetworkPolicies and RBAC.

Creating and Using Namespaces

Create namespaces imperatively or with a manifest. Always prefer manifests in production so that namespace configuration is version-controlled:

# Imperative (fast but not version-controlled) kubectl create namespace payments # Declarative (preferred) — namespace.yaml # --- # apiVersion: v1 # kind: Namespace # metadata: # name: payments # labels: # team: platform # env: production kubectl apply -f namespace.yaml # Switch your active context to a namespace (saves typing --namespace on every command) kubectl config set-context --current --namespace=payments # Verify — every subsequent command now scopes to 'payments' kubectl config view --minify | grep namespace: # List all namespaces kubectl get namespaces # List pods in a specific namespace (overrides context default) kubectl get pods -n kube-system # List pods across ALL namespaces (useful for cluster-wide audits) kubectl get pods --all-namespaces # or the equivalent short form: kubectl get pods -A

The namespace is declared in every manifest under metadata.namespace. If omitted, the object lands in whatever namespace your kubeconfig context points at — usually default. In production pipelines, always be explicit:

apiVersion: apps/v1 kind: Deployment metadata: name: checkout-api namespace: payments # <-- always explicit in production manifests labels: app: checkout-api team: payments env: production spec: replicas: 3 selector: matchLabels: app: checkout-api template: metadata: labels: app: checkout-api team: payments env: production version: "2.4.1" spec: containers: - name: checkout-api image: myregistry/checkout-api:2.4.1 ports: - containerPort: 8080

ResourceQuotas and LimitRanges: Enforcing Guardrails

A namespace without quotas is an unguarded blast radius. One team's runaway job can consume all cluster memory and starve everyone else. Attach a ResourceQuota to every production namespace to set hard ceilings and a LimitRange to set per-Pod defaults:

# resource-quota.yaml — hard limits for the 'payments' namespace apiVersion: v1 kind: ResourceQuota metadata: name: payments-quota namespace: payments spec: hard: requests.cpu: "8" # Total CPU requests across all Pods requests.memory: 16Gi # Total memory requests limits.cpu: "16" # Total CPU limits limits.memory: 32Gi # Total memory limits count/pods: "50" # Max number of Pods count/services: "20" count/secrets: "100" count/configmaps: "50" persistentvolumeclaims: "10" --- # limit-range.yaml — per-container defaults (prevents Pods without resource specs) apiVersion: v1 kind: LimitRange metadata: name: payments-limits namespace: payments spec: limits: - type: Container default: # Applied when container omits 'resources.limits' cpu: 500m memory: 512Mi defaultRequest: # Applied when container omits 'resources.requests' cpu: 100m memory: 128Mi max: # Hard ceiling per container cpu: "4" memory: 8Gi min: # Minimum per container (prevents near-zero requests) cpu: 50m memory: 64Mi
Production pitfall: If a namespace has a ResourceQuota with CPU/memory entries, every Pod scheduled into that namespace MUST declare resource requests and limits. Pods without explicit resources will be rejected by the admission controller. Always pair a ResourceQuota with a LimitRange that provides defaults — otherwise developers who forget resource specs will get confusing "exceeded quota" errors even when the namespace is nowhere near the ceiling.

Labels: The Universal Tag System

Labels are arbitrary key-value pairs attached to any Kubernetes object. They are not just organizational metadata — the control plane uses them constantly: Services find their Pods via labels, Deployments manage their ReplicaSets via labels, NetworkPolicies apply to Pods via labels, PodDisruptionBudgets target workloads via labels, and Horizontal Pod Autoscalers match pods via labels. Getting your label schema right at the start saves enormous operational pain later.

Kubernetes recommends a standard set of well-known labels (under the app.kubernetes.io/ prefix) that observability tools, Helm charts, and dashboards know how to interpret:

  • app.kubernetes.io/name — the application name (e.g., checkout-api)
  • app.kubernetes.io/instance — a unique instance name distinguishing replicas (e.g., checkout-api-prod)
  • app.kubernetes.io/version — the current version (e.g., 2.4.1)
  • app.kubernetes.io/component — the architectural role (e.g., frontend, backend, cache, worker)
  • app.kubernetes.io/part-of — the higher-level application this belongs to (e.g., ecommerce-platform)
  • app.kubernetes.io/managed-by — the tool managing this resource (e.g., helm, argocd)
Pro practice: Adopt the app.kubernetes.io/ prefix from day one. Prometheus, Grafana dashboards, the Kubernetes Dashboard, and most Helm charts all read these labels. If you use bare keys like app or version, you lose automatic integration with the entire ecosystem. The migration cost later, when hundreds of objects need relabeling, is enormous.

Label Selectors: Querying the Cluster

Selectors are how you query objects by their labels. Kubernetes supports two selector syntaxes:

Equality-based (supported everywhere): =, ==, !=

Set-based (supported in newer resources like Deployments, ReplicaSets, Jobs): in, notin, exists, !

# Equality-based — get all production pods for the payments team kubectl get pods -n payments -l env=production,team=payments # Set-based — get pods whose tier is either frontend OR backend kubectl get pods -l 'tier in (frontend, backend)' # Negation — exclude canary pods from results kubectl get pods -l 'track notin (canary)' # Key existence — pods that have a version label (any value) kubectl get pods -l version # Key absence — pods with NO version label (useful to find unlabeled objects) kubectl get pods -l '!version' # Combine set-based and equality-based kubectl get deployments -l 'env=production,app.kubernetes.io/component in (backend, worker)' # Label a running pod imperatively (useful for debugging — add a temporary label) kubectl label pod checkout-api-7d6f8c9b4-xk2pn debug=true # Remove a label (append - to the key) kubectl label pod checkout-api-7d6f8c9b4-xk2pn debug- # Show labels on objects kubectl get pods --show-labels # Use label selectors with other resources kubectl get services -l app=checkout-api kubectl delete pods -l env=staging # Dangerous — double-check before running!

Selectors Inside Manifests

The selector in a Deployment's spec is immutable after creation — it cannot be changed without deleting and re-creating the Deployment. This is a critical constraint. The selector must match the Pod template's labels exactly, and you should design your label schema before writing your first manifest:

# A Service selecting Pods by label — the fundamental wiring of Kubernetes networking apiVersion: v1 kind: Service metadata: name: checkout-api namespace: payments spec: selector: app.kubernetes.io/name: checkout-api # Matches pods with this label env: production # AND this label (all conditions must match) ports: - protocol: TCP port: 80 targetPort: 8080 type: ClusterIP --- # Deployment selector (IMMUTABLE after creation) apiVersion: apps/v1 kind: Deployment metadata: name: checkout-api namespace: payments spec: selector: matchLabels: app.kubernetes.io/name: checkout-api template: metadata: labels: app.kubernetes.io/name: checkout-api # MUST include selector labels app.kubernetes.io/version: "2.4.1" # Additional labels on pods are fine env: production team: payments

Annotations: Non-Identifying Metadata

Annotations are also key-value pairs but they are fundamentally different from labels: they are not queryable by selectors. They exist for metadata that tools, operators, and humans need to read but that Kubernetes itself does not use for object selection. Think of them as structured comments attached to objects.

Common uses of annotations in production clusters:

  • Deployment metadata: kubernetes.io/change-cause: "Bump image to 2.4.1 for CVE-2024-1234 fix" (shown in rollout history)
  • Ingress controller directives: nginx.ingress.kubernetes.io/proxy-body-size: "50m"
  • Prometheus scraping: prometheus.io/scrape: "true", prometheus.io/port: "9090"
  • CI/CD provenance: ci.mycompany.com/pipeline: "123", ci.mycompany.com/commit: "abc123"
  • Cluster autoscaler hints: cluster-autoscaler.kubernetes.io/safe-to-evict: "false"
  • IAM role binding (AWS): eks.amazonaws.com/role-arn: "arn:aws:iam::123456789:role/checkout-api"
Namespace partitioning and label-based selector wiring Kubernetes Cluster namespace: payments Service selector: app=checkout Pod A app=checkout env=production Pod B app=checkout env=production ResourceQuota CPU limit: 16 cores Memory limit: 32Gi Max pods: 50 selects by label namespace: monitoring Prometheus Pod app=prometheus team=platform Grafana Pod app=grafana team=platform ResourceQuota CPU limit: 4 cores / Memory: 8Gi Max pods: 20 Cross-Namespace Rules Services only select Pods in the SAME namespace NetworkPolicy selectors are namespace-scoped ClusterRoles span all namespaces (RBAC)
Two namespaces with independent ResourceQuotas. The Service in the payments namespace selects Pods by label — only Pods in the same namespace qualify. ResourceQuotas enforce hard resource ceilings per namespace.

Namespace Strategy at Big-Tech Scale

How do top-tier companies actually structure namespaces? Several patterns have emerged:

  • Per-environment namespaces (payments-dev, payments-staging, payments-prod) — simple, works well when environments are in the same cluster. Common in smaller organizations.
  • Per-team namespaces on shared clusters — each team owns one or more namespaces. RBAC grants team members full access to their namespace. This is the most common enterprise pattern for cost efficiency.
  • Per-service namespaces — one namespace per microservice or bounded context. Provides the finest-grained quota and RBAC control, at the cost of operational overhead managing many namespaces. Tools like Argo CD and Helm handle this well with templated namespace names.
Pro practice: Use a namespace naming convention that encodes both team and environment: <team>-<env> (e.g., payments-prod, analytics-staging). This makes RBAC rules readable (payments engineers get edit on payments-*), ResourceQuotas easy to audit, and kubectl get pods -A | grep payments-prod immediately useful. Avoid generic names like production — they become dumping grounds.

Finding Orphaned or Unlabeled Resources

In mature clusters, rogue resources accumulate — Pods deployed imperatively without labels, ConfigMaps from abandoned experiments, Services that no longer have matching Pods. Make auditing for these a routine practice:

# Find Services with NO matching Pods (selector finds nothing — a dangling service) # This checks each service's selector against running pods kubectl get services -A -o json | python3 -c " import json, sys, subprocess data = json.load(sys.stdin) for svc in data['items']: ns = svc['metadata']['namespace'] name = svc['metadata']['name'] sel = svc['spec'].get('selector', {}) if not sel: continue label_str = ','.join(f\"{k}={v}\" for k, v in sel.items()) result = subprocess.run( ['kubectl', 'get', 'pods', '-n', ns, '-l', label_str, '--no-headers'], capture_output=True, text=True ) if not result.stdout.strip(): print(f'DANGLING: {ns}/{name} selector={sel}') " # List all pods missing the recommended label set kubectl get pods -A -o json | \ python3 -c " import json, sys data = json.load(sys.stdin) required = ['app.kubernetes.io/name', 'app.kubernetes.io/version', 'env'] for p in data['items']: labels = p['metadata'].get('labels', {}) missing = [k for k in required if k not in labels] if missing: ns = p['metadata']['namespace'] name = p['metadata']['name'] print(f'{ns}/{name} missing: {missing}') " # Quick: count objects per namespace to spot namespace sprawl kubectl get all -A --no-headers | awk '{print $1}' | cut -d/ -f1 | sort | uniq -c | sort -rn

Labels and namespaces are foundational. Every feature you learn from here — NetworkPolicies, RBAC, PodAffinity, HorizontalPodAutoscaler, Prometheus scraping — targets objects using exactly these mechanisms. Invest time in getting your labeling schema consistent before your cluster grows; retrofitting labels onto hundreds of running objects in a live production cluster, while minimizing downtime, is a project you do not want to run.