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 form | Example | Where it is used |
|---|---|---|
| Equality | app=web,tier!=cache | Services, -l on the CLI, ReplicaSets |
| Set-based | tier in (frontend,backend) | matchExpressions in Deployments and policies |
| Exists | canary | Anything tagged, regardless of value |
| Does not exist | !canary | Excluding 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=weband 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.
| Group | Namespaced | Cluster-scoped |
|---|---|---|
| Workloads | Pod, Deployment, StatefulSet, Job, CronJob, DaemonSet | — |
| Config | ConfigMap, Secret, ServiceAccount | — |
| Network | Service, Ingress, NetworkPolicy | — |
| Storage | PersistentVolumeClaim | PersistentVolume, StorageClass, CSIDriver |
| Access | Role, RoleBinding | ClusterRole, ClusterRoleBinding |
| Cluster | — | Node, 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 withkube-. - 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.
| Characteristic | Label | Annotation |
|---|---|---|
| Selectable with a query | Yes | No |
| Value constraints | Strict format, 63 characters | Free-form, can be JSON or YAML |
| Typical content | app name, version, environment, tier | build URL, owner contact, checksum, controller config |
| Hidden cost | Too many labels makes every object large and slow to query | A 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 revisionFAQ
Namespace per team, per environment or per app?
Can I change a Service selector at runtime?
Related
kubectl fundamentals and writing manifests Scheduling: affinity, taints and tolerations
Last refreshed 2026-09-18.