Objects and the Kubernetes API

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

Every kubectl command and every controller loop in Kubernetes ends up doing the same thing: reading or writing objects through one HTTP API. Understanding that object model — what fields every object shares, how the API is versioned and discovered, how objects are named, grouped and selected, and how they are linked and cleaned up — explains almost everything else in this section, including how a Deployment turns into Pods (ReplicaSets and Deployments) and how kubectl apply reaches the API server in the first place (Introduction and Architecture). This page describes the API as it stands in Kubernetes 1.37 with kubectl 1.37.

The object model

Almost every resource kubectl can get — Pods, Deployments, Services, ConfigMaps, and the custom resources that extend the API — is a Kubernetes object: a persistent record of desired and observed state, stored in etcd behind the API server. Every object manifest has the same four top-level fields:

Field Meaning

apiVersion

Which version of which API group defines this object’s schema, e.g. apps/v1 or plain v1 for the core group.

kind

The object type within that group and version, e.g. Deployment, Pod, Namespace.

metadata

Data that identifies the object and is common to every kind: name, namespace, uid, labels, annotations, resourceVersion, creationTimestamp, ownerReferences, finalizers.

spec

The desired state, written by whoever creates or edits the object — a user, kubectl apply, or another controller.

status

The observed state, written back by the controller responsible for that kind (never by the client). Most objects follow a reconciliation loop that continuously drives status toward spec, exactly the loop introduced in Introduction and Architecture.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: catalog-api
  namespace: bookshelf
  labels:
    app.kubernetes.io/name: catalog-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app.kubernetes.io/name: catalog-api
  template:
    metadata:
      labels:
        app.kubernetes.io/name: catalog-api
    spec:
      containers:
        - name: catalog-api
          image: ghcr.io/example/bookshelf-catalog-api:1.0.0
          ports:
            - containerPort: 8080
status:
  replicas: 3
  readyReplicas: 3
  availableReplicas: 3

API groups and versions

The API is not one flat namespace of kinds. It is split into groups, each independently versioned:

Group apiVersion examples Typical kinds

Core (legacy, unnamed) group

v1

Pod, Service, Namespace, ConfigMap, Secret, Node, PersistentVolumeClaim

apps

apps/v1

Deployment, ReplicaSet, StatefulSet, DaemonSet

batch

batch/v1

Job, CronJob

networking.k8s.io

networking.k8s.io/v1

Ingress, NetworkPolicy

gateway.networking.k8s.io

gateway.networking.k8s.io/v1

Gateway, HTTPRoute (see Gateway API)

autoscaling

autoscaling/v2

HorizontalPodAutoscaler

policy

policy/v1

PodDisruptionBudget

Within a group, a version carries a maturity level baked into its name: v1alpha1 (disabled by default, may change or vanish without notice), v1beta1 (enabled by default, well-tested, but the schema may still change in incompatible ways before graduating), and v1 (GA — generally available, supported for as long as the group exists). A resource can be served simultaneously under several versions (the API server converts between them), and kubectl get <resource>.<version>.<group> pins a specific one, e.g. kubectl get deployments.v1.apps. Derived from Kubernetes Docs — The Kubernetes API and Kubernetes Docs — API concepts.

The deprecation policy and the migration guide

Kubernetes' API deprecation policy gives every API a documented lifetime before it can be removed:

  • A GA (v1) API version never expires and is never removed, though individual GA fields can still be deprecated.

  • A beta API version is supported for at least 9 months or 3 minor releases after it is deprecated (whichever is longer), and no more than one deprecated beta version stays in a still-served release.

  • An alpha API version can be removed in any release without notice.

  • A deprecation is always announced at least one release ahead, in the release notes and via a kubectl warning printed on every request against the deprecated version.

The deprecated API migration guide is the authoritative table of every API that has actually been removed, the release it was removed in, and its replacement — the reference to check before upgrading a cluster, not the general policy above. The best-known historical case, extensions/v1beta1 and apps/v1beta1/apps/v1beta2 Deployments, was removed in 1.16; any manifest still written against them (as in the 2017 sampler’s listings) must be rewritten against apps/v1 before it will apply to a 1.37 cluster.

API discovery

A client never has to guess which groups, versions and kinds a given cluster serves — it discovers them from the API server itself:

