Labels and Selectors
Labels are how every object in Kubernetes finds every other object - a Service finds Pods, a Deployment owns a ReplicaSet, a NetworkPolicy picks a target. They are also the single most common thing to get subtly wrong, and the errors are unhelpful in two different ways: one is a loud rejection at create time, the other is complete silence. Both are here.
Application Design and Build Guide 13 of 44 Beginner
- Kubernetes1.36.4
- Runtimecontainerd 2.2.6
- CNICalico v3.32.1
- TimeAbout 13 min
- Reviewed23 August 2026
Written against the versions above. Equality selectors (`tier=backend`) and set-based selectors (`env in (prod,stage)`, `tier!=frontend`, bare `env` for existence) are both accepted by `kubectl -l`. A Service's `spec.selector`, however, only supports equality - set-based expressions there are not valid.
| Server Name | IP Address | OS | Roles | CPU | RAM | HDD |
|---|---|---|---|---|---|---|
| CKA1001 | 192.168.0.175 | Ubuntu 26.04 LTS | Control Plane Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE01 | 192.168.0.176 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE02 | 192.168.0.177 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
| CKA1001-NODE03 | 192.168.0.178 | Ubuntu 26.04 LTS | Worker Node | 2 Core | 4 GB | 50 GB |
Before you start
- A cluster and kubectl.
- The session creates namespace
ckad-labelwith Deploymentsapiandweb, labels and annotates them, and finishes with a Deployment the API server refuses.
-
Labels on, and how to see them
kubectl labeltakes several at once, and--show-labelsis the flag worth remembering - without it, labels are invisible in every listing.Note what
kubectl create deploymentalready added:app=api. Every generator sets a label, and that label is what the Deployment's own selector uses. Yours are additions on top.bash Example session kubectl create namespace ckad-labelnamespace/ckad-label createdkubectl -n ckad-label create deployment api --image=nginx:alpinedeployment.apps/api createdkubectl -n ckad-label label deployment api tier=backend env=proddeployment.apps/api labeledkubectl -n ckad-label label deployment web tier=frontend env=stagedeployment.apps/web labeledkubectl -n ckad-label get deploy --show-labelsNAME READY UP-TO-DATE AVAILABLE AGE LABELSapi 1/1 1 1 1s app=api,env=prod,tier=backendweb 1/1 1 1 1s app=web,env=stage,tier=frontendExpected resultTwo Deployments carrying three labels each.
Success conditionYou can see labels, which most listings hide.
-
Four ways to select
kubectl get deploy -l tier=backend # equality kubectl get deploy -l 'env in (prod,stage)' # set membership kubectl get deploy -l 'tier!=frontend' # negation kubectl get deploy -l 'env' # existenceAll four work anywhere
-lis accepted, includingdelete, which is the fastest way to remove a group of objects - and the fastest way to remove more than you meant.kubectl getfirst, every time, with the exact same selector.The existence form (
-l 'env') is the one people forget, and it is the quickest way to answer "which of these has been labelled at all".bash Example session kubectl -n ckad-label get deploy -l tier=backendNAME READY UP-TO-DATE AVAILABLE AGEapi 1/1 1 1 1skubectl -n ckad-label get deploy -l 'env in (prod,stage)' -o 'custom-columns=NAME:.metadata.name,ENV:.metadata.labels.env'NAME ENVapi prodweb stagekubectl -n ckad-label get deploy -l 'tier!=frontend' -o 'custom-columns=NAME:.metadata.name'NAMEapikubectl -n ckad-label get deploy -l 'env' -o 'custom-columns=NAME:.metadata.name'NAMEapiwebExpected resultFour selectors, four different result sets.
Success conditionYou can express any of the four selector forms from memory.
-
The Deployment's labels are not the Pods' labels
This catches people constantly. The Deployment carries
tier=backend:{"app":"api","env":"prod","tier":"backend"}but its Pods do not:
NAME ... LABELS api-... ... app=api,pod-template-hash=...and selecting Pods by that label returns nothing.
kubectl label deploymentlabels the Deployment object. The Pods get their labels fromspec.template.metadata.labels, which is a different field on a different part of the manifest. To label the Pods you must patch the template - and that triggers a rollout, because it changes the Pod spec.This is why a Service you wrote against
tier=backendselects nothing while the Deployment clearly has that label.bash Example session kubectl -n ckad-label get deploy api -o jsonpath='{.metadata.labels}'{"app":"api","env":"prod","tier":"backend"}kubectl -n ckad-label get pods -l app=api --show-labelsNAME READY STATUS RESTARTS AGE LABELSapi-5d47c54ff7-4llcb 1/1 Running 0 2s app=api,pod-template-hash=5d47c54ff7kubectl -n ckad-label get pods -l tier=backend --no-headersNo resources found in ckad-label namespace.Expected resultThe label on the Deployment, absent from its Pods.
Success conditionYou know which of the two label sets a Service actually matches.
-
Annotations look the same and are not
platform-teamThe annotation is there and readable. But select on it and you get nothing back - annotations are not selectable, by design. They hold data for humans and tools: an owner, a change-cause, a checksum that forces a rollout, controller configuration like the ingress-nginx annotations.
The practical split:
- Label anything you will ever want to select, group or count by. Short values, no spaces.
- Annotate everything else. Long values, URLs, JSON, whatever you like.
bash Example session kubectl -n ckad-label annotate deployment api owner=platform-team contact=nobody@example.comdeployment.apps/api annotatedkubectl -n ckad-label get deploy api -o jsonpath='{.metadata.annotations.owner}'platform-teamkubectl -n ckad-label get deploy -l owner=platform-teamNo resources found in ckad-label namespace.Expected resultThe annotation readable, and unselectable.
Success conditionYou will not try to select on an annotation on exam day.
-
The selector that does not match its own template
A Deployment selecting
app: onewith a template labelledapp: two:The Deployment "mismatched" is invalid: spec.template.metadata.labels: Invalid value: {"app":"two"}: `selector` does not match template `labels`Rejected outright. The API server will not create a controller that cannot own the Pods it is about to make - it would spawn Pods forever, never count any of them, and never stop.
This is the loud label failure, and it is the friendly one: it happens at create time and says exactly what is wrong. It usually appears after hand-editing a generated manifest - changing the template labels and forgetting the selector, or the other way round.
The quiet one is a Service whose selector matches nothing. Nothing is rejected, nothing is logged, and the Service simply has no endpoints. That is guide 66.
bash Example session kubectl -n ckad-label apply -f - <<'YAML'apiVersion: apps/v1kind: Deploymentmetadata: name: mismatchedspec: replicas: 1 selector: matchLabels: {app: one} template: metadata: labels: {app: two} spec: containers: - name: app image: nginx:alpineYAMLThe Deployment "mismatched" is invalid: spec.template.metadata.labels: Invalid value: {"app":"two"}: `selector` does not match template `labels`[exit 1]kubectl delete namespace ckad-label --wait=falsenamespace "ckad-label" deletedExpected resultA rejection naming both the selector and the template labels.
Success conditionYou can read the mismatch error and know which of the two to change.
Troubleshooting
selector does not match template labels.Why:
spec.selector.matchLabelsandspec.template.metadata.labelsdisagree.Fix:Make them match. Remember the selector is immutable after creation - see guide 12.
A Service has no endpoints although the Deployment has the right label.
Why: The label is on the Deployment, not on the Pod template.
Fix:
kubectl get pods --show-labels. A Service matches Pod labels only.kubectl delete -lremoved more than intended.Why: The selector matched more broadly than expected.
Fix:Always run
kubectl getwith the identical selector first.Selecting on an annotation returns nothing.
Why: Annotations are not indexed and cannot be selected.
Fix:Use a label if you need to select on it.