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 |
|---|---|
|
Which version of which API group defines this object’s schema, e.g. |
|
The object type within that group and version, e.g. |
|
Data that identifies the object and is common to every kind: |
|
The desired state, written by whoever creates or edits the object — a user, |
|
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 |
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
kubectlwarning 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 |
|
The default for application workloads. |
Cluster-scoped |
|
Infrastructure-level objects with no natural per-team owner. |
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
Source: Kubernetes Docs — Namespaces.
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 |
Value |
≤ 63 characters, the same alphanumeric/ |
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 |
|
|
Set-based |
|
|
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.
The recommended app.kubernetes.io/* labels
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 |
|---|---|---|
|
|
The application’s name, independent of any particular deployment of it. |
|
|
A unique name for this instance, distinguishing e.g. a prod and a canary install of the same app. |
|
|
The application (not chart) version currently running — update it alongside the image tag. |
|
|
The role within the architecture, e.g. |
|
|
The higher-level application this component belongs to. |
|
|
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 bykubectl 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"
Source: Kubernetes Docs — Annotations.
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 |
|---|---|
|
The owner is deleted immediately; the garbage collector deletes dependents afterwards, in the background. |
|
The owner is marked for deletion (a |
|
The owner is deleted and |
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
Source: Kubernetes Docs — Garbage Collection.
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:
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.
References
-
Rensin, D. (2015). Kubernetes: Scheduling the Future at Cloud Scale. O’Reilly. Ch. 3, "Organize, Grow, and Go".
-
Hightower, K., Burns, B., Beda, J. (2017). Kubernetes: Up and Running (1st ed. sampler). O’Reilly. Ch. 1, "Introduction".