kubectl api-versions                       # every served "group/version", one per line
kubectl api-resources                      # every kind: NAME, SHORTNAMES, APIVERSION, NAMESPACED, KIND
kubectl api-resources --api-group=apps     # narrowed to one group
kubectl explain deployment.spec --recursive

kubectl api-resources is how you find that deploy is a short name for deployments, whether a kind is namespaced, and which apiVersion to write in a manifest, and it is exactly what kubectl explain and shell completion query under the hood. Derived from Kubernetes Docs — API concepts: API discovery.

Watching the API through kubectl proxy and curl

kubectl proxy opens a local HTTP proxy to the API server that reuses the current kubeconfig’s credentials, which makes it the simplest way to talk to the API directly without handling a bearer token:

kubectl proxy --port=8001 &
curl -s http://localhost:8001/apis/apps/v1/namespaces/bookshelf/deployments | jq '.items[].metadata.name'

# A watch: the connection stays open and streams one JSON "event" object per change
curl -s "http://localhost:8001/apis/apps/v1/namespaces/bookshelf/deployments?watch=true"
{"type":"ADDED","object":{"kind":"Deployment","metadata":{"name":"catalog-api", ...}}}
{"type":"MODIFIED","object":{"kind":"Deployment","metadata":{"name":"catalog-api", ...}}}

This is the same mechanism every controller uses instead of polling: it lists the current objects once, then keeps a long-lived watch connection open and reacts to ADDED / MODIFIED / DELETED events as they arrive — the "observe" step of the reconciliation loop from Introduction and Architecture. Source: Kubernetes Docs — Use an HTTP proxy to access the API.

resourceVersion and optimistic concurrency

Every object carries metadata.resourceVersion, an opaque string set by etcd and bumped on every write. It has two jobs: telling a watch where to resume (?resourceVersion=…​), and enforcing optimistic concurrency on updates. When a client sends a PUT, it includes the resourceVersion it last read; if the object has changed since (its stored resourceVersion no longer matches), the API server rejects the write with 409 Conflict instead of silently overwriting someone else’s change. kubectl apply and controllers handle this by re-reading and retrying; hand-written clients must do the same rather than retry blindly with a stale copy. Derived from Kubernetes Docs — API concepts: resource versions.

Names, UIDs and namespaces

Two identifiers distinguish objects: metadata.name is a human-chosen string, unique only within its namespace and kind, and reusable after deletion; metadata.uid is a cluster-wide unique identifier generated by the API server and never reused, which is what owner references (below) actually point at. A name must be a DNS subdomain name (lowercase alphanumerics, - and ., at most 253 characters) for most kinds, or the stricter DNS label form (at most 63 characters, no .) for kinds used as a label value or a hostname segment, such as a Service.

A namespace is a virtual cluster inside the real one: a scope for names, an attachment point for RBAC RoleBinding`s and `ResourceQuota/LimitRange, and a unit a team or environment can own. Not every kind lives inside one:

Scope Examples Notes

Namespaced

Pod, Deployment, Service, ConfigMap, Secret, Job, PersistentVolumeClaim

The default for application workloads. kubectl get <kind> only shows the current namespace’s objects unless -A/--all-namespaces is passed.

Cluster-scoped

Namespace, Node, PersistentVolume, StorageClass, ClusterRole, CustomResourceDefinition

Infrastructure-level objects with no natural per-team owner. kubectl api-resources --namespaced=false lists them all.

Use namespaces to separate environments or teams sharing one cluster (bookshelf-dev vs bookshelf-staging), to scope quotas and RBAC, or to isolate a third-party chart’s resources — exactly the "shared dev/test clusters" efficiency argument from Book 2 ch. 1. Do not use namespaces to shard a single application’s own microservices from each other for no operational reason; labels (below) already do that inside one namespace, with far less cross-namespace networking and DNS friction. A small cluster with one team and one environment is often simplest with everything in default plus labels.

kubectl create namespace bookshelf
kubectl get pods -n bookshelf
kubectl get persistentvolumes -A   # cluster-scoped: -A/-n both meaningless, but harmless

Labels and selectors

