Kubernetes Entry Created: 05 Mar 2026 Updated: 05 Mar 2026

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:

  1. Production abhors a singleton. A single instance always grows into a set. Labels let Kubernetes deal with sets instead of isolated objects.
  2. 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.

  1. The prefix must be a valid DNS subdomain (maximum 253 characters). Examples: acme.com, kubernetes.io.
  2. The name is required, maximum 63 characters, must start and end with an alphanumeric character, with dashes (-), underscores (_), and dots (.) allowed between characters.
  3. The value follows the same rules as the name: maximum 63 characters, alphanumeric start and end.
KeyValueValid?
appproduct-apiYes
envprodYes
ver2Yes
acme.com/tierbackendYes
kubernetes.io/cluster-servicetrueYes
my labelvalueNo — 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:

  1. On the Deployment itself (metadata.labels) — describing the Deployment object.
  2. On the Pod template (spec.template.metadata.labels) — copied to every Pod the Deployment creates. The Deployment's selector must match these labels.

Here is a Deployment for a .NET Product API with three labels — app, env, and ver:

apiVersion: apps/v1
kind: Deployment
metadata:
name: product-api-prod
labels:
app: product-api
env: prod
ver: "1"
spec:
replicas: 2
selector:
matchLabels:
app: product-api
env: prod
template:
metadata:
labels:
app: product-api
env: prod
ver: "1"
spec:
containers:
- name: product-api
image: myregistry/product-api:1.0
ports:
- containerPort: 8080
name: http
protocol: TCP
env:
- name: ASPNETCORE_URLS
value: "http://+:8080"
- name: ASPNETCORE_ENVIRONMENT
value: "Production"
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"

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:

kubectl label deployment product-api-prod "canary=true"

To view specific labels as columns in a listing, use the -L flag:

kubectl get deployments -L app,env,ver

To remove a label, append a minus sign (-) to the key name:

kubectl label deployment product-api-prod "canary-"

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.

kubectl get pods --selector="env=prod"

Combine multiple conditions with a comma — this is a logical AND, meaning both conditions must be true:

kubectl get pods --selector="app=product-api,env=prod"

Exclude a value with !=:

kubectl get pods --selector="env!=staging"

Set-Based Selectors

More expressive: match against a set of values using in and notin.

kubectl get pods --selector="app in (product-api,inventory-api)"
kubectl get pods --selector="env notin (staging,dev)"

Existence Selectors

Check whether a label key exists at all, regardless of its value:

kubectl get deployments --selector="canary"

Or select objects that do not have a particular label:

kubectl get deployments --selector='!canary'

Selector Operator Reference

OperatorDescriptionExample
key=valuekey is set to valueenv=prod
key!=valuekey is not set to valueenv!=staging
key in (v1, v2)key is one of v1 or v2env in (prod,staging)
key notin (v1, v2)key is not any of v1 or v2env notin (dev,test)
keykey exists (any value)canary
!keykey 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:

selector:
matchLabels:
app: product-api
matchExpressions:
- key: env
operator: In
values:
- prod
- staging

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:

selector:
app: product-api
env: prod

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:

  1. A Deployment uses label selectors to find and manage its Pods via a ReplicaSet.
  2. A Service uses a label selector to determine which Pods should receive incoming traffic.
  3. A NetworkPolicy uses label selectors to define which Pods can or cannot communicate with each other.
  4. 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:

  1. A Git commit hash or image build tag (e.g., acme.com/git-commit: a3f2c1d)
  2. A link to the CI/CD pipeline run that created this deployment
  3. A human-readable reason for the last change (e.g., kubernetes.io/change-cause: "Bumped memory for Black Friday")
  4. Contact information for the owning team
  5. Configuration consumed by sidecar tools like Prometheus, Istio, or Fluentd
  6. 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:

  1. kubernetes.io/change-cause — Kubernetes uses this to record rollout reasons
  2. deployment.kubernetes.io/revision — used internally by Deployments for rollback tracking
  3. acme.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:

apiVersion: apps/v1
kind: Deployment
metadata:
name: product-api-prod
labels:
app: product-api
env: prod
ver: "1"
annotations:
acme.com/git-commit: "a3f2c1d9e8b7"
acme.com/build-pipeline: "https://ci.acme.com/builds/4201"
acme.com/owner: "platform-team@acme.com"
kubernetes.io/change-cause: "Deploy v1.0 for initial release"

Step 10: Labels vs Annotations — When to Use Which

ConsiderationLabelsAnnotations
PurposeIdentify and group objectsAttach descriptive metadata for tools and humans
Can be used in selectors?YesNo
Value length limit63 charactersNo limit
Value format validationAlphanumeric with limited symbolsAny string (no validation)
Typical examplesapp name, environment, version, tierGit 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

kubectl get pods --show-labels
NAME READY STATUS RESTARTS AGE LABELS
product-api-prod-6d7f4b-x2k9p 1/1 Running 0 2m app=product-api,env=prod,ver=1
product-api-prod-6d7f4b-n8mzt 1/1 Running 0 2m app=product-api,env=prod,ver=1
product-api-staging-7c9d2b-p4wsq 1/1 Running 0 90s app=product-api,env=staging,ver=2

Show Specific Labels as Columns

kubectl get deployments -L app,env,ver
NAME READY UP-TO-DATE AVAILABLE AGE APP ENV VER
product-api-prod 2/2 2 2 3m product-api prod 1
product-api-staging 1/1 1 1 2m product-api staging 2
inventory-api-prod 2/2 2 2 2m inventory-api prod 2

Filter Pods by Label Selector

