Everything in Kubernetes is an API object. Pods, Services, Deployments, Secrets, Nodes — even RBAC rules — are all resources exposed through a RESTful API. Understanding the API model is essential: it's the foundation for writing manifests, building operators, and debugging any issue.
1. API Groups — Organizing the API
The Kubernetes API isn't one flat list. It's organized into API groups — logical collections of related resources. This structure enables independent versioning and extension.
The Two API Paths
| Path Pattern | What Lives Here | Example |
|---|---|---|
/api/v1/... | Core group (legacy, no group name) | Pods, Services, Nodes, ConfigMaps, Secrets |
/apis/{group}/{version}/... | Named groups | Deployments, StatefulSets, CRDs |
Common API Groups
| Group | API Path | Resources |
|---|---|---|
| (core) | /api/v1 | Pod, Service, ConfigMap, Secret, Node, Namespace, PV, PVC |
apps | /apis/apps/v1 | Deployment, StatefulSet, DaemonSet, ReplicaSet |
batch | /apis/batch/v1 | Job, CronJob |
networking.k8s.io | /apis/networking.k8s.io/v1 | Ingress, NetworkPolicy, IngressClass |
rbac.authorization.k8s.io | /apis/rbac.authorization.k8s.io/v1 | Role, ClusterRole, RoleBinding, ClusterRoleBinding |
storage.k8s.io | /apis/storage.k8s.io/v1 | StorageClass, CSIDriver, VolumeAttachment |
policy | /apis/policy/v1 | PodDisruptionBudget |
autoscaling | /apis/autoscaling/v2 | HorizontalPodAutoscaler |
# Discover all API groups on your cluster: kubectl api-versions # List resources in a specific group: kubectl api-resources --api-group=apps # NAME SHORTNAMES APIVERSION NAMESPACED KIND # deployments deploy apps/v1 true Deployment # statefulsets sts apps/v1 true StatefulSet # daemonsets ds apps/v1 true DaemonSet # replicasets rs apps/v1 true ReplicaSet
apiVersion: apps/v1 in a YAML manifest, you're specifying the API group (apps) and version (v1). The core group is special — you just write apiVersion: v1 (no group name).
2. GVK and GVR — The Coordinate System
Every Kubernetes resource type can be uniquely identified two ways:
GVK — Group, Version, Kind (the "type")
Identifies a type of object. Used in YAML manifests and by controllers internally.
# In a manifest, you declare GVK: apiVersion: apps/v1 # Group: apps, Version: v1 kind: Deployment # Kind: Deployment # For core group: apiVersion: v1 # Group: (core), Version: v1 kind: Pod # Kind: Pod
GVR — Group, Version, Resource (the "URL")
Identifies the REST path to interact with that type. Used by clients to construct API URLs.
# GVR maps to the API URL:
# Group: apps, Version: v1, Resource: deployments
# → /apis/apps/v1/namespaces/{ns}/deployments/{name}
# Group: (core), Version: v1, Resource: pods
# → /api/v1/namespaces/{ns}/pods/{name}
GVK ↔ GVR Mapping
| Concept | Identifies | Example | Used By |
|---|---|---|---|
| GVK | A type of object | apps/v1 Deployment | YAML manifests, controller code |
| GVR | A REST endpoint | apps/v1 deployments | HTTP clients, dynamic clients |
| Kind | Singular, CamelCase type name | Deployment | Manifests (kind: field) |
| Resource | Plural, lowercase URL segment | deployments | API paths, RBAC rules |
Deployment (CamelCase, singular). Resource is deployments (lowercase, plural). In RBAC, you write resources: ["deployments"] — the resource name, not the Kind. A common source of confusion.
3. Object Structure — The Spec/Status Convention
Every Kubernetes object follows the same four-part structure:
apiVersion: apps/v1 # ← GV (group + version)
kind: Deployment # ← K (kind)
metadata: # ← Identity + system annotations
name: nginx
namespace: default
labels:
app: nginx
uid: a1b2c3d4-...
resourceVersion: "12345"
creationTimestamp: "2024-01-15T10:00:00Z"
spec: # ← DESIRED state (user-controlled)
replicas: 3
selector:
matchLabels:
app: nginx
template:
...
status: # ← ACTUAL state (system-controlled)
availableReplicas: 3
readyReplicas: 3
conditions:
- type: Available
status: "True"
The Four Sections
| Section | Who Writes It | Purpose |
|---|---|---|
apiVersion + kind | User | Identifies the type (GVK) |
metadata | User + System | Name, namespace, labels, annotations, UID, resourceVersion |
spec | User | Desired state — what you want |
status | System (controllers) | Observed state — what actually is |
spec (intent). Controllers read spec, compare with reality, and write status. The gap between spec and status is what controllers work to close. This split exists on almost every object.
Key Metadata Fields
| Field | Set By | Purpose |
|---|---|---|
name | User | Unique within namespace (DNS-compatible) |
namespace | User | Isolation boundary (some resources are cluster-scoped) |
uid | System | Globally unique identifier (UUID) |
resourceVersion | System | etcd revision — used for optimistic concurrency |
generation | System | Increments on spec changes (not status) |
labels | User | Key-value pairs for selection/filtering |
annotations | User/System | Arbitrary metadata (not selectable) |
ownerReferences | System | Parent object — enables garbage collection |
finalizers | Controllers | Block deletion until cleanup completes |
resourceVersion and Optimistic Concurrency
Every object has a resourceVersion (an opaque string, actually the etcd ModRevision). When you update an object, you must include the current resourceVersion. If it doesn't match (someone else updated it), the API server returns 409 Conflict.
# This is why "kubectl apply" can fail with: # "the object has been modified; please apply your changes to the latest version" # Solution: re-read the object, re-apply your changes, try again.
4. Subresources
Some resources expose subresources — separate API endpoints nested under the main resource path. They have their own access control and semantics.
| Subresource | Path | Purpose |
|---|---|---|
/status | /apis/apps/v1/.../deployments/nginx/status | Update status without changing spec |
/scale | /apis/apps/v1/.../deployments/nginx/scale | Read/write replica count (used by HPA) |
/log | /api/v1/.../pods/nginx/log | Stream container logs |
/exec | /api/v1/.../pods/nginx/exec | Execute command in container |
/portforward | /api/v1/.../pods/nginx/portforward | Tunnel TCP traffic |
/eviction | /api/v1/.../pods/nginx/eviction | Graceful Pod eviction (respects PDB) |
update deployments vs update deployments/status.
5. API Versioning & Maturity
Kubernetes resources evolve through version stages:
| Stage | Format | Stability | Example |
|---|---|---|---|
| Alpha | v1alpha1 | May change/disappear. Disabled by default. | flowcontrol.apiserver.k8s.io/v1alpha1 |
| Beta | v1beta1 | Well-tested, may have breaking changes. Enabled by default (since 1.22: feature gate required). | admissionregistration.k8s.io/v1beta1 |
| Stable | v1, v2 | Guaranteed backward-compatible. Safe for production. | apps/v1 |
# Check which versions are available for a resource: kubectl api-resources | grep deployment # deployments deploy apps/v1 true Deployment # See all versions the server supports for a group: kubectl api-versions | grep apps # apps/v1
pluto scan your YAML for deprecated API versions before cluster upgrades.
Version Conversion
The API server can store objects in one version and serve them in another. For example, a CRD might store as v1 but serve as v1 and v2. Conversion webhooks handle the transformation between versions.
6. REST URL Patterns
Once you understand GVR, you can predict any API URL:
Namespaced Resources
# List all pods in namespace "production": GET /api/v1/namespaces/production/pods # Get a specific pod: GET /api/v1/namespaces/production/pods/nginx-abc123 # Create a deployment: POST /apis/apps/v1/namespaces/default/deployments # Update status subresource: PUT /apis/apps/v1/namespaces/default/deployments/nginx/status # Watch for changes: GET /api/v1/namespaces/default/pods?watch=true&resourceVersion=9876
Cluster-Scoped Resources
# List all nodes (no namespace): GET /api/v1/nodes # Get a specific ClusterRole: GET /apis/rbac.authorization.k8s.io/v1/clusterroles/admin # List PersistentVolumes: GET /api/v1/persistentvolumes
Namespaced vs Cluster-Scoped
| Cluster-Scoped (no namespace) | Namespaced |
|---|---|
| Node, Namespace, PersistentVolume | Pod, Deployment, Service, ConfigMap |
| ClusterRole, ClusterRoleBinding | Role, RoleBinding |
| StorageClass, IngressClass | Ingress, NetworkPolicy |
| CRD (the definition itself) | CR instances (usually) |
# Quick check: is a resource namespaced? kubectl api-resources --namespaced=true # namespaced resources kubectl api-resources --namespaced=false # cluster-scoped resources
7. Labels & Selectors — How Objects Find Each Other
Labels are the connective tissue of Kubernetes. They're how Services find Pods, ReplicaSets track their Pods, and NetworkPolicies target workloads.
Label Syntax
metadata:
labels:
app: nginx # simple key=value
environment: production
app.kubernetes.io/name: nginx # recommended structured labels
app.kubernetes.io/version: "1.25"
app.kubernetes.io/component: frontend
Selector Types
| Type | Syntax | Used By |
|---|---|---|
| Equality-based | app=nginx, env!=staging | Services, ReplicationControllers |
| Set-based | env in (production, staging), tier notin (frontend), !canary | Deployments, ReplicaSets, Jobs, NetworkPolicies |
# kubectl label selectors:
kubectl get pods -l app=nginx
kubectl get pods -l 'environment in (prod,staging)'
kubectl get pods -l app=nginx,version=v2
# In YAML (matchLabels + matchExpressions):
selector:
matchLabels:
app: nginx
matchExpressions:
- key: environment
operator: In
values: [production, staging]
spec.selector is immutable after creation. You cannot change which Pods a Deployment manages after it's created. This prevents accidental adoption/orphaning of Pods. Plan your labels carefully.
Labels vs Annotations
| Labels | Annotations | |
|---|---|---|
| Purpose | Identify and select objects | Attach non-identifying metadata |
| Indexed? | Yes — efficient server-side filtering | No — stored but not queryable |
| Size limit | 63 chars value, 253 chars key | 256KB total annotations |
| Use cases | Service routing, scheduling, RBAC scoping | Build info, deploy tooling metadata, config hashes |
kubectl.kubernetes.io/last-applied-configuration (stores last apply), deployment.kubernetes.io/revision (rollout tracking), custom ones like company.io/owner: team-platform for cost allocation. Labels are for machines to filter; annotations are for humans and tools to read.
Summary
| Concept | Remember |
|---|---|
| API Groups | Logical collections: core (/api/v1), named (/apis/apps/v1) |
| GVK | Group + Version + Kind = type identity (used in YAML) |
| GVR | Group + Version + Resource = REST path (used in URLs, RBAC) |
| Spec/Status | You write spec (desire), controllers write status (reality) |
| resourceVersion | Optimistic concurrency — prevents lost updates |
| Subresources | Separate endpoints for /status, /scale, /exec, /log |
| Labels | Selectable identity — how objects find each other |
📝 Quiz: The Kubernetes API Model
Q1: What's the full API URL to get the Deployment "web-app" in namespace "production"?
/apis/apps/v1/namespaces/production/deployments/web-appGroup: apps, Version: v1, Resource: deployments (plural, lowercase).
Q2: In an RBAC rule, you want to allow reading Deployments. Do you use the Kind or the Resource name?
resources: ["deployments"]. RBAC never uses Kind directly. The apiGroup must also be specified: apiGroups: ["apps"].Q3: A controller updates a Deployment's status, but another process updated the same object 1ms earlier. What happens?
resourceVersion in the update request doesn't match the current version in etcd. The controller must re-read the object, re-apply its status change, and retry.Q4: Why is the /status subresource separate from the main resource endpoint?
update deployments/status without being able to change spec. Users get update deployments without being able to fake status. It also avoids conflicts — spec and status updates use different resourceVersions.Q5: You have a Service selecting app: web. Can you change the Service's selector after creation?
Q6: What's the difference between apiVersion: v1 and apiVersion: apps/v1?
v1 is the core group (legacy, no group name). Its REST path is /api/v1/.... apps/v1 is a named group called "apps" at version v1. Its REST path is /apis/apps/v1/.... The core group exists for historical reasons — early K8s had no groups.