kubectl is your primary interface to Kubernetes. In exams, speed with kubectl is the difference between finishing or not. In production, it's your debugging swiss-army knife. This lesson covers the techniques that make you fast and precise.
1. Imperative vs Declarative — Two Worlds
kubectl supports both approaches. Know when to use each:
Imperative Commands (do it now)
# Create resources directly: kubectl run nginx --image=nginx kubectl create deployment web --image=nginx --replicas=3 kubectl expose deployment web --port=80 --type=ClusterIP kubectl scale deployment web --replicas=5 # Modify in place: kubectl set image deployment/web nginx=nginx:1.25 kubectl label pod nginx-abc app=web kubectl annotate deployment web owner=team-platform kubectl taint node worker-1 dedicated=gpu:NoSchedule
Declarative Management (apply the desired state)
# Apply a manifest (create or update): kubectl apply -f deployment.yaml kubectl apply -f ./manifests/ # whole directory kubectl apply -k ./overlays/prod/ # kustomize # Delete what's declared: kubectl delete -f deployment.yaml
When to Use Each
| Approach | Best For | Drawbacks |
|---|---|---|
| Imperative | CKA/CKAD exams (speed), quick debugging, one-off tasks | Not reproducible, no Git history, drift |
| Declarative | Production, GitOps, team workflows | Slower for quick experiments |
kubectl create deployment web --image=nginx --dry-run=client -o yaml > dep.yaml → edit → kubectl apply -f dep.yaml.
create vs apply
kubectl create | kubectl apply | |
|---|---|---|
| If exists | ❌ Error | ✅ Updates it |
| If not exists | ✅ Creates | ✅ Creates |
| Tracks changes | No | Yes (via last-applied-configuration annotation) |
| Merge strategy | N/A (full replacement) | Strategic merge patch |
apply uses three-way merge: It compares (1) the new manifest, (2) the last-applied-configuration annotation, and (3) the live object. This lets it detect fields that were removed from your manifest and delete them from the live object — something a simple patch can't do.
2. Contexts & kubeconfig
kubectl needs to know: which cluster, which user, and which namespace. This is configured in ~/.kube/config (or via $KUBECONFIG).
kubeconfig Structure
apiVersion: v1
kind: Config
clusters: # ← WHERE to connect
- name: production
cluster:
server: https://k8s.prod.company.com:6443
certificate-authority-data: LS0t...
- name: staging
cluster:
server: https://k8s.stg.company.com:6443
users: # ← WHO you are
- name: admin
user:
client-certificate-data: LS0t...
client-key-data: LS0t...
- name: developer
user:
token: eyJhbG...
contexts: # ← Combines cluster + user + namespace
- name: prod-admin
context:
cluster: production
user: admin
namespace: default
- name: stg-dev
context:
cluster: staging
user: developer
namespace: app-team
current-context: prod-admin # ← Active context
Context Commands
# See all contexts: kubectl config get-contexts # Switch context: kubectl config use-context stg-dev # Set default namespace for current context: kubectl config set-context --current --namespace=kube-system # View current context: kubectl config current-context # Quick temporary override (doesn't change config): kubectl get pods --context=prod-admin -n monitoring
kubectl config use-context <name> at the start of each question. Forgetting this = answering against the wrong cluster.
kubectx and kubens for fast switching. In production, protect yourself: set PS1 to show current context/namespace in your shell prompt. Accidentally running kubectl delete against prod instead of staging is a real disaster pattern.
3. Output Formats — Getting the Data You Need
The -o Flag
| Format | Use Case | Example |
|---|---|---|
-o wide | More columns (node, IP) | kubectl get pods -o wide |
-o yaml | Full object as YAML | kubectl get deploy nginx -o yaml |
-o json | Full object as JSON | kubectl get pod nginx -o json |
-o name | Just resource names | kubectl get pods -o name → pod/nginx |
-o jsonpath | Extract specific fields | See below |
-o custom-columns | Custom table format | See below |
JSONPath — Surgical Data Extraction
# Get the IP of a Pod:
kubectl get pod nginx -o jsonpath='{.status.podIP}'
# Get all node names:
kubectl get nodes -o jsonpath='{.items[*].metadata.name}'
# Get image of first container:
kubectl get pod nginx -o jsonpath='{.spec.containers[0].image}'
# Multiple fields with formatting:
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.status.addresses[0].address}{"\n"}{end}'
# Conditional (nodes that are Ready):
kubectl get nodes -o jsonpath='{.items[?(@.status.conditions[?(@.type=="Ready")].status=="True")].metadata.name}'
Custom Columns
# Readable table with chosen fields: kubectl get pods -o custom-columns=\ NAME:.metadata.name,\ NODE:.spec.nodeName,\ STATUS:.status.phase,\ IP:.status.podIP # Output: # NAME NODE STATUS IP # nginx-abc worker-1 Running 10.244.1.5 # redis-xyz worker-2 Running 10.244.2.3
{.items[*].status.addresses[?(@.type=="InternalIP")].address}. Tip: pipe to tr ' ' '\n' to get one per line.
4. dry-run, diff, and explain
--dry-run: Generate Without Applying
Two modes:
| Flag | What It Does | Use Case |
|---|---|---|
--dry-run=client | Generates YAML locally, no server contact | Scaffold manifests fast (exam workflow) |
--dry-run=server | Sends to API server for full validation, but doesn't persist | Validate that admission webhooks and RBAC allow it |
# The exam power move — generate YAML skeleton: kubectl create deployment web --image=nginx --replicas=3 \ --dry-run=client -o yaml > deploy.yaml # Generate a Service: kubectl expose deployment web --port=80 --target-port=8080 \ --dry-run=client -o yaml > svc.yaml # Generate a Pod with resource requests: kubectl run nginx --image=nginx \ --dry-run=client -o yaml -- /bin/sh -c "sleep 3600" > pod.yaml # Validate against server (catches invalid fields, denied by webhooks): kubectl apply -f deploy.yaml --dry-run=server
kubectl create ... --dry-run=client -o yaml > file.yaml. Edit the file to add what you need, then kubectl apply -f file.yaml. Never write YAML from scratch in an exam.
kubectl diff: Preview Changes
# See what would change before applying: kubectl diff -f deploy.yaml # Output looks like unified diff: # - replicas: 3 # + replicas: 5
This is invaluable in production — review changes before they go live, especially for GitOps workflows.
kubectl explain: Built-in Documentation
# What fields does a Deployment spec have? kubectl explain deployment.spec # Go deeper: kubectl explain deployment.spec.strategy kubectl explain deployment.spec.strategy.rollingUpdate # See the full tree: kubectl explain pod.spec --recursive | grep -i volume # Check what apiVersion to use: kubectl explain deployment | head -5 # KIND: Deployment # VERSION: apps/v1
kubectl explain is available during exams and is faster than searching docs. Use it whenever you can't remember a field name: kubectl explain pod.spec.containers.livenessProbe.
5. Essential Commands Reference
Inspection
# Describe — human-readable details + events: kubectl describe pod nginx kubectl describe node worker-1 # Logs: kubectl logs nginx # current logs kubectl logs nginx -c sidecar # specific container kubectl logs nginx --previous # previous (crashed) container kubectl logs -l app=web --all-containers # all pods matching label kubectl logs nginx -f # stream (follow) kubectl logs nginx --since=5m # last 5 minutes kubectl logs nginx --tail=100 # last 100 lines # Events (cluster-wide, sorted by time): kubectl get events --sort-by='.lastTimestamp' kubectl get events -n kube-system --field-selector reason=FailedScheduling
Editing Live Objects
# Open in $EDITOR, saves on close:
kubectl edit deployment web
# Patch (strategic merge):
kubectl patch deployment web -p '{"spec":{"replicas":5}}'
# JSON patch (more precise):
kubectl patch deployment web --type=json \
-p '[{"op":"replace","path":"/spec/replicas","value":5}]'
# Replace (full object overwrite, needs resourceVersion):
kubectl replace -f deploy.yaml
# Force replace (delete + create — causes downtime!):
kubectl replace -f deploy.yaml --force
Deletion
# Delete by name: kubectl delete pod nginx # Delete by file: kubectl delete -f deploy.yaml # Delete by label: kubectl delete pods -l app=test # Force immediate deletion (skip graceful period): kubectl delete pod nginx --grace-period=0 --force # Delete all pods in namespace: kubectl delete pods --all -n testing
Exec and Debug
# Execute command in a running container: kubectl exec nginx -- ls /etc/nginx kubectl exec -it nginx -- /bin/bash # interactive shell # Port forward (access a Pod locally): kubectl port-forward pod/nginx 8080:80 kubectl port-forward svc/web 8080:80 # Copy files: kubectl cp nginx:/etc/nginx/nginx.conf ./nginx.conf kubectl cp ./config.yaml nginx:/tmp/config.yaml # Debug with ephemeral container (K8s 1.23+): kubectl debug -it nginx --image=busybox --target=nginx # Debug a node: kubectl debug node/worker-1 -it --image=ubuntu
kubectl exec into production pods should be rare and audited. CKS topic: audit policies can log exec events, and RBAC should restrict pods/exec to break-glass roles only.
6. Speed Tips for Exams and Daily Use
Shell Setup
# Add to ~/.bashrc for exams: alias k=kubectl complete -o default -F __start_kubectl k # enable completion for alias # Shortcuts that save seconds per question: alias kgp='kubectl get pods' alias kgs='kubectl get svc' alias kgd='kubectl get deploy' alias kdn='kubectl describe node' alias kdp='kubectl describe pod' # Set default namespace: export ns="-n kube-system" kubectl get pods $ns
Imperative Generation Cheat Sheet
| Need | Command |
|---|---|
| Pod | k run nginx --image=nginx --dry-run=client -o yaml |
| Deployment | k create deploy web --image=nginx --replicas=3 --dry-run=client -o yaml |
| Service (ClusterIP) | k expose deploy web --port=80 --dry-run=client -o yaml |
| Service (NodePort) | k expose deploy web --port=80 --type=NodePort --dry-run=client -o yaml |
| Job | k create job pi --image=perl --dry-run=client -o yaml -- perl -Mbignum=bpi -wle 'print bpi(2000)' |
| CronJob | k create cronjob backup --image=busybox --schedule="0 2 * * *" --dry-run=client -o yaml -- /bin/sh -c "echo backup" |
| ConfigMap | k create configmap app-cfg --from-literal=KEY=value --dry-run=client -o yaml |
| Secret | k create secret generic db-creds --from-literal=pass=s3cr3t --dry-run=client -o yaml |
| ServiceAccount | k create sa my-sa --dry-run=client -o yaml |
| Role | k create role pod-reader --verb=get,list --resource=pods --dry-run=client -o yaml |
| RoleBinding | k create rolebinding read-pods --role=pod-reader --user=jane --dry-run=client -o yaml |
| Ingress | k create ingress web --rule="host.com/path=svc:80" --dry-run=client -o yaml |
Useful Filters
# Sort by restart count (find crashlooping pods): kubectl get pods --sort-by='.status.containerStatuses[0].restartCount' # Find pods on a specific node: kubectl get pods --field-selector spec.nodeName=worker-1 # Show pods not in Running state: kubectl get pods --field-selector status.phase!=Running # All resources in a namespace: kubectl get all -n my-namespace # Watch changes live: kubectl get pods -w
Verbosity for Debugging API Calls
# See the actual HTTP request kubectl makes: kubectl get pods -v=6 # shows URL kubectl get pods -v=8 # shows request/response headers kubectl get pods -v=9 # shows request/response bodies # Example output at -v=6: # GET https://k8s.example.com:6443/api/v1/namespaces/default/pods?limit=500 # 200 OK in 23ms
-v=6 is your secret weapon: It shows the exact API URL kubectl uses. When you're confused about which API path a resource uses, or why RBAC denies a request, -v=6 reveals the truth. Combine with Lesson 4 (API model) and everything clicks.
Summary: The kubectl Mental Model
| Task | Fastest Approach |
|---|---|
| Create something quickly | Imperative: kubectl create/run |
| Generate YAML scaffold | --dry-run=client -o yaml > file.yaml |
| Update existing resource | kubectl apply -f (declarative) or kubectl edit (quick fix) |
| Validate before applying | kubectl diff -f then kubectl apply --dry-run=server |
| Debug/inspect | describe, logs, events, -v=6 |
| Look up field syntax | kubectl explain resource.field |
| Extract specific data | -o jsonpath='{...}' |
📝 Quiz: kubectl Fluency
Q1: What's the fastest way to generate a Deployment YAML without actually creating it on the cluster?
kubectl create deployment web --image=nginx --dry-run=client -o yaml > deploy.yamlThis generates the YAML locally without touching the API server.
Q2: What's the difference between --dry-run=client and --dry-run=server?
server: Sends to the API server which runs full admission (webhooks, RBAC, validation) but doesn't persist. Use server-side to verify admission policies will accept your manifest.
Q3: You applied a manifest that added a label. Now you remove that label from the YAML and apply again. Does the label get removed from the live object?
kubectl apply). apply uses three-way merge: it compares the new manifest, the last-applied-configuration annotation, and the live object. It detects that the label was in last-applied but is now gone, so it removes it. This wouldn't work with kubectl patch (which doesn't track removed fields).Q4: Write the jsonpath expression to get the container image of the first container in pod "web".
kubectl get pod web -o jsonpath='{.spec.containers[0].image}'Q5: You want to see what HTTP request kubectl makes when you run kubectl get pods. What flag do you add?
-v=6 (or higher). This shows the API URL: GET https://...server.../api/v1/namespaces/default/pods. Use -v=8 for full headers, -v=9 for request/response bodies.Q6: You're in a CKA exam. The question says "use context k8s-prod." What's the first command you run?
kubectl config use-context k8s-prodAlways do this first. Operating against the wrong cluster is an instant score of zero for that question.