Kubernetes Label & Annotation Validator

Paste manifests, or pairs one per line, and find out which keys and values the API server will refuse and why. A label value stops at 63 characters and has no room for a slash or a colon, which is why an image reference or a URL belongs in an annotation, and why the object with it in a label does not partly apply.

Paste below, or drop a file anywhere on this panel

Or drop a file anywhere on this panel. Nothing is uploaded: the analysis runs in this tab.

The answer appears here

Paste on the left and press Validate. Nothing leaves this tab.

Examples

Real input you can load into the tool above. Each one shows a different thing going wrong, because that is what the tool is for.

Value over 63 characters

The limit that rejects an apply with an error naming the label, not the length

app.kubernetes.io/name: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

An invalid key prefix

The prefix before the slash must be a DNS subdomain, so capitals are rejected

My-App/tier: frontend

Common mistakes

These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.

  1. Using a label value longer than 63 characters

    The limit applies to the value, and a common source is a Git branch name or a full image reference pasted into a label.

    Instead:Truncate deliberately, or move the long value to an annotation, which allows far more.

  2. Putting a URL or an image reference in a label

    Label values cannot contain slashes or colons. Annotations can, and are the intended home for anything not used as a selector.

    Instead:Labels are for selecting. Annotations are for everything else.

  3. Changing a label that a Deployment selects on

    spec.selector on a Deployment is immutable after creation. Changing it requires deleting and recreating the Deployment.

    Instead:Choose selector labels once, and keep changeable metadata in non-selector labels.

What it checks

Every key and value in what you paste, against the rule for the half of the key it sits in, plus the limits that belong to the object rather than to the pair.

Why an image reference is not a legal label value

A label value is 63 characters of letters, digits, hyphens, underscores and dots. The slashes in a repository path and the colon before a tag are both outside that alphabet, so registry.example.com/team/api:1.4.0 breaks at three separate characters and takes the whole object down with it. A value that looks like an image reference or a URL is named as such in the finding, and the fix is the annotation rather than a shorter label.

A key is two parts with two different alphabets

An optional DNS subdomain prefix of up to 253 characters, a slash, then a name of up to 63. The prefix follows the DNS rules, lowercase with no underscores, while the name after the slash takes capitals, underscores and dots. So example.com/Team_Name is valid and Example.COM/team is not, and each half is reported against its own rule. A second slash is reported on its own, since there is nowhere for a third part to go.

The plus sign in a version

Semver build metadata is where this usually lands: 1.4.0+build.27 in app.kubernetes.io/version is rejected, and the error only names the label. The suggested fix is the underscore form, 1.4.0_build.27, which is what the Docker tag beside it has to do for the same reason.

Annotations have no alphabet, they have a budget

An annotation value is any string: no character rules and no per-value length. What it has is a shared limit of 262,144 bytes per object, summed across every key and every value, so the size check is done per object rather than per annotation. Anything past half of that is called out too, because kubectl apply stores the whole object as JSON in last-applied-configuration and that counts against the same total, so an object can be created and then fail to apply.

The values YAML changed before the API server saw them

Labels and annotations are both map[string]string. An unquoted 1.24 arrives as a number and an unquoted true as a boolean, and the API server rejects the whole object with a Go unmarshalling error that never names the key. An empty value is the opposite case: legal, and not the same as the label being absent, so it is reported as information rather than a fault.

Every place a label hides in a manifest

metadata.labels and metadata.annotations, the pod template's labels and annotations including a CronJob's jobTemplate, spec.selector.matchLabels, the keys and values inside matchExpressions, a Service's plain spec.selector, a NetworkPolicy's podSelector and a nodeSelector. Selector keys on a Deployment, StatefulSet, DaemonSet, ReplicaSet or Job also get a note that they are immutable, which is the one thing about a label that cannot be undone without deleting the object.

What a label is, what an annotation is, and why their rules differ

A label is an index key. The API server selects on it, so it is held to something short and comparable: 63 characters and a narrow alphabet. An annotation is somewhere to keep a string, nothing selects on it, and it has no alphabet or per-value limit at all. Almost every invalid label error is annotation-shaped data in a label: an image reference, a URL, a git subject line, a JSON blob.

A key, part by part

