Kubernetes Labels and Annotations
Overview
As your application grows, you will quickly find yourself managing dozens or hundreds of Kubernetes objects — Pods, Deployments, Services, and more. Labels and annotations are the two built-in mechanisms Kubernetes provides to attach metadata to these objects so you can organize, query, and extend them at scale.
Labels are key/value pairs that attach identifying information to objects. They are the foundation for grouping and for connecting objects to each other. Services find their target Pods through labels, and ReplicaSets manage their Pods through label selectors.
Annotations are also key/value pairs, but they carry non-identifying information intended for tools, automation scripts, and external systems. You cannot filter or query objects by annotation values — they are purely descriptive.
By the end of this article, you will understand how to design a label strategy for a multi-environment .NET microservice, apply selectors to filter objects at runtime, and attach rich metadata via annotations.
Core Concepts
Step 1: What Are Labels and Why Do They Exist?
Kubernetes was designed to manage software at scale. In production you rarely have a single instance of anything — you have multiple versions, multiple environments, and multiple replicas. Labels give Kubernetes (and you) a way to reason about sets of objects rather than individual items.
Consider a scenario: you run a Product API in three environments (prod, staging, dev) and you are testing two versions simultaneously. Without labels, how would you delete only the staging Pods? How would a Service know which Pods to route traffic to? Labels are the answer to both questions.
Two key insights drove the design of labels:
- Production abhors a singleton. A single instance always grows into a set. Labels let Kubernetes deal with sets instead of isolated objects.
- No fixed hierarchy scales for everyone. An application's structure changes over time. Labels are flexible enough to represent any grouping without locking you into a rigid tree.
Step 2: Label Syntax
Labels are key/value pairs where both are strings. A label key has two parts separated by a slash: an optional prefix and a required name.
- The prefix must be a valid DNS subdomain (maximum 253 characters). Examples:
acme.com,kubernetes.io. - The name is required, maximum 63 characters, must start and end with an alphanumeric character, with dashes (
-), underscores (_), and dots (.) allowed between characters. - The value follows the same rules as the name: maximum 63 characters, alphanumeric start and end.
| Key | Value | Valid? |
|---|---|---|
app | product-api | Yes |
env | prod | Yes |
ver | 2 | Yes |
acme.com/tier | backend | Yes |
kubernetes.io/cluster-service | true | Yes |
my label | value | No — spaces are not allowed |
When a domain name is used as a prefix, it signals ownership. Kubernetes reserves the kubernetes.io/ prefix for its own use. Your organization can use a reverse-DNS prefix to prevent key collisions with other tools.
Step 3: Defining Labels in a Manifest
Labels are defined in the metadata.labels field of any Kubernetes object. For a Deployment, labels typically appear in two places:
- On the Deployment itself (
metadata.labels) — describing the Deployment object. - On the Pod template (
spec.template.metadata.labels) — copied to every Pod the Deployment creates. The Deployment'sselectormust match these labels.
Here is a Deployment for a .NET Product API with three labels — app, env, and ver:
The selector.matchLabels block ties the Deployment to its Pods. When Kubernetes needs to count replicas or roll out an update, it finds the right Pods by querying for app=product-api, env=prod.
Step 4: Adding and Modifying Labels After Creation
You can add or change labels on a live object without redeploying:
To view specific labels as columns in a listing, use the -L flag:
To remove a label, append a minus sign (-) to the key name:
Important: kubectl label on a Deployment updates the label on the Deployment object only, not on the Pods it already created. To propagate label changes to Pods, update spec.template.metadata.labels in the manifest and re-apply it.
Step 5: Label Selectors
A label selector is a query expression that matches objects by their labels. You use selectors both in kubectl commands and inside Kubernetes object definitions (such as how a Service locates its target Pods).
Equality-Based Selectors
The simplest form: match a specific key/value pair.
Combine multiple conditions with a comma — this is a logical AND, meaning both conditions must be true:
Exclude a value with !=:
Set-Based Selectors
More expressive: match against a set of values using in and notin.
Existence Selectors
Check whether a label key exists at all, regardless of its value:
Or select objects that do not have a particular label:
Selector Operator Reference
| Operator | Description | Example |
|---|---|---|
key=value | key is set to value | env=prod |
key!=value | key is not set to value | env!=staging |
key in (v1, v2) | key is one of v1 or v2 | env in (prod,staging) |
key notin (v1, v2) | key is not any of v1 or v2 | env notin (dev,test) |
key | key exists (any value) | canary |
!key | key does not exist | !canary |
Step 6: Label Selectors in API Objects
Inside Kubernetes object definitions, selectors are written in YAML. There are two forms depending on the object type.
Modern Form: matchLabels and matchExpressions
Most objects — Deployments, ReplicaSets, Jobs — use the newer, more powerful selector form:
All conditions in matchLabels and matchExpressions are evaluated as a logical AND. Valid operators are In, NotIn, Exists, and DoesNotExist.
Legacy Form: Direct Key/Value Map
Older object types such as Services and ReplicationControllers only support the = operator, expressed as a direct map:
This is equivalent to app=product-api AND env=prod. It is simpler but cannot express set-based conditions.
Step 7: How Kubernetes Uses Labels Internally
Labels are the glue that holds Kubernetes together. The system is purposefully decoupled — no single component owns everything. Objects are connected through labels and selectors:
- A Deployment uses label selectors to find and manage its Pods via a ReplicaSet.
- A Service uses a label selector to determine which Pods should receive incoming traffic.
- A NetworkPolicy uses label selectors to define which Pods can or cannot communicate with each other.
- PodAffinity rules use label selectors to influence which nodes a Pod is scheduled onto.
When you look at a Service YAML and wonder "how does this Service know which Pods to target?" — the answer is always a label selector. Understanding this internal use of labels is essential for debugging and designing Kubernetes applications.
Step 8: What Are Annotations?
Annotations are also key/value pairs on Kubernetes objects, but they serve a completely different purpose from labels. Where labels identify and group objects, annotations describe them. You cannot use annotations in selector queries — they are purely for humans and tools reading the object metadata.
Common uses for annotations:
- A Git commit hash or image build tag (e.g.,
acme.com/git-commit: a3f2c1d) - A link to the CI/CD pipeline run that created this deployment
- A human-readable reason for the last change (e.g.,
kubernetes.io/change-cause: "Bumped memory for Black Friday") - Contact information for the owning team
- Configuration consumed by sidecar tools like Prometheus, Istio, or Fluentd
- Alpha feature flags or configuration that has not yet graduated to a first-class API field
Step 9: Annotation Syntax
Annotation keys follow the same format as label keys: an optional DNS subdomain prefix and a required name. Because annotations are typically written by tools, the prefix is especially important to avoid collisions. Example keys used in practice:
kubernetes.io/change-cause— Kubernetes uses this to record rollout reasonsdeployment.kubernetes.io/revision— used internally by Deployments for rollback trackingacme.com/build-pipeline— your own CI/CD system's metadata
Annotation values are free-form strings with no length limit and no format validation. You can store a JSON document, a URL, or a short human-readable note. The Kubernetes API server treats all annotation values as opaque strings — it will not validate their format.
Annotations are defined in the metadata.annotations section, alongside labels:
Step 10: Labels vs Annotations — When to Use Which
| Consideration | Labels | Annotations |
|---|---|---|
| Purpose | Identify and group objects | Attach descriptive metadata for tools and humans |
| Can be used in selectors? | Yes | No |
| Value length limit | 63 characters | No limit |
| Value format validation | Alphanumeric with limited symbols | Any string (no validation) |
| Typical examples | app name, environment, version, tier | Git hash, build URL, owner email, sidecar config |
A practical rule of thumb: if you might ever want to query or filter by a piece of metadata, use a label. If you only need it for documentation or tool configuration, use an annotation. When in doubt, start with an annotation and promote it to a label once you find yourself wanting to use it in a selector.
Hands-On: Kubernetes Commands
Show All Labels on Pods
Show Specific Labels as Columns
Filter Pods by Label Selector
Add a Label to a Running Object
Remove a Label from a Running Object
View Annotations on an Object
Look for the Annotations: section in the output.
Add or Update an Annotation
Remove an Annotation
Delete Objects by Label Selector
Step-by-Step Example
Let's build a realistic multi-deployment scenario from scratch. We will deploy two .NET microservices across different environments, connect a Service to the production Pods using a label selector, and attach operational annotations.
- Build the Product API container image. Save this
Dockerfilein your ASP.NET Core project root:
- Create the production Deployment for the Product API. Save as
product-api-prod-deployment.yaml:
- Create the staging Deployment for the Product API. Save as
product-api-staging-deployment.yaml:
- Create the production Deployment for the Inventory API. Save as
inventory-api-prod-deployment.yaml:
- Create a Service that routes traffic to the production Product API. Save as
product-api-service.yaml:
- The
spec.selectorhere uses the legacy equality form. Kubernetes will route traffic to any Pod carrying bothapp=product-apiandenv=prod— which means the staging Pods are automatically excluded. - Apply all four manifests:
- View all Deployments with their labels:
- Use selectors to filter objects. Get only production Pods:
- Get all version 2 Pods across both apps:
- Get all product-api Pods regardless of environment:
- Get Pods belonging to either API in production:
- Add a canary marker to the staging Deployment:
- List only Deployments that do not carry the canary label:
- Add operational annotations to the production Deployment:
- Verify the annotations were stored:
- Clean up all resources created in this example:
Summary
Labels are key/value pairs that identify Kubernetes objects and enable grouping. They are the mechanism by which Deployments manage Pods, Services route traffic, and kubectl filters objects. Every resource in a real cluster should carry a consistent set of labels — at minimum the application name, environment, and version.
Annotations are also key/value pairs, but their role is to carry non-identifying metadata for tools and humans. They have no format constraints (beyond being a string), cannot be used in queries, and are ideal for storing build hashes, CI/CD pipeline URLs, owner contacts, and configuration consumed by sidecar containers or operators.
Together, labels and annotations give you a powerful, flexible system for organizing and enriching any Kubernetes resource — without imposing a rigid hierarchy on your application's structure.