Lesson  in  Kubernetes 101

Labels, selectors and annotations

The glue of Kubernetes: label objects, select them with equality-based and set-based selectors, and find out why annotations are not just long labels.

Labels and selectors

Kubernetes has no folders. It has no hierarchies, no groups, no tree to put objects in. Everything that looks like a relationship between objects (a Service that finds its Pods, a Deployment that knows which ones are its own, a NetworkPolicy that protects some and not others) is built on a single mechanism: labels and selectors.

It is a deliberately poor design, and a surprisingly powerful one. A Pod belongs to nobody: it simply carries labels, and anyone who looks at them can claim it.

The playground comes with five Pods already labeled. Look at them from the dev-machine tab:

kubectl get pods --show-labels

Four carry three labels (app, entorno and tier; entorno is Spanish for environment). The fifth, huerfano (huerfano means "orphan"), carries none of the three: only the run=huerfano label that kubectl run adds on its own when you don't give it any. We'll come back for it.

Step 1: Selecting by equality

The basic form of the selector, the one you've been using throughout the course:

kubectl get pods -l entorno=tienda
kubectl get pods -l app=api,entorno=tienda
kubectl get pods -l entorno!=tienda

What does the comma do? A logical AND, never an OR. app=api,entorno=tienda selects the Pods that meet both conditions. It's the same syntax matchLabels uses in the selector of a Deployment or a Service, and that is why there's no OR there either.

Step 2: Selecting by set

The second family of selectors, the one almost nobody knows and that answers questions the first one can't:

kubectl get pods -l 'entorno in (tienda,staging)'
kubectl get pods -l 'app notin (api)'
kubectl get pods -l 'entorno'
kubectl get pods -l '!entorno'

The four cases, in order: membership in a set (here there is something like an OR), exclusion, existence of the label whatever its value, and absence of the label.

That last one, !entorno, is pure gold in a real cluster: it's how you find what someone deployed without following the conventions. Try it.

Note

💡 Set-based selectors work in kubectl and in the matchExpressions of the objects that support it (Deployment, ReplicaSet, NetworkPolicy). But a Service's selector only accepts equality: it's a flat map of key and value. It's one of those asymmetries you only learn by running into them.

Step 3: Labeling live objects

Adopt the orphan. The labels of an existing object are changed with kubectl label:

kubectl label pod huerfano app=tienda entorno=desarrollo
kubectl get pods --show-labels

And now try changing a label that already exists:

kubectl label pod huerfano entorno=tienda

Error: already has a value. Kubernetes forces you to say out loud that you're overwriting, with --overwrite. It's a deliberate protection: changing a label can move a Pod from one Service to another without anyone noticing.

Leave it as it was (entorno=desarrollo, with --overwrite if needed).

And while we're at it, learn to remove labels. huerfano has no tier, so try it on one of the Pods that does carry it:

kubectl label pod tienda-dev tier-
kubectl get pods --show-labels

That trailing dash is the syntax to delete a label. If the key doesn't exist, kubectl tells you with a label "tier" not found: try it on huerfano too to see the error.

The conventions that save you arguments

Kubernetes reserves a prefix, app.kubernetes.io/, for a set of recommended labels that the whole ecosystem understands: app.kubernetes.io/name, /instance, /version, /component, /part-of and /managed-by. Helm sets them on its own. They aren't mandatory, but adopting them means your objects speak the language of everyone's tools.

And a format detail that will bite you on day one: a label value has a maximum of 63 characters and doesn't accept just any character: it must start and end with an alphanumeric. It can be empty, which is a detail that surprises almost everyone. If you need to store something longer or freer, a label isn't what you need. That is what you'll see in the next unit.

Summary

  • Labels are the only relationship mechanism between objects in Kubernetes. There are no folders.
  • Equality selectors (a=b,c=d, always a logical AND) and set-based ones (in, notin, existence, !absence).
  • A Service's selector only understands equality; Deployments and NetworkPolicies accept matchExpressions.
  • kubectl label labels, --overwrite overwrites and the - suffix deletes.