Labels are key/value pairs in metadata.labels, attached to an object purely to be queried against later — they carry no meaning to the API server itself. This is the mechanism Book 1 ch. 3 introduces as the replacement for naming conventions baked into resource names: a ReplicationController (now a ReplicaSet) does not know its Pods by name, it finds them with a label selector, which is what "loose coupling" means throughout this section.

Syntax and limits

Part Rule

Key

An optional prefix/ (a DNS subdomain, ≤ 253 characters, e.g. app.kubernetes.io/) followed by a required name segment (≤ 63 characters: alphanumerics, -, _, ., starting and ending alphanumeric). The empty prefix is reserved for end users; kubernetes.io/ and k8s.io/ prefixes are reserved for Kubernetes system components.

Value

≤ 63 characters, the same alphanumeric/-/_/. charset, or empty.

Book 1’s own numbers (a 64-character value limit with no separate prefix rule) predate the object schema being formalised this precisely; the limits above are what a 1.37 API server actually validates.

Label selectors

A selector is how a Service, ReplicaSet, NetworkPolicy or kubectl query picks a set of objects by their labels. Two forms exist, and a selector can freely mix them (comma-separated terms are ANDed together):

Form Syntax Example

Equality-based

key=value, key!=value

app.kubernetes.io/name=catalog-api,environment!=canary

Set-based

key in (v1,v2), key notin (v1,v2), key, !key

app.kubernetes.io/component in (api,worker),!deprecated

kubectl get pods -l app.kubernetes.io/name=catalog-api
kubectl get pods -l 'app.kubernetes.io/component in (api,worker)'
kubectl label pod <pod-name> app.kubernetes.io/name- debug=true   # pull one Pod out of its ReplicaSet for debugging

Relabelling a running Pod so it no longer matches its ReplicaSet’s selector — the last command above removes the selector label from one Pod (app.kubernetes.io/name-) and marks it debug=true so it is still easy to find — is exactly the Book 1 ch. 3 technique for isolating one misbehaving replica: the ReplicaSet controller immediately creates a fresh replacement Pod, while the relabelled one keeps running for inspection outside the controller’s reach. Source: Kubernetes Docs — Labels and Selectors.

Kubernetes documents a shared set of labels so that dashboards, kubectl, and third-party tooling can all reason about "which application does this object belong to" the same way, across every workload in a cluster. Every Bookshelf manifest in this section applies them consistently:

Label Example (catalog-api) Meaning

app.kubernetes.io/name

catalog-api

The application’s name, independent of any particular deployment of it.

app.kubernetes.io/instance

catalog-api-prod

A unique name for this instance, distinguishing e.g. a prod and a canary install of the same app.

app.kubernetes.io/version

1.0.0

The application (not chart) version currently running — update it alongside the image tag.

app.kubernetes.io/component

api

The role within the architecture, e.g. api, worker, database, cache.

app.kubernetes.io/part-of

bookshelf

The higher-level application this component belongs to.

app.kubernetes.io/managed-by

kubectl (or helm)

The tool managing this object’s lifecycle.

metadata:
  labels:
    app.kubernetes.io/name: catalog-api
    app.kubernetes.io/instance: catalog-api-prod
    app.kubernetes.io/version: "1.0.0"
    app.kubernetes.io/component: api
    app.kubernetes.io/part-of: bookshelf
    app.kubernetes.io/managed-by: kubectl

Only app.kubernetes.io/name (or another stable subset) should ever appear in a Deployment’s or Service’s own spec.selector — instance and version change across rollouts and environments, and a workload controller’s selector (Deployment, ReplicaSet, StatefulSet, DaemonSet) is immutable once created, so putting a changing label in it would require recreating the object. A Service’s spec.selector can be changed in place, which is exactly what blue/green switching in Deployment Strategies relies on. Source: Kubernetes Docs — Recommended labels.

Annotations

Annotations also attach key/value metadata, with the same key syntax as labels but values up to 256 KiB and no selector support — they exist to be read by tooling, not queried against. Book 1’s own examples (before annotations were formalised) already used labels for this purpose out of necessity; the current split is:

  • Labels — anything you (or a controller) will ever want to select on: identity, environment, ownership.

  • Annotations — everything else that still needs to travel with the object: build metadata (git-commit, build-date), a tool’s own bookkeeping (kubectl.kubernetes.io/last-applied-configuration, written by kubectl apply), configuration read by a controller that is not a selector, such as an Ingress class hint or a Prometheus scrape annotation (see Prometheus: Containers and Kubernetes Monitoring), or a human-readable description.

