skip to content

Kubernetes manifests declare fields such as `apiVersion: apps/v1` and `kind: Deployment`. Explain how group, version and kind map onto the REST URLs the API server exposes, and how the same stored object can be served at more than one version.

level: middleimportance: should knowfreq 45%

answer

  1. apiVersion = group/version; kind = type; URL uses plural resource
  2. core group = empty name → /api/v1; others → /apis/<group>/<version>
  3. GVK ≠ GVR; subresources /status, /scale have own RBAC
  4. one storage version + conversion via internal hub
  5. CRDs: storage:true, storedVersions, conversion webhook

basics

~10 s

apiVersion is group/version and kind is the type; together they select a REST path like /apis/apps/v1/namespaces/ns/deployments. The server stores one storage version and converts to whichever served version a client requests.

solid answer

~50 s

`apiVersion: apps/v1` splits into group `apps` and version `v1`; `kind: Deployment` is the Go type name. The API server maps that to a lowercase plural **resource** (`deployments`) and builds the path: - core group (historical, empty group name): `/api/v1/namespaces/<ns>/pods/<name>` - named groups: `/apis/apps/v1/namespaces/<ns>/deployments/<name>` - cluster-scoped kinds drop the namespace segment: `/apis/rbac.authorization.k8s.io/v1/clusterroles/<name>` The distinction people miss is **GVK versus GVR**: kind is the schema type, resource is the URL noun, and the mapping is not always mechanical — one kind can back several resources (`deployments`, `deployments/status`, `deployments/scale`), and subresources have their own paths and RBAC rules. A group can serve several versions at once. The server persists exactly one **storage version** and converts on the way in and out through an internal hub type, so `v1beta1` and `v1` are two views of the same stored object. `kubectl api-resources` and `kubectl api-versions` enumerate what a given cluster serves.

code

bash · 8 lines
bash
kubectl api-resources --api-group=apps
kubectl api-versions | sort | head

# same object, requested at an explicit group/version path
kubectl get --raw /apis/apps/v1/namespaces/default/deployments/web | head -c 300

# subresources have their own paths
kubectl get --raw /apis/apps/v1/namespaces/default/deployments/web/scale

go deeper

for a junior

Be able to read apiVersion and kind and say which group/version an object belongs to, and that core objects use a bare v1.

for a middle

Build the REST path from the triple, distinguish namespaced from cluster-scoped, and explain that several versions can be served over one stored version.

for a senior

Cover GVK versus GVR, subresources with independent RBAC, the internal hub conversion, and what a storage-version migration involves before dropping an API version.

for a principal

Reason about API evolution policy: deprecation windows, conversion-webhook availability as a control-plane dependency, and the upgrade risk of removed versions across a fleet.

