Skip to main content
RunBook Academy

Git, CI/CD & GitOpsCXIV · Deployment MarkersRuntimeIdentity

Deploy timestamp and version labels — the runtime identity

Intermediate⏱ ~24 mingitkubectl

What you'll learn

  • Write the app.kubernetes.io/version label onto a Deployment from the pipeline so the runtime identity is queryable
  • Write a deploy-timestamp label so the wall-clock moment of the change is preserved on the workload object
  • Recognise why a mutable label breaks the trail - the version must be immutable for the label to identify the deploy that produced the measurement
  • Query the runtime identity through label selectors and through the Prometheus metric labels the label propagates to

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

The change-cause annotation carries the human-readable cause of the deploy. The label carries the immutable runtime identity - the version string and the deploy timestamp - that the on-call engineer queries during an investigation. The annotation answers “why is this running”; the label answers “what is running and since when”. A workload object that has only the annotation is half-marked; a workload object that has only the label is also half-marked. The two surfaces together produce the runtime identity the trail requires.

The version label

The conventional version label for a Kubernetes workload is app.kubernetes.io/version. The label value is the version string of the running artifact - the same string the OpenTelemetry service.version attribute carries and the same string the pipeline writes to the container image tag. The label is the join key between the workload object, the running pods, and the metric labels.

NAME=api
NS=payments
VERSION=v3.4.1
kubectl label deploy $NAME -n $NS \
  app.kubernetes.io/version=$VERSION --overwrite

The label is indexed in the Kubernetes API. A label selector app.kubernetes.io/version=v3.4.1 returns every object that carries that version string. The selector is the query the investigator runs to find every Deployment, every Pod, every Service that is running the suspect version. The label is propagated to the pods that the Deployment manages; the pods inherit the label through the deployment selector. The pods are the running replicas whose metrics carry the version label.

The version label propagates to the metric labels. The kube-state-metrics exporter reads the Deployment labels and exposes them as Prometheus metric labels; the workload SDK emits the version as a resource attribute on every span, and the Prometheus receiver attaches the attribute as a label on the histogram. The investigator queries http_request_duration_seconds_bucket{service="api",version="v3.4.1"} and retrieves only the measurements produced by the suspect version.

flowchart LR
    A["Pipeline\nkubectl label"] --> B["Deployment label\napp.kubernetes.io/version"]
    B --> C["Pod label\ninherited via selector"]
    B --> D["kube-state-metrics\nmetric label"]
    C --> E["Workload SDK\nresource attribute"]
    D --> F["Prometheus query\nfiltered by version"]
    E --> G["Tempo / Loki\nfiltered by version"]

The deploy-timestamp label

The second label the runtime identity requires is the deploy timestamp. The timestamp is the wall-clock moment the change landed in the cluster. The annotation carries the timestamp implicitly through its metadata.creationTimestamp; the label carries it explicitly as a queryable value. The convention is to use a label key like deploy.timestamp or app.kubernetes.io/deploy-timestamp, with a value in RFC 3339 UTC.

NAME=api
NS=payments
TIMESTAMP=$(date -u +%FT%TZ)
kubectl label deploy $NAME -n $NS \
  deploy.timestamp="$TIMESTAMP" --overwrite

The deploy-timestamp label is the answer to “since when has this version been running”. A label selector deploy.timestamp>=2026-08-21T13:00:00Z returns every Deployment that has been updated since the given moment. The selector is the query the investigator runs to find every workload that changed in the incident window. The timestamp is also the value the change-cause annotation’s apply time can be cross-checked against: if the label timestamp and the annotation timestamp disagree, the deploy was tampered with or was applied by a non-pipeline actor.

Why the label must be queryable

The label is indexed; the annotation is not. The Kubernetes API exposes label selectors as a first-class query primitive; a label selector is evaluated by the API server without scanning the entire object store. The annotation is a string field; querying annotations requires reading each object and string-matching the value, which is operationally expensive at cluster scale.

The label selector is the primitive the operational tools use. kubectl get deploy -l app.kubernetes.io/version=v3.4.1 returns the matching Deployments in milliseconds. The Prometheus query kube_deployment_labels{label_app_kubernetes_io_version="v3.4.1"} returns the matching Deployments as a time series. The Grafana variable ${deployment_labels} is populated from the label selector. The label is the field the operational surface queries; the annotation is the field the engineer reads.

What the label propagates to

The label propagates to three surfaces. First, the pods the Deployment manages. The Deployment’s label selector matches the pods; the label is added to the pods through the spec.template.metadata.labels when the manifest is applied. Second, the kube-state-metrics metric labels. The exporter reads the Deployment labels and exposes them as Prometheus metric labels. Third, the workload SDK resource attributes. The SDK reads the OTEL_SERVICE_VERSION environment variable from the container spec and emits it as the service.version resource attribute.

The three propagations make the label the single source of truth for the version identity. The pipeline writes the label once; the cluster, the metrics, and the traces consume it. A label that disagrees with the trace attribute is a label whose write did not propagate.

Production discipline

  1. Write the version label from the pipeline, with the version string from the build. The label must be immutable for the label to identify the deploy.
  2. Write the deploy-timestamp label as RFC 3339 UTC. The timestamp is the wall-clock moment of the apply; the format must be parseable by Grafana variables and by Prometheus label matchers.
  3. Propagate the label to the pods, the metric labels, and the SDK resource attributes. The label is the single source of truth; the propagations must agree.
  4. Enforce label immutability outside the deploy pipeline. A team policy that forbids ad-hoc label writes from operator shell sessions is the structural defence.
  5. Audit the absence of the version label as a pipeline failure. A weekly query for Deployments missing app.kubernetes.io/version surfaces the silent half-deploys.

Cross-course references

  • This course, Part CXIV-02 (ChangeCause) covers the annotation that complements the label with the human-readable cause.
  • Observability course - Part LII (Exemplars) covers the metric-to-trace pivot that pairs with the version label for trace correlation.
  • Kubernetes for Production Sysadmins - Parts XXXVIII-XL cover the workload label patterns at the runtime layer.

Quiz

Knowledge check · 4 questions

  1. Q1. What is the operational consequence of allowing an operator to rewrite the app.kubernetes.io/version label from a shell session after a deploy has completed?

  2. Q2. The change-cause annotation is not sufficient as the runtime identity of a workload; the version label is redundant because the same information is recoverable from the annotation text.

  3. Q3. Name the two labels the runtime identity requires, the format of the timestamp label, and the three surfaces the version label propagates to.

  4. Q4. Diagnose the trail gap and recommend the structural fix.

    Team T writes the version label onto Deployments from the pipeline. The pipeline writes the label as a best-effort step after the manifest is applied. One day, a Deploy job fails halfway: the manifest is applied but the label write returns a 409 Conflict because the label already exists. The pipeline reports success because the rollout completed. The Deployments in the affected environment are running the new version but carrying the old version label. Two weeks later, a regression appears; the investigator filters the metrics by version=v3.4.1, expecting to find the suspect version, and finds nothing - the suspect version is actually labelled v3.4.0.

Passing score: 75%. Answers are checked in this browser.