Labels, selectors, namespaces and annotations

The metadata layer that connects objects: label conventions, selector syntax, namespace boundaries, and when to annotate instead.

Labels and selectors are how objects find each other

A label is a key-value pair attached to an object. A selector is a query over labels. Almost every relationship in Kubernetes is a selector: a Service finds pods, a Deployment adopts ReplicaSets, a NetworkPolicy picks targets, a PodDisruptionBudget picks victims. Nothing is wired up by name.

metadata:
  labels:
    app.kubernetes.io/name: web
    app.kubernetes.io/instance: web-prod
    app.kubernetes.io/version: "1.27"
    app.kubernetes.io/component: frontend
    app.kubernetes.io/part-of: storefront
    app.kubernetes.io/managed-by: helm
    environment: prod
    track: stable
Selector formExampleWhere it is used
Equalityapp=web,tier!=cacheServices, -l on the CLI, ReplicaSets
Set-basedtier in (frontend,backend)matchExpressions in Deployments and policies
ExistscanaryAnything tagged, regardless of value
Does not exist!canaryExcluding a subset from a rollout or policy
Empty selector{}Selects everything in scope — great for default-deny, dangerous elsewhere
kubectl get pods --show-labels
kubectl get pods -l 'app.kubernetes.io/name=web,environment in (prod,staging)'
kubectl get pods -l '!canary'
kubectl label pod web-abc123 track=stable
kubectl label pod web-abc123 track-        # remove a label
kubectl get pods -l app=web --field-selector status.phase=Running
  • Keys may have an optional DNS prefix (example.com/env) and a name of up to 63 characters; values are alphanumerics, dashes, underscores and dots.
  • Labels are for selection, so keep values bounded and enumerable. Free text belongs in an annotation.
  • Selectors on a Deployment are immutable after creation. Getting them wrong means deleting and recreating the object.
  • Use the same labels on every object in an app. Consistent labels are what make kubectl get all -l app=web and every dashboard work.

Namespaces divide names, not networks

A namespace is a scope for names and for RBAC. Two objects may share a name in different namespaces. It is not a security boundary on its own: by default pods in any namespace can reach pods in any other.

GroupNamespacedCluster-scoped
WorkloadsPod, Deployment, StatefulSet, Job, CronJob, DaemonSet
ConfigConfigMap, Secret, ServiceAccount
NetworkService, Ingress, NetworkPolicy
StoragePersistentVolumeClaimPersistentVolume, StorageClass, CSIDriver
AccessRole, RoleBindingClusterRole, ClusterRoleBinding
ClusterNode, Namespace, PriorityClass, CustomResourceDefinition
kubectl create namespace team-a
kubectl get namespaces
kubectl get pods -n team-a
kubectl get pods --all-namespaces
kubectl config set-context --current --namespace=team-a
kubectl get pods -A -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name

# a common pattern: one namespace per team or per environment
kubectl create namespace storefront-staging
kubectl create namespace storefront-prod
  • Delete a namespace and everything inside it goes with it. That is convenient for a preview environment and catastrophic in production.
  • Some names are reserved: kube-system, kube-public, kube-node-lease, and anything starting with kube-.
  • Cross-namespace DNS always works; cross-namespace pod selectors and NetworkPolicies do not, which is why teams pair namespaces with default-deny policies.
  • Quotas and LimitRanges apply per namespace, so the namespace is also the unit of resource accounting.

Annotations carry everything else

When metadata is for a human or a tool rather than for selection, it is an annotation. Annotations may hold structured data, are not indexed, and may be up to roughly 256 KB in total per object.

CharacteristicLabelAnnotation
Selectable with a queryYesNo
Value constraintsStrict format, 63 charactersFree-form, can be JSON or YAML
Typical contentapp name, version, environment, tierbuild URL, owner contact, checksum, controller config
Hidden costToo many labels makes every object large and slow to queryA huge annotation is copied into every list response
metadata:
  annotations:
    kubernetes.io/change-cause: "roll to 1.27.1 - ticket OPS-4821"
    prometheus.io/scrape: "true"
    prometheus.io/port: "8080"
    checksum/config: "9f2c1a..."     # changing it forces a rollout
    owner: "[email protected]"
kubectl annotate deploy web kubernetes.io/change-cause="rollback - OPS-4902"
kubectl rollout history deploy web   # the change-cause appears per revision
⚠️
Because a Deployment selector is immutable, changing a pod's identifying labels is a breaking change: the running pods no longer match, the rollout stalls, and old pods are never cleaned up. Introduce a new workload and shift traffic instead of relabelling in place.

FAQ

Namespace per team, per environment or per app?
Per environment and per team boundary is the common answer, because it lines up with RBAC, quotas and default-deny policies. Splitting one app into many namespaces mostly adds DNS names and duplicated config.
Can I change a Service selector at runtime?
Yes, and it is the cheapest blue-green switch you have. Point it at the old pods while the new ones warm up, then flip it back — but keep both sets of labels present on the pods during the transition.

kubectl fundamentals and writing manifests Scheduling: affinity, taints and tolerations

Last refreshed 2026-09-18.