metadata:
  annotations:
    kubernetes.io/change-cause: "roll out catalog-api 1.0.0"
    example.com/build-commit: "8f3c9a1"

Field selectors

Field selectors filter objects by a field of their own JSON, not by a label, over a small, kind-specific set of supported fields (commonly metadata.name, metadata.namespace, and a handful of status/spec fields such as status.phase or spec.nodeName):

kubectl get pods --field-selector=status.phase=Running
kubectl get pods --field-selector=spec.nodeName=kind-worker,status.phase!=Succeeded
kubectl get events --field-selector involvedObject.name=catalog-api-7d4b6c9f9c-x2p7q

Unlike label selectors, arbitrary custom fields are not selectable — only the fields a kind’s API registers for it. Source: Kubernetes Docs — Field Selectors.

Owner references and garbage collection

Most objects a controller creates on your behalf — the ReplicaSet a Deployment creates, the Pods a ReplicaSet creates — carry an ownerReferences entry pointing at the object’s UID:

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: catalog-api-7d4b6c9f9c
  ownerReferences:
    - apiVersion: apps/v1
      kind: Deployment
      name: catalog-api
      uid: 3f2a9e6a-...
      controller: true
      blockOwnerDeletion: true

The garbage collector watches for owner deletions and, by default, deletes every dependent whose owner is gone (background cascading deletion for most kinds). kubectl delete exposes all three cascading modes explicitly:

--cascade= Behaviour

background (default for most kinds)

The owner is deleted immediately; the garbage collector deletes dependents afterwards, in the background.

foreground

The owner is marked for deletion (a foregroundDeletion finalizer is added) but stays visible until every dependent with blockOwnerDeletion: true is deleted first, then the owner itself disappears.

orphan

The owner is deleted and ownerReferences are stripped from its dependents, which are left running, now unowned.

kubectl delete deployment catalog-api --cascade=orphan     # Pods and ReplicaSet survive, unowned
kubectl delete deployment catalog-api --cascade=foreground  # blocks until every Pod is gone

Finalizers

A finalizer is a string in metadata.finalizers that a controller adds to an object it needs to run cleanup logic for before that object is actually removed. kubectl delete (or any DELETE call) on an object with finalizers only sets metadata.deletionTimestamp — the object stays visible, stuck "Terminating", until every controller that owns one of its finalizers has finished its cleanup and removed its own entry. The built-in kubernetes.io/pv-protection and kubernetes.io/pvc-protection finalizers (which stop a bound PersistentVolume/ PersistentVolumeClaim from being deleted, covered in Persistent Volumes and Storage Classes) and the foregroundDeletion finalizer above are the two most commonly seen in practice; a Pod stuck Terminating after a node has already disappeared is almost always a finalizer (or a kubectl delete --grace-period=0 --force, which skips graceful shutdown but still waits on finalizers) situation, not a hung kubectl. Source: Kubernetes Docs — Finalizers.

Owner references end to end: Deployment, ReplicaSet, Pod

A single Bookshelf catalog-api Deployment produces a chain of owned objects three levels deep, each link recorded as an ownerReferences entry on the child pointing back at the parent’s UID:

flowchart TB D["Deployment/catalog-api\nuid: 3f2a9e6a"] RS["ReplicaSet/catalog-api-7d4b6c9f9c\nownerReferences -> Deployment 3f2a9e6a"] P1["Pod/catalog-api-7d4b6c9f9c-x2p7q\nownerReferences -> ReplicaSet"] P2["Pod/catalog-api-7d4b6c9f9c-k9m2r\nownerReferences -> ReplicaSet"] P3["Pod/catalog-api-7d4b6c9f9c-q4t8w\nownerReferences -> ReplicaSet"] D -->|creates and owns| RS RS -->|creates and owns| P1 RS -->|creates and owns| P2 RS -->|creates and owns| P3

Deleting the Deployment with the default --cascade=background therefore deletes the ReplicaSet, which the garbage collector then follows down to all three Pods; a new rollout instead creates a new ReplicaSet (a new label-template hash) alongside the old one, scaling it up and the old one down, which is exactly the mechanism ReplicaSets and Deployments covers next.