Labels look like the most trivial thing in Kubernetes — a few key/value pairs stapled to an object. But they are the connective tissue of the entire system. A Service finds its Pods through a label selector. A Deployment owns its Pods through a label selector. Jobs, ReplicaSets, NetworkPolicies, Pod affinity, kubectl filtering — all of it runs on labels. On the CKAD exam, a single wrong or missing label is one of the most common reasons a task silently fails: your Deployment rolls out, your Service exists, and yet kubectl get endpoints comes back empty because the selector doesn’t match the Pod template.
This guide covers labels, selectors, and annotations the way the CKAD actually tests them — not as trivia, but as the mechanism you use to wire objects together and to filter them quickly under time pressure. If you want the full exam picture first, start with the CKAD exam guide for 2026 and sequence your prep with the CKAD study plan. This piece assumes you can already create Pods and Deployments and focuses on the label mechanics that hold them together.
What Labels Actually Are
A label is a key/value pair attached to an object’s metadata that is intended to be identifying and queryable. “Identifying” is the important word: labels are meant to describe attributes you will later select on — the app name, the tier, the environment, the release. Kubernetes indexes labels so that selecting objects by them is fast, even in large clusters.
apiVersion: v1
kind: Pod
metadata:
name: web-frontend
labels:
app: store
tier: frontend
environment: prod
release: canary
spec:
containers:
- name: nginx
image: nginx:1.27
Label keys can have an optional prefix (a DNS subdomain like app.kubernetes.io/) followed by a name, and values must be 63 characters or fewer, alphanumeric with -, _, and . allowed. Both keys and values are constrained — a value cannot contain spaces or arbitrary text. That constraint is deliberate: labels are for selection, not for storing free-form data. When you need free-form data, you use annotations (covered below).
You can add and change labels imperatively, which is by far the fastest path on the exam:
# Add or overwrite a label
kubectl label pod web-frontend team=payments
# Overwrite an existing label (must pass --overwrite)
kubectl label pod web-frontend environment=staging --overwrite
# Remove a label (trailing minus)
kubectl label pod web-frontend release-
# Label every Pod at once
kubectl label pods --all tier=frontend
Selectors: How Objects Find Each Other
A selector is a query over labels. Kubernetes supports two selector syntaxes, and knowing which controllers accept which is directly examable.
Equality-Based Selectors
Equality-based selectors match on exact key/value equality (or inequality). They are written as a comma-separated list, where every clause is ANDed together:
# All Pods where app=store AND tier=frontend
kubectl get pods -l 'app=store,tier=frontend'
# Inequality
kubectl get pods -l 'environment!=prod'
Older objects — a Service, a ReplicationController — only understand equality-based selectors, expressed as a simple map:
apiVersion: v1
kind: Service
metadata:
name: store-frontend
spec:
selector:
app: store
tier: frontend
ports:
- port: 80
targetPort: 8080
That selector map means “send traffic to every Pod carrying both app=store and tier=frontend.” If the Pods behind it are missing either label, the Service has no endpoints and returns connection failures. This is the single most common wiring bug on the exam.
Set-Based Selectors
Set-based selectors are richer: they support in, notin, and existence checks. Newer controllers — Deployment, ReplicaSet, Job, DaemonSet, StatefulSet — use them through the matchLabels / matchExpressions structure.
# Set-based on the command line
kubectl get pods -l 'environment in (prod,staging)'
kubectl get pods -l 'tier notin (cache)'
kubectl get pods -l 'release' # key exists, any value
kubectl get pods -l '!release' # key does NOT exist
In YAML, a Deployment’s selector looks like this:
apiVersion: apps/v1
kind: Deployment
metadata:
name: store-frontend
spec:
replicas: 3
selector:
matchLabels:
app: store
matchExpressions:
- key: tier
operator: In
values: [frontend]
- key: environment
operator: NotIn
values: [dev]
template:
metadata:
labels:
app: store
tier: frontend
environment: prod
spec:
containers:
- name: nginx
image: nginx:1.27
matchLabels and matchExpressions are ANDed together. The available operators are In, NotIn, Exists, and DoesNotExist (the last two take no values).
The Rule That Trips Everyone Up
For a Deployment (and ReplicaSet), the Pod template’s labels must satisfy the selector, and the selector is immutable after creation. If the template labels don’t match the selector, the API server rejects the Deployment outright:
selector does not match template labels
Because the selector can’t be changed later, if you get it wrong you must delete and recreate the object. On the exam, the safe habit is to let kubectl create deployment generate a consistent app=<name> label on both the selector and the template, then only add extra labels to the template:
kubectl create deployment store-frontend --image=nginx:1.27 --replicas=3
This produces a Deployment whose selector is app=store-frontend and whose template carries the same label — matched by construction. You avoid the mismatch error entirely.
Equality vs Set-Based: Quick Reference
| Aspect | Equality-based | Set-based |
|---|---|---|
| Operators | =, ==, != | In, NotIn, Exists, DoesNotExist |
| Used by | Service, ReplicationController | Deployment, ReplicaSet, Job, DaemonSet, StatefulSet |
| YAML field | plain selector map | selector.matchLabels + selector.matchExpressions |
| CLI example | -l app=store | -l 'app in (store,cart)' |
| Combining | comma = AND | multiple expressions = AND |
The exam-relevant takeaway: a Service uses a plain map; a Deployment uses matchLabels/matchExpressions. Mixing them up produces schema errors.
Verifying the Wiring
The fastest way to confirm a selector actually matches Pods is to check endpoints, not to eyeball YAML:
# Does the Service have any backends?
kubectl get endpoints store-frontend
# Which Pods match this exact selector?
kubectl get pods -l app=store,tier=frontend
# See labels on everything, as columns
kubectl get pods --show-labels
# Pivot a label into its own column
kubectl get pods -L tier,environment
If kubectl get endpoints shows <none>, the Service selector and the Pod labels disagree. Compare the two with kubectl get svc store-frontend -o yaml and kubectl get pods --show-labels, fix the labels with kubectl label ... --overwrite, and re-check. This loop — check endpoints, fix labels, re-check — is one you should be able to run in under a minute.
Annotations: The Other Metadata
Labels and annotations look identical in YAML — both are key/value maps under metadata — but they exist for opposite reasons.
- Labels are for selecting and grouping. They are constrained, indexed, and queried by selectors.
- Annotations are for attaching arbitrary non-identifying data. They are not indexed, cannot be selected on, and can hold large, structured, or free-form values.
apiVersion: v1
kind: Pod
metadata:
name: web-frontend
labels:
app: store # you select on this
annotations:
kubernetes.io/change-cause: "Roll out nginx 1.27"
contact: "[email protected]"
config-hash: "9f2b1c8e-build-4471"
description: "Front door for the storefront; owns TLS termination."
Use annotations for things like build metadata, contact info, tooling hints, checksums that trigger rollouts when they change, and human-readable descriptions. Ingress controllers, cert managers, and other tools read their configuration from annotations. You cannot do kubectl get pods -l contact=... — annotations are invisible to selectors by design.
| Labels | Annotations | |
|---|---|---|
| Purpose | Identify & select | Attach arbitrary metadata |
| Selectable? | Yes | No |
| Value limits | 63 chars, restricted charset | Large, free-form |
| Typical use | app, tier, env, release | build info, contacts, tool config, change-cause |
| Set imperatively | kubectl label | kubectl annotate |
kubectl annotate pod web-frontend description="Storefront front door"
kubectl annotate pod web-frontend description- # remove
One annotation worth memorising is kubernetes.io/change-cause, which kubectl rollout history displays. Setting it (via kubectl ... --record in older versions, or by annotating) gives your rollouts a readable audit trail.
Field Selectors: Selecting on Non-Label Fields
Label selectors query the labels map. Field selectors query built-in object fields instead — things that aren’t labels at all, like a Pod’s phase or the node it landed on. They are a separate mechanism and a favourite for filtering quickly:
# Only Pods that are actually running
kubectl get pods --field-selector status.phase=Running
# Everything except succeeded/failed pods
kubectl get pods --field-selector status.phase!=Succeeded
# Pods on a specific node
kubectl get pods --field-selector spec.nodeName=worker-2 -A
# Combine with a label selector
kubectl get pods -l app=store --field-selector status.phase=Running
The supported fields vary by resource — metadata.name, metadata.namespace, and status.phase are widely available; spec.nodeName works for Pods. Field selectors and label selectors combine freely, which is a genuinely useful exam trick when you need “the running Pods of app X.”
The Recommended Label Set
Kubernetes defines a set of recommended common labels under the app.kubernetes.io/ prefix. You don’t have to use them, but the exam may reference them and real tooling (Helm, dashboards) understands them:
metadata:
labels:
app.kubernetes.io/name: store
app.kubernetes.io/instance: store-prod
app.kubernetes.io/version: "1.27"
app.kubernetes.io/component: frontend
app.kubernetes.io/part-of: storefront
app.kubernetes.io/managed-by: helm
The idea is a shared vocabulary so that unrelated tools can reason about your objects. For exam speed you’ll usually use short labels (app, tier), but knowing the recommended set exists — and that it’s a convention, not enforced — is enough.
Exam-Day Speed Techniques
Labels are where you win or lose time on the CKAD. A few reflexes:
# Generate YAML with the right labels already wired, then edit
kubectl create deployment api --image=nginx --dry-run=client -o yaml > api.yaml
# Expose a Deployment — kubectl copies the label selector for you
kubectl expose deployment api --port=80 --target-port=8080
# The Service selector is auto-set to match the Deployment's pod labels
# Bulk-select for deletion or inspection
kubectl delete pods -l 'environment=dev'
kubectl get all -l app=store
# Scale/patch everything matching a label
kubectl get deploy -l tier=frontend
kubectl expose is the highest-leverage of these: it reads the target’s Pod template labels and builds a matching Service selector automatically, so you never hand-write a mismatched Service. When a task says “create a Service for this Deployment,” reach for kubectl expose before you reach for YAML.
Two more habits that prevent silent failures:
- Always verify with
kubectl get endpointsafter wiring a Service. Empty endpoints = label mismatch, every time. - Never try to edit a Deployment’s
selector— it’s immutable. If it’s wrong,kubectl deleteand recreate. Trying tokubectl editthe selector wastes time on an error you can’t resolve in place.
Common Mistakes
| Mistake | Symptom | Fix |
|---|---|---|
| Service selector doesn’t match Pod labels | endpoints empty; connection refused | Align labels with kubectl label --overwrite |
| Deployment template labels don’t satisfy selector | API rejects: “selector does not match template labels” | Make template labels a superset of the selector |
| Trying to change a Deployment selector | Immutable field error | Delete and recreate the Deployment |
Using set-based syntax in a Service selector | Schema/validation error | Services take a plain equality map only |
| Storing free-form data in a label | 63-char / charset validation error | Use an annotation instead |
Forgetting --overwrite on kubectl label | ”already has a value” error | Add --overwrite |
Practising Until It’s Automatic
Labels and selectors are muscle memory. The way to internalise them is to build the wiring, break it on purpose, and fix it under a clock: create a Deployment, expose it, confirm endpoints, then deliberately mislabel a Pod and watch the Service drop it. Doing that a dozen times makes the “empty endpoints → check labels” reflex instant — which is exactly what the CKAD rewards.
That timed, hands-on loop is what the CKAD Certification Mock Exam Bundle is built for. It runs on a real cluster with real kubectl in a browser-based terminal that mirrors the proctored PSI environment, so you practise the exact wiring-and-verifying loop above against graded scenarios instead of reading about it. Pair it with the CKAD Services & Networking guide to see selectors in their most common context, and the SecurityContext & ServiceAccounts guide for the identity side of Pod configuration.
Frequently Asked Questions
What is the difference between a label and an annotation in Kubernetes?
Labels are identifying key/value pairs meant to be selected and grouped on — they are indexed, size-constrained, and queried by selectors used by Services, Deployments, and more. Annotations attach arbitrary, non-identifying metadata (build info, contacts, tool config) that cannot be selected on and can hold large free-form values. Rule of thumb: if you’ll ever query by it, it’s a label; otherwise it’s an annotation.
What is the difference between matchLabels and matchExpressions?
Both live under a controller’s selector. matchLabels is a simple equality map (app: store). matchExpressions is a list of set-based rules using operators In, NotIn, Exists, and DoesNotExist. They are ANDed together, so an object must satisfy every clause of both to be selected.
Why does my Service have no endpoints?
Almost always a label mismatch: the Service’s selector doesn’t exactly match the labels on the target Pods. Run kubectl get pods --show-labels and compare against kubectl get svc <name> -o yaml. Fix the Pod labels with kubectl label --overwrite or correct the Service selector, then re-check kubectl get endpoints <name>.
Can I change a Deployment’s selector after creating it?
No. The selector field of a Deployment and ReplicaSet is immutable. If it’s wrong, you must delete the object and recreate it with the correct selector. This is why the safest approach is to let kubectl create deployment set matching labels on both the selector and the Pod template.
What are field selectors and how do they differ from label selectors?
Label selectors query the labels map. Field selectors query built-in object fields such as status.phase, metadata.name, metadata.namespace, and (for Pods) spec.nodeName. Use --field-selector on the command line, and combine it with -l to filter on both labels and fields at once, e.g. kubectl get pods -l app=store --field-selector status.phase=Running.
Are the app.kubernetes.io/ labels required?
No. The app.kubernetes.io/* set is a recommended convention so that different tools share a common vocabulary, but nothing enforces it. On the exam you’ll usually use short labels like app and tier for speed; just know the recommended set exists and what it’s for.