The prefix before the slash is a DNS subdomain and follows the DNS rules, which means lowercase and no underscores. The name after the slash is wider than that: capitals, digits, hyphens, underscores and dots, beginning and ending on a letter or a digit. Same key, two alphabets, which is why an uppercase letter is a fault in one half and fine in the other. The prefix is optional, and a key with no slash is just the name part.

example.com/Team_Name

  prefix   example.com
           optional, up to 253 characters
           a DNS subdomain: lowercase, with dots and
           hyphens and no underscores

  name     Team_Name
           up to 63 characters
           letters in either case, digits, "-", "_"
           and ".", beginning and ending alphanumeric

Example.COM/team         rejected, uppercase in the prefix
example.com/Team_Name    valid
team/sub/owner           rejected, a second slash

A label value, exactly

At most 63 characters of letters in either case, digits, hyphens, underscores and dots, starting and ending with a letter or a digit. It may also be empty. Note what is not in there: no slash, no colon, no plus, no space, and no @ sign, which between them rule out every reference-shaped string anyone wants to store.

[A-Za-z0-9]  and  -  _  .     up to 63 characters
first and last character alphanumeric

Production      valid, capitals are fine here
1.4.0           valid
""              valid, and not the same as absent
-1.4.0          rejected, cannot start with "-"

An image reference against that alphabet

Held up against the rule, an image reference fails at the registry separator, at every path separator and at the tag colon, and a digest reference adds an @ sign as well. There is no shortening that fixes it, because the characters are the problem rather than the length. The reference belongs in an annotation, and if you need to select on the version, the tag alone goes in the label.

registry.example.com/team/api:1.4.0
                    ^    ^   ^
                    |    |   position 30: ":" before the tag
                    |    position 26: "/" in the path
                    position 21: "/" after the registry

annotation   example.com/image: registry.example.com/team/api:1.4.0
label        app.kubernetes.io/version: 1.4.0

The plus sign, which fails twice

Semver build metadata is not a legal label value, and app.kubernetes.io/version is exactly where it lands. The same character is illegal in a Docker tag, so the image and the label fail together and only the label is mentioned in the error. The convention on both sides is the underscore.

1.4.0+build.27      rejected at position 6
1.4.0_build.27      valid

the tag has the same problem, and takes the
same repair

The annotation budget is per object, not per annotation

The API server sums the key and the value of every annotation on an object and refuses anything over 256 KB with "metadata.annotations: Too long". There is no individual limit, which is why a single large annotation and fifty medium ones fail identically. The half-budget warning matters because kubectl apply writes a full JSON copy of the object into last-applied-configuration, inside the same budget, so the create succeeds and the next apply does not. Server-side apply tracks the same information in managedFields and writes no annotation.

sum of every key + every value   <= 262,144 bytes

created with 200 KB of annotations   fine
kubectl apply on the same object     adds ~200 KB
                                     of last-applied
                                     and fails

kubectl apply --server-side          no annotation

Empty is not absent

An empty label value is valid and means something specific. Selecting on the key with no value matches only the objects where the key is set to empty. Selecting on the bare key matches anything carrying it whatever the value. Two different queries, and the difference is invisible in the manifest.

kubectl get pods -l tier=       key present, value empty
kubectl get pods -l tier        key present, any value
kubectl get pods -l '!tier'     key absent

The selector is the label you cannot change

spec.selector is immutable on an apps/v1 Deployment, StatefulSet, DaemonSet and ReplicaSet, and on a Job. Every key in it is fixed for the life of the object: an apply that changes one is rejected, and the way through is to delete and recreate, which for a Deployment means downtime you did not schedule. So the selector holds only what identifies the workload and never moves, and anything that does move, a version or a build number, goes on the pod template labels, which are free to change.

spec.selector.matchLabels
  app.kubernetes.io/name: api        fixed forever
  app.kubernetes.io/instance: prod   fixed forever

spec.template.metadata.labels
  app.kubernetes.io/name: api
  app.kubernetes.io/instance: prod
  app.kubernetes.io/version: 1.4.0   free to change

kubernetes.io and k8s.io are not your domains

Those prefixes are reserved for Kubernetes core components and its subprojects. Nothing stops you setting one today, and a future release is free to define the same key and act on it. The keys Kubernetes actually defines, topology.kubernetes.io/zone and the rest, are recognised rather than flagged, because a false positive on a manifest that is doing nothing wrong costs more than the rule earns. What is left over is your own key wearing a Kubernetes domain, and the answer is a domain you control.

