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 PatternWhat Lives HereExample
/api/v1/...Core group (legacy, no group name)Pods, Services, Nodes, ConfigMaps, Secrets
/apis/{group}/{version}/...Named groupsDeployments, StatefulSets, CRDs

Common API Groups

GroupAPI PathResources
(core)/api/v1Pod, Service, ConfigMap, Secret, Node, Namespace, PV, PVC
apps/apis/apps/v1Deployment, StatefulSet, DaemonSet, ReplicaSet
batch/apis/batch/v1Job, CronJob
networking.k8s.io/apis/networking.k8s.io/v1Ingress, NetworkPolicy, IngressClass
rbac.authorization.k8s.io/apis/rbac.authorization.k8s.io/v1Role, ClusterRole, RoleBinding, ClusterRoleBinding
storage.k8s.io/apis/storage.k8s.io/v1StorageClass, CSIDriver, VolumeAttachment
policy/apis/policy/v1PodDisruptionBudget
autoscaling/apis/autoscaling/v2HorizontalPodAutoscaler
# 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
Why this matters: When you write 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

GVK (Type Identity) Group: apps Version: v1 Kind: Deployment REST Mapper GVR (URL Path) Group: apps Version: v1 Resource: deployments /apis/apps/v1/namespaces/default/deployments/nginx
ConceptIdentifiesExampleUsed By
GVKA type of objectapps/v1 DeploymentYAML manifests, controller code
GVRA REST endpointapps/v1 deploymentsHTTP clients, dynamic clients
KindSingular, CamelCase type nameDeploymentManifests (kind: field)
ResourcePlural, lowercase URL segmentdeploymentsAPI paths, RBAC rules
Kind ≠ Resource: Kind is 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

SectionWho Writes ItPurpose
apiVersion + kindUserIdentifies the type (GVK)
metadataUser + SystemName, namespace, labels, annotations, UID, resourceVersion
specUserDesired state — what you want
statusSystem (controllers)Observed state — what actually is
The Spec/Status split is the reconciliation contract: You write 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

FieldSet ByPurpose
nameUserUnique within namespace (DNS-compatible)
namespaceUserIsolation boundary (some resources are cluster-scoped)
uidSystemGlobally unique identifier (UUID)
resourceVersionSystemetcd revision — used for optimistic concurrency
generationSystemIncrements on spec changes (not status)
labelsUserKey-value pairs for selection/filtering
annotationsUser/SystemArbitrary metadata (not selectable)
ownerReferencesSystemParent object — enables garbage collection
finalizersControllersBlock 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.
In controller code, this optimistic concurrency pattern avoids locks entirely. If two controllers try to update the same object simultaneously, one wins and the other gets a 409 and must retry. The client-go library handles this retry logic automatically in most cases.

4. Subresources

Some resources expose subresources — separate API endpoints nested under the main resource path. They have their own access control and semantics.

SubresourcePathPurpose
/status/apis/apps/v1/.../deployments/nginx/statusUpdate status without changing spec
/scale/apis/apps/v1/.../deployments/nginx/scaleRead/write replica count (used by HPA)
/log/api/v1/.../pods/nginx/logStream container logs
/exec/api/v1/.../pods/nginx/execExecute command in container
/portforward/api/v1/.../pods/nginx/portforwardTunnel TCP traffic
/eviction/api/v1/.../pods/nginx/evictionGraceful Pod eviction (respects PDB)
Why /status is a separate subresource: It enables RBAC separation. Controllers need to update status but shouldn't change spec. Users need to update spec but shouldn't fake status. With the status subresource, these are different permissions: update deployments vs update deployments/status.

5. API Versioning & Maturity

Kubernetes resources evolve through version stages:

StageFormatStabilityExample
Alphav1alpha1May change/disappear. Disabled by default.flowcontrol.apiserver.k8s.io/v1alpha1
Betav1beta1Well-tested, may have breaking changes. Enabled by default (since 1.22: feature gate required).admissionregistration.k8s.io/v1beta1
Stablev1, v2Guaranteed 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
Always use stable versions in production manifests. When K8s deprecates a version, it still works for several releases (deprecation window), then is removed. Tools like 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, PersistentVolumePod, Deployment, Service, ConfigMap
ClusterRole, ClusterRoleBindingRole, RoleBinding
StorageClass, IngressClassIngress, 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
Practical rule: If a resource makes sense only within a team/app boundary → namespaced. If it's a cluster-wide configuration → cluster-scoped. Namespaces are themselves cluster-scoped (you can't nest namespaces).

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

TypeSyntaxUsed By
Equality-basedapp=nginx, env!=stagingServices, ReplicationControllers
Set-basedenv in (production, staging), tier notin (frontend), !canaryDeployments, 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]
Immutable selectors: A Deployment's 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

LabelsAnnotations
PurposeIdentify and select objectsAttach non-identifying metadata
Indexed?Yes — efficient server-side filteringNo — stored but not queryable
Size limit63 chars value, 253 chars key256KB total annotations
Use casesService routing, scheduling, RBAC scopingBuild info, deploy tooling metadata, config hashes
Common annotation patterns: 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

ConceptRemember
API GroupsLogical collections: core (/api/v1), named (/apis/apps/v1)
GVKGroup + Version + Kind = type identity (used in YAML)
GVRGroup + Version + Resource = REST path (used in URLs, RBAC)
Spec/StatusYou write spec (desire), controllers write status (reality)
resourceVersionOptimistic concurrency — prevents lost updates
SubresourcesSeparate endpoints for /status, /scale, /exec, /log
LabelsSelectable 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-app
Group: 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?

The Resource name (lowercase, plural): 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?

The API server returns 409 Conflict because the 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?

RBAC separation. It allows different permissions for spec vs status updates. Controllers get 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?

Yes, Service selectors ARE mutable (unlike Deployment selectors which are immutable). You can update a Service's selector to point to different Pods. But be cautious — this will immediately shift traffic to a different set of Pods.

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.