Annotations

An annotation looks a lot like a label: it's a key-value pair in the metadata of any object. And there's only one difference, but it changes everything:

Nobody can select by annotations.

That restriction is the reason it exists. Labels are an index: short, restricted, designed so the API can filter millions of objects fast. Annotations are a drawer: they can hold up to 256 KB, accept any character, a whole JSON, a certificate, a commit message. Nobody is going to index them, so they can afford it.

Step 1: Annotating an object

Write down who is responsible for api in production. The keys are responsable (who answers for it) and descripcion (a free-text description), and the values stay in Spanish, like everything in the tienda:

kubectl annotate pod api-prod responsable="equipo-plataforma@empresa.com"
kubectl annotate pod api-prod descripcion="API de pedidos. Contacto de guardia en el canal #api-oncall"
kubectl get pod api-prod -o jsonpath='{.metadata.annotations}' | jq .

The syntax is a twin of kubectl label: --overwrite to replace and the - suffix to delete (kubectl annotate pod api-prod descripcion-).

And now the check that nails down the concept. Try selecting by it:

kubectl get pods -l responsable

No resources found. Not even asking for the key alone, with no value. It isn't that the annotation doesn't exist: it's that -l looks at labels, and this is an annotation. That silence is the whole lesson.

And if you try to be more precise, selecting by the full value, you don't even get as far as the silence:

kubectl get pods -l responsable=equipo-plataforma@empresa.com
# Invalid value: "equipo-plataforma@empresa.com": a valid label must be an empty
# string or consist of alphanumeric characters, '-', '_' or '.'

The API rejects the question before searching for anything. That at sign the annotation accepted without blinking is illegal in a label: label values are restricted precisely because they are an index. Two commands, the same lesson from both sides.

Step 2: The annotations that do things

Here comes the interesting part, and it's what confuses everyone at first: many annotations aren't notes, they're configuration. Kubernetes and its ecosystem use them as an extension channel, a way to pass parameters to a component without having to change the API schema.

You've already run into several without knowing it:

  • deployment.kubernetes.io/revision: the annotation where the Deployment stores the revision number you looked up in the rollback.
  • kubectl.kubernetes.io/last-applied-configuration: the copy of the last applied YAML that kubectl apply keeps so it can compute the changes.
  • kubernetes.io/change-cause: if you apply a change with --record or write it yourself, it shows up in the CHANGE-CAUSE column of kubectl rollout history. It's what turns an anonymous revision history into a readable one.
  • nginx.ingress.kubernetes.io/rewrite-target, cert-manager.io/cluster-issuer and company: all the advanced configuration of Ingress controllers lives in annotations. That abuse, proprietary configuration hidden in unvalidated strings, is precisely the problem the Gateway API came to solve by moving those fields into the schema.

Look at it on an object you already know:

kubectl get pods -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.metadata.annotations}{"\n"}{end}'

And isn't this dangerous? A little. An annotation is unvalidated text: if you get the key name wrong, absolutely nothing happens, no error, and the configuration is simply ignored in silence. When an Ingress controller annotation "does nothing", the first suspect is always a typo in the name.

When to use which

The rule is short and doesn't fail:

  • Do I need to find this object by this piece of data, or have another object select it? → label.
  • Is it information for people or for a tool, and nobody is going to filter by it? → annotation.

The classic mistake is putting into labels what should go into annotations (a deployment timestamp, a commit identifier, a long description) and ending up with an API index full of unique values that don't group anything.

Summary

  • Labels: for selecting. Short (63 characters), restricted, indexed.
  • Annotations: for everything else. Large (up to 256 KB), free-form, invisible to selectors.
  • Half of Kubernetes is configured through annotations: Deployment revisions, last-applied-configuration, change-cause, and all the configuration of Ingress controllers.
  • A misspelled annotation gives no error: it's ignored. Always check the key before blaming the controller.