kubernetes.io/team=platform               reserved prefix
topology.kubernetes.io/zone=eu-west-1a    a key Kubernetes defines
example.com/team=platform                 yours

What this does not check

Whether a selector matches anything. That needs both objects, the selector and the pods, and this reads the text in front of it: a valid selector that matches nothing is valid. It also does not know whether the values mean what you intend, and pairs pasted as a bare list are checked as labels, since a loose key=value line carries nothing that says it was an annotation.

checked      keys, values, lengths, positions,
             the per-object annotation budget

not checked  whether the selector matches a pod
             whether the value is the right value

More kubernetes tools

Kubernetes YAML Generator Answer a few questions, get a manifest Kubernetes YAML Validator Is this YAML valid, and where is it wrong? Kubernetes Rollout & Probe Timing Restart loops, capacity dips and stuck drains Kubernetes Service Checker Will this Service have any endpoints? Kubernetes Network Policy Viewer What can reach what, and what just broke Kubernetes Manifest Diff What actually changed Kubernetes Resource Calculator What these manifests actually reserve Kubernetes RBAC Viewer Who can actually do what Kubernetes Deployment Generator See the capacity dip before you deploy Kubernetes Service Generator port, targetPort, and which one you meant Kubernetes StatefulSet Generator Stable names, and volumes that outlive you Kubernetes QoS Class Calculator And where you sit in the eviction order Kubernetes Node Capacity Planner Allocatable is not capacity Kubernetes Max Pods per Node Calculator The ENI limit, not the 110 default Kubernetes Label Selector Tester Which key failed, not just that one did Kubernetes Taint & Toleration Tester Tolerations permit, they do not attract Kubernetes Resource Name Validator RFC 1123 subdomain, label, or 1035 Kubernetes CrashLoopBackOff Guide The status is a timer, not a cause Kubernetes TLS Certificate Decoder When it expires, and what it covers Kubernetes ServiceAccount Token Decoder Bound or legacy, and when it dies Kubernetes imagePullSecret Generator The registry URL nobody gets right Kubernetes cert-manager Certificates Issuer scope, and the rate limit that costs a week Kubernetes Flux HelmRelease Generator With the source it cannot work without Kubernetes Helm Chart Generator Chart.yaml, values, and working templates Kubernetes Argo CD Application Generator The defaults that are off, turned on Kubernetes Ingress Generator pathType, and the TLS secret that fails quietly Kubernetes HPA Generator Why it says unknown and never scales Kubernetes SecurityContext Generator Pod level, container level, and which wins Kubernetes Probe Generator How long it gets to start before liveness kills it Kubernetes PVC Generator Access modes your storage can actually do Kubernetes ResourceQuota Generator And the LimitRange that stops it breaking deploys Kubernetes CronJob Generator The fields that decide whether it runs Kubernetes NetworkPolicy Generator With the DNS rule already in it Kubernetes RBAC Generator And what it actually grants Kubernetes PodDisruptionBudget Generator Can this budget ever be satisfied? Kubernetes API Deprecation Checker What breaks on the next upgrade Kubernetes Kubeconfig Viewer Read it without handing it to anyone Kubernetes Quantity Parser What 512Mi, 100m and 1e3 actually mean Kubernetes Multi-Document YAML Splitter One file per object, in apply order Kubernetes .env to ConfigMap & Secret Split the credentials out on the way Kubernetes Secret Decoder See what is actually in there Kubernetes Ingress to Gateway API Which annotations silently stop working Kubernetes Manifest Visualizer What points at what, and what points at nothing Kubernetes Patch Tester Which --type, and what it deletes Kubernetes kubectl JSONPath Tester kubectl's dialect, not generic JSONPath Kubernetes Node Affinity Tester Required excludes, preferred only scores Kubernetes Helm Values Diff A missing key is a third value Kubernetes Pod and Service CIDR Fixed at cluster creation Kubernetes Secret Encoder base64 is not encryption Kubernetes API Version Checker Will this apply on that cluster Kubernetes Go Template Tester Go truthiness is not JavaScript's Kubernetes Cluster Cost Estimator At your rates, not a price table Kubernetes Kustomize Build Transformers run in kustomize's order Kubernetes Helm Chart Validator version is SemVer, appVersion is not

Elsewhere on the site