RESTful resource model, API groups and versions, kubectl verbs, output formats, dry-run, and kubectl explain.
Everything in Kubernetes is an API object. Pods, Services, Deployments, Secrets — they're all resources served by the API Server over HTTPS. kubectl is just a fancy HTTP client.
kubectl get pods → GET /api/v1/namespaces/default/pods
┌──────────────────────────────────┐
│ API URL Structure │
│ │
│ /api/v1 │ ← core group
│ /apis/apps/v1 │ ← named group
│ /apis/batch/v1 │ ← named group
│ /apis/networking.k8s.io/v1 │ ← named group
│ │
│ /api/v1/namespaces/default/pods │ ← specific resource
│ /apis/apps/v1/deployments │ ← all deployments
└──────────────────────────────────┘
Resources are organized into API groups, each with a version:
| Group | apiVersion | Resources |
|---|---|---|
| Core (legacy) | v1 |
Pod, Service, ConfigMap, Secret, Namespace, Node |
| apps | apps/v1 |
Deployment, ReplicaSet, StatefulSet, DaemonSet |
| batch | batch/v1 |
Job, CronJob |
| networking.k8s.io | networking.k8s.io/v1 |
Ingress, NetworkPolicy |
| rbac.authorization.k8s.io | rbac.authorization.k8s.io/v1 |
Role, ClusterRole, RoleBinding |
The apiVersion field in your YAML tells the API Server which group and version to use:
apiVersion: apps/v1 # group "apps", version "v1"
kind: Deployment # resource type within that group
Why does this matter? If you use the wrong apiVersion, the API Server rejects the manifest.
kubectl explain <resource>tells you the correct one. API versions also evolve —extensions/v1beta1for Ingress was replaced bynetworking.k8s.io/v1.
Every Kubernetes object follows the same four-part structure:
apiVersion: apps/v1 # 1. API group + version
kind: Deployment # 2. Resource type
metadata: # 3. Identity — name, namespace, labels, annotations
name: web
namespace: default
labels:
app: web
spec: # 4. Desired state — what you want
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:1.25
The API Server adds a fifth part — status — which you don't write. It's the actual state, updated by controllers:
kubectl get deployment web -o yaml | grep -A 5 "^status:"
# status:
# availableReplicas: 3
# readyReplicas: 3
# replicas: 3
# updatedReplicas: 3
kubectl is the CLI for talking to the API Server. It reads your kubeconfig (~/.kube/config), authenticates, and sends HTTP requests.
# CREATE — make a new resource
kubectl create deployment web --image=nginx
kubectl apply -f manifest.yaml # create or update (preferred)
# READ — view resources
kubectl get pods # list
kubectl get pod nginx -o yaml # full YAML
kubectl describe pod nginx # human-readable detail
# UPDATE — modify resources
kubectl apply -f manifest.yaml # declarative update
kubectl edit deployment web # opens in $EDITOR
kubectl patch deployment web -p '{"spec":{"replicas":5}}'
kubectl scale deployment web --replicas=5
kubectl set image deployment/web nginx=nginx:1.26
# DELETE — remove resources
kubectl delete pod nginx
kubectl delete -f manifest.yaml
kubectl delete deployment --all # all deployments in namespace
kubectl get pods
# NAME READY STATUS RESTARTS AGE
# web-7d4b8c6f5-abc12 1/1 Running 0 5m
# web-7d4b8c6f5-def34 1/1 Running 0 5m
kubectl get pods,svc,deploy # multiple resource types at once
kubectl get all # shorthand for common types
kubectl get pods -l app=web # exact match
kubectl get pods -l 'app in (web, api)' # set-based
kubectl get pods -l app=web,tier=frontend # multiple labels (AND)
kubectl get pods -l '!canary' # label doesn't exist
kubectl get pods -n kube-system # specific namespace
kubectl get pods -A # all namespaces
kubectl get pods --all-namespaces # same as -A
kubectl supports several output formats. Mastering these is essential:
kubectl get pods -o wide
# NAME READY STATUS RESTARTS AGE IP NODE
# web-7d4b8c6f5-abc12 1/1 Running 0 5m 10.1.0.15 node-1
kubectl get pod nginx -o yaml # full YAML (including status)
kubectl get pod nginx -o json # full JSON
# Get just the Pod IP
kubectl get pod nginx -o jsonpath='{.status.podIP}'
# 10.1.0.15
# Get all Pod names
kubectl get pods -o jsonpath='{.items[*].metadata.name}'
# web-abc12 web-def34 web-ghi56
# Get Pod name and IP as a table
kubectl get pods -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.podIP}{"\n"}{end}'
# web-abc12 10.1.0.15
# web-def34 10.1.0.16
# web-ghi56 10.1.0.17
kubectl get pods -o custom-columns=\
NAME:.metadata.name,\
STATUS:.status.phase,\
IP:.status.podIP,\
NODE:.spec.nodeName
# NAME STATUS IP NODE
# web-7d4b8c6f5-abc12 Running 10.1.0.15 node-1
# web-7d4b8c6f5-def34 Running 10.1.0.16 node-1
Tip:
-o jsonpathand-o custom-columnsare CKA/CKAD exam essentials. Practice them until they're muscle memory. They let you script around kubectl output withoutgrepandawk.
describe gives you everything about a resource in a readable format, including events (the most useful part for debugging):
kubectl describe pod web-7d4b8c6f5-abc12
# Name: web-7d4b8c6f5-abc12
# Namespace: default
# Priority: 0
# Service Account: default
# Node: docker-desktop/192.168.65.3
# Start Time: Fri, 31 Jan 2025 10:00:00 +0000
# Labels: app=web
# pod-template-hash=7d4b8c6f5
# Status: Running
# IP: 10.1.0.15
# Controlled By: ReplicaSet/web-7d4b8c6f5
# Containers:
# nginx:
# Image: nginx:1.25
# Port: <none>
# State: Running
# Started: Fri, 31 Jan 2025 10:00:02 +0000
# Ready: True
# Restart Count: 0
# Events:
# Type Reason Age From Message
# ---- ------ ---- ---- -------
# Normal Scheduled 5m default-scheduler Successfully assigned ...
# Normal Pulling 5m kubelet Pulling image "nginx:1.25"
# Normal Pulled 5m kubelet Successfully pulled image
# Normal Created 5m kubelet Created container nginx
# Normal Started 5m kubelet Started container nginx
The Events section at the bottom is gold. When something goes wrong, this is where you look first.
explain is kubectl's built-in reference. It shows the schema for any resource or field:
# Top-level fields for a Pod
kubectl explain pod
# KIND: Pod
# VERSION: v1
# DESCRIPTION:
# Pod is a collection of containers that can run on a host.
# FIELDS:
# apiVersion <string>
# kind <string>
# metadata <ObjectMeta>
# spec <PodSpec>
# status <PodStatus>
# Drill into a specific field
kubectl explain pod.spec.containers
# KIND: Pod
# VERSION: v1
# FIELD: containers <[]Container>
# DESCRIPTION:
# List of containers belonging to the pod.
# FIELDS:
# name <string> -required-
# image <string>
# command <[]string>
# args <[]string>
# ports <[]ContainerPort>
# env <[]EnvVar>
# resources <ResourceRequirements>
# ...
# See the full tree (recursive)
kubectl explain pod.spec --recursive | head -40
Tip:
kubectl explainis available during CKA/CKAD exams and doesn't require internet. Use it instead of googling.kubectl explain deployment.spec.strategytells you exactly what fields are available and what they mean.
Before applying manifests to a live cluster, validate them:
Validates the YAML locally without sending anything to the API Server. Useful for generating YAML templates:
# Generate a Deployment YAML without creating it
kubectl create deployment web --image=nginx --replicas=3 \
--dry-run=client -o yaml
# apiVersion: apps/v1
# kind: Deployment
# metadata:
# labels:
# app: web
# name: web
# spec:
# replicas: 3
# selector:
# matchLabels:
# app: web
# template:
# metadata:
# labels:
# app: web
# spec:
# containers:
# - image: nginx
# name: nginx
# Save it to a file
kubectl create deployment web --image=nginx --replicas=3 \
--dry-run=client -o yaml > deployment.yaml
Sends the request to the API Server for full validation (schema checks, admission controllers) but doesn't persist:
kubectl apply -f deployment.yaml --dry-run=server
# deployment.apps/web created (server dry run)
Shows what would change if you applied a manifest:
kubectl diff -f deployment.yaml
# diff -u -N /tmp/LIVE-123 /tmp/MERGED-456
# --- /tmp/LIVE-123
# +++ /tmp/MERGED-456
# @@ -6,7 +6,7 @@
# spec:
# - replicas: 3
# + replicas: 5
Workflow: Generate with
--dry-run=client -o yaml, edit the YAML, validate with--dry-run=server, check withdiff, thenapply. This is the safest workflow.
# Sort by creation time (newest last)
kubectl get pods --sort-by=.metadata.creationTimestamp
# Sort by restart count
kubectl get pods --sort-by=.status.containerStatuses[0].restartCount
# Sort events by time
kubectl get events --sort-by=.lastTimestamp
Filter by object fields (not labels):
# Pods on a specific node
kubectl get pods --field-selector spec.nodeName=node-1
# Pods not in Running phase
kubectl get pods --field-selector status.phase!=Running
# Combine field selectors
kubectl get pods --field-selector status.phase=Running,spec.nodeName=node-1
# All resources in a specific namespace (field selector)
kubectl get pods --field-selector metadata.namespace=kube-system
Two philosophies for managing resources:
kubectl create deployment web --image=nginx
kubectl scale deployment web --replicas=3
kubectl set image deployment/web nginx=nginx:1.26
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
spec:
replicas: 3
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:1.26
kubectl apply -f deployment.yaml
| Aspect | Imperative | Declarative |
|---|---|---|
| How | Run commands | Write YAML, apply |
| History | No record of changes | YAML in git = audit trail |
| Reproducible | No | Yes — same YAML → same result |
| Automation | Hard | Natural — CI/CD applies YAML |
| Use for | Quick testing, debugging | Everything else |
Rule: Use imperative commands for quick ad-hoc tasks and
--dry-run=client -o yamlto generate starting YAML. Use declarativeapplyfor anything you want to keep, version, or automate.
Progress through each section in order, or jump to where you need practice.
Practice individual concepts you just learned.
Combine concepts and learn patterns. Each challenge has multiple variants at different difficulties.
kubectl get pods = GET /api/v1/namespaces/default/podsapiVersion, kind, metadata, spec (desired state), and status (actual state)-l (labels), -o for output format (wide, yaml, json, jsonpath)-o jsonpath='{.status.podIP}')--dry-run=client for local validation, --dry-run=server for full API validationapply -f) for everything real--dry-run=client -o yaml instead of writing from scratch