## The three coordinates Every Kubernetes object is identified by a **Group, Version, Kind** triple. - **Group** partitions the API into areas: `apps`, `batch`, `networking.k8s.io`, `rbac.authorization.k8s.io`, plus custom groups like `monitoring.coreos.com`. Groups can be versioned and extended independently, which is the whole reason they exist. The original objects — Pod, Service, ConfigMap, Node, Namespace — live in the **core group**, whose name is the empty string, written as bare `apiVersion: v1`. - **Version** is the stability contract: `v1alpha1` (may change or vanish, often disabled by default), `v1beta1` (schema fairly stable, may still change), `v1` (stable, guaranteed). - **Kind** is the type name in Go/CamelCase: `Deployment`, `Pod`, `ClusterRole`. ## From GVK to a URL URLs are built from the **resource**, not the kind: the lowercase plural noun `deployments`, `pods`, `clusterroles`. The mapping from Kind to Resource is provided by the server's discovery documents (`/api`, `/apis`, `/openapi/v3`) and cached by clients — this is the RESTMapper in client-go, and it is why `kubectl` can handle CRDs it has never seen. Path shapes: ``` /api/v1/namespaces/{ns}/pods/{name} # core group, namespaced /api/v1/nodes/{name} # core group, cluster-scoped /apis/apps/v1/namespaces/{ns}/deployments/{name} # named group, namespaced /apis/apps/v1/deployments # cross-namespace list /apis/rbac.authorization.k8s.io/v1/clusterroles/{name} ``` Verbs follow HTTP: GET one or list a collection, POST to create, PUT to replace, PATCH to modify (strategic-merge, JSON-merge, JSON-patch, or server-side apply via `application/apply-patch+yaml`), DELETE, and GET with `?watch=true` to stream. ## GVK versus GVR, and subresources This is where interviews probe. **GVK** names a schema type; **GVR** names an endpoint. They are related but not interchangeable, and the difference is load-bearing: - A kind can back multiple resources: `deployments`, `deployments/status`, `deployments/scale`. - **Subresources** have their own URL and their own RBAC verb, so you can grant permission to update a Pod's `status` without granting permission to modify the Pod's spec. Controllers rely on this: writing to `/status` avoids fighting with users editing the spec, and `metadata.generation` versus `status.observedGeneration` is how a controller detects it has caught up. - Some subresources are not object storage at all: `pods/exec`, `pods/portforward`, `pods/log`, and `nodes/proxy` are streaming or proxying endpoints that happen to hang off a resource path — and RBAC governs them exactly the same way. ## Serving several versions of one object A group serves whichever versions are enabled. Internally, the API server keeps an **internal hub version** and defines conversion functions between each external version and that hub. On write it decodes the client's version to internal, applies defaulting, and encodes it into the single configured **storage version** for etcd. On read it decodes from the storage version to internal and encodes into whichever version the client's URL asked for. Consequences worth stating: - The `apiVersion` you get back is the one you asked for, not necessarily the one on disk. - Promoting a beta API to GA does not require rewriting etcd; it changes which version is stored and served, and a `StorageVersionMigration` (or a simple no-op read-write pass) rewrites objects lazily. - Once a version is removed from the server, objects stored in it are unreadable unless something converted them first — the practical reason deprecation windows exist and why `kubectl convert` and API-deprecation alerts matter before upgrades. For **CustomResourceDefinitions** the same model applies, but you supply the conversion: either `strategy: None` (fields are simply reinterpreted, which only works if versions are structurally compatible) or a **conversion webhook** the API server calls. Exactly one CRD version is marked `storage: true`, and `status.storedVersions` records which versions actually exist in etcd — you cannot drop a version still listed there without migrating. ## Discovery in practice ``` kubectl api-resources # kind ↔ resource ↔ group, namespaced or not kubectl api-versions # every group/version served here kubectl explain deployment.spec # schema straight from the server's OpenAPI ``` Because this is served by the cluster rather than compiled into the client, a CRD installed a minute ago is immediately usable by `kubectl` — and, conversely, a broken aggregated API can stall discovery and make otherwise unrelated `kubectl` commands slow.

  • Why does a controller update status through the /status subresource instead of writing the whole object?
    The subresource is a separate endpoint with its own RBAC verb, so the controller can be granted status-write without spec-write, and its write cannot clobber a user's concurrent spec edit. It also keeps `metadata.generation` untouched, which is precisely how `status.observedGeneration` lets the controller and observers tell whether the controller has caught up with the latest spec.
  • What breaks if you remove a version from a CRD while objects are still stored in it?
    The API server can no longer decode those objects, so reads fail and the resource becomes effectively unreadable. That is why `status.storedVersions` on the CRD lists every version actually present in etcd and the API refuses removals that would strand data: you must first read-and-rewrite each object under the new storage version (a storage-version migration), then drop the old version.

saying these in an interview costs you the question

  • Treating kind and resource as the same thing, so subresources and their separate RBAC verbs are missed
  • Thinking every served version is stored separately in etcd
  • Assuming apiVersion: v1 means "version 1 of Kubernetes" rather than the core group's v1
  • Believing conversion between CRD versions is automatic without either structural compatibility or a conversion webhook
  • Hardcoding URL paths instead of using discovery, then breaking on custom resources

context