kubectl get pods --selector="env=prod"
kubectl get pods -l "app=product-api,ver=2"
kubectl get pods -l "app in (product-api,inventory-api)"
kubectl get pods -l "ver=2,!canary"

Add a Label to a Running Object

kubectl label deployment product-api-staging "canary=true"

Remove a Label from a Running Object

kubectl label deployment product-api-staging "canary-"

View Annotations on an Object

kubectl describe deployment product-api-prod

Look for the Annotations: section in the output.

Add or Update an Annotation

kubectl annotate deployment product-api-prod \
"acme.com/change-cause=Bumped CPU limits for load test"

Remove an Annotation

kubectl annotate deployment product-api-prod "acme.com/change-cause-"

Delete Objects by Label Selector

kubectl delete deployments --selector="env=staging"
kubectl delete all --selector="app=product-api"

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.

  1. Build the Product API container image. Save this Dockerfile in your ASP.NET Core project root:
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish

FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "ProductApi.dll"]
docker build -t myregistry/product-api:1.0 .
docker push myregistry/product-api:1.0
  1. Create the production Deployment for the Product API. Save as product-api-prod-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: product-api-prod
labels:
app: product-api
env: prod
ver: "1"
spec:
replicas: 2
selector:
matchLabels:
app: product-api
env: prod
template:
metadata:
labels:
app: product-api
env: prod
ver: "1"
spec:
containers:
- name: product-api
image: myregistry/product-api:1.0
ports:
- containerPort: 8080
name: http
protocol: TCP
env:
- name: ASPNETCORE_URLS
value: "http://+:8080"
- name: ASPNETCORE_ENVIRONMENT
value: "Production"
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
  1. Create the staging Deployment for the Product API. Save as product-api-staging-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: product-api-staging
labels:
app: product-api
env: staging
ver: "2"
spec:
replicas: 1
selector:
matchLabels:
app: product-api
env: staging
template:
metadata:
labels:
app: product-api
env: staging
ver: "2"
spec:
containers:
- name: product-api
image: myregistry/product-api:2.0
ports:
- containerPort: 8080
name: http
protocol: TCP
env:
- name: ASPNETCORE_URLS
value: "http://+:8080"
- name: ASPNETCORE_ENVIRONMENT
value: "Staging"
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "250m"
memory: "256Mi"
  1. Create the production Deployment for the Inventory API. Save as inventory-api-prod-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: inventory-api-prod
labels:
app: inventory-api
env: prod
ver: "2"
spec:
replicas: 2
selector:
matchLabels:
app: inventory-api
env: prod
template:
metadata:
labels:
app: inventory-api
env: prod
ver: "2"
spec:
containers:
- name: inventory-api
image: myregistry/inventory-api:2.0
ports:
- containerPort: 8080
name: http
protocol: TCP
env:
- name: ASPNETCORE_URLS
value: "http://+:8080"
- name: ASPNETCORE_ENVIRONMENT
value: "Production"
resources:
requests:
cpu: "150m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
  1. Create a Service that routes traffic to the production Product API. Save as product-api-service.yaml:
apiVersion: v1
kind: Service
metadata:
name: product-api
labels:
app: product-api
env: prod
spec:
selector:
app: product-api
env: prod
ports:
- name: http
port: 80
targetPort: 8080
protocol: TCP
type: ClusterIP
  1. The spec.selector here uses the legacy equality form. Kubernetes will route traffic to any Pod carrying both app=product-api and env=prod — which means the staging Pods are automatically excluded.
  2. Apply all four manifests:
kubectl apply -f product-api-prod-deployment.yaml
kubectl apply -f product-api-staging-deployment.yaml
kubectl apply -f inventory-api-prod-deployment.yaml
kubectl apply -f product-api-service.yaml
  1. View all Deployments with their labels:
kubectl get deployments --show-labels
NAME READY UP-TO-DATE AVAILABLE AGE LABELS
product-api-prod 2/2 2 2 45s app=product-api,env=prod,ver=1
product-api-staging 1/1 1 1 30s app=product-api,env=staging,ver=2
inventory-api-prod 2/2 2 2 20s app=inventory-api,env=prod,ver=2
  1. Use selectors to filter objects. Get only production Pods:
kubectl get pods --selector="env=prod"
  1. Get all version 2 Pods across both apps:
kubectl get pods --selector="ver=2"
  1. Get all product-api Pods regardless of environment:
kubectl get pods --selector="app=product-api"
  1. Get Pods belonging to either API in production:
kubectl get pods --selector="app in (product-api,inventory-api),env=prod"
  1. Add a canary marker to the staging Deployment:
kubectl label deployment product-api-staging "canary=true"
  1. List only Deployments that do not carry the canary label:
kubectl get deployments --selector='!canary'
NAME READY UP-TO-DATE AVAILABLE AGE
product-api-prod 2/2 2 2 3m
inventory-api-prod 2/2 2 2 3m
  1. Add operational annotations to the production Deployment:
kubectl annotate deployment product-api-prod \
"acme.com/git-commit=a3f2c1d" \
"acme.com/build-pipeline=https://ci.acme.com/builds/4201" \
"kubernetes.io/change-cause=Deploy product-api v1.0 for initial release"
  1. Verify the annotations were stored:
kubectl describe deployment product-api-prod
Name: product-api-prod
Namespace: default
Annotations: acme.com/build-pipeline: https://ci.acme.com/builds/4201
acme.com/git-commit: a3f2c1d
kubernetes.io/change-cause: Deploy product-api v1.0 for initial release
  1. Clean up all resources created in this example:
kubectl delete deployments --selector="app in (product-api,inventory-api)"
kubectl delete service product-api

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.

Share this lesson: