CertGrid CertGrid
Hands-on Lab·Certified Kubernetes Application Developer

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

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.

Two Deployments in one namespace. Nothing here depends on node placement.
Server NameIP AddressOSRolesCPURAMHDD
CKA1001192.168.0.175Ubuntu 26.04 LTSControl Plane Node2 Core4 GB50 GB
CKA1001-NODE01192.168.0.176Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB
CKA1001-NODE02192.168.0.177Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB
CKA1001-NODE03192.168.0.178Ubuntu 26.04 LTSWorker Node2 Core4 GB50 GB

Before you start

  1. Labels on, and how to see them

    kubectl label takes several at once, and --show-labels is the flag worth remembering - without it, labels are invisible in every listing.

    Note what kubectl create deployment already 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=frontend

    Expected resultTwo Deployments carrying three labels each.

    Success conditionYou can see labels, which most listings hide.

  2. 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'                            # existence

    All four work anywhere -l is accepted, including delete, which is the fastest way to remove a group of objects - and the fastest way to remove more than you meant. kubectl get first, 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'NAMEapiweb

    Expected resultFour selectors, four different result sets.

    Success conditionYou can express any of the four selector forms from memory.

  3. 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 deployment labels the Deployment object. The Pods get their labels from spec.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=backend selects 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.

  4. Annotations look the same and are not

    platform-team

    The 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.

  5. The selector that does not match its own template

    A Deployment selecting app: one with a template labelled app: 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" deleted

    Expected 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

Official sources