This is Chapter 2 of the Kubernetes Fundamentals series.
Installing kubectl
Install kubectl before a local cluster tool like minikube - minikube depends on it being present.
- kubectl reference
- kubectl Quick Reference
- Install kubectl - official install steps per OS
Confirm kubectl can reach the cluster:
kubectl cluster-info
List the nodes in it:
kubectl get nodes
Check a node’s details - capacity, conditions, and what’s running on it:
kubectl describe nodes
Output formatting
kubectl [command] [TYPE] [NAME] -o <format> controls how a command’s output is rendered:
| Flag | Output |
|---|---|
-o json | JSON-formatted API object, e.g. kubectl create namespace test-123 --dry-run -o json |
-o yaml | YAML-formatted API object, e.g. kubectl create namespace test-123 --dry-run -o yaml |
-o wide | Plain text plus additional columns, e.g. kubectl get pods -o wide |
-o name | Just the resource name, nothing else |
Imperative vs Declarative commands
Every command below falls into one of these two styles:
| Imperative | Declarative | |
|---|---|---|
| How it works | Direct commands - run, create, replace, delete, edit | Describe the desired state in YAML; kubectl apply works out the diff and patches it in |
| Source of truth | Whatever’s currently live in the cluster | The YAML file |
| Best for | Quick one-off changes, debugging, exploration | Anything meant to be repeatable, reviewed, or version-controlled |
The imperative commands below are the more direct, ad-hoc way to work with a Pod. The declarative commands further down describe what should exist and let Kubernetes reconcile the difference - the pattern every later chapter in this series builds on.
Imperative commands
Create
Create from a YAML file - the Pod definition from Chapter 1:
kubectl create -f pod-definition.yml
Create a Pod directly from an image - skip this if myapp-pod from Chapter 1 is already running:
kubectl run --image=nginx nginx
Namespacing a Pod at creation time:
kubectl run nginx-pod --image=nginx --namespace=dev
The image name matters - it has to exist under that name on Docker Hub (or whichever registry is configured).
--dry-run=client -o yaml generates a Pod’s YAML without creating anything - useful for previewing or scaffolding a definition before it exists:
kubectl run nginx --image=nginx --dry-run=client -o yaml | tee nginx.yaml
Manage
List the Pods in the current namespace:
kubectl get pods
-o wide adds the node and internal IP columns:
kubectl get pods -o wide
Check a Pod’s events - useful for diagnosing why it isn’t starting:
kubectl describe pod <pod_name>
Check a Pod’s stdout:
kubectl logs <pod_name>
-c picks a specific container’s logs, needed only when a Pod has more than one:
kubectl logs <pod_name> -c <container_name>
Check cluster-wide events, not just one Pod’s:
kubectl get events
Delete a Pod:
kubectl delete pod <pod_name>
Check the current kubectl configuration:
kubectl config view
Update an existing Pod’s spec from a YAML file:
kubectl replace -f pod-definition.yml
--force deletes and recreates it instead, for changes replace alone can’t apply live:
kubectl replace --force -f pod-definition.yml
Delete from the same file:
kubectl delete -f pod-definition.yml
Extract an existing Pod’s definition to a file:
kubectl get pod <pod_name> -o yaml > pod-definition.yaml
Edit it live instead - only a few fields are mutable on a running Pod:
kubectl edit pod <pod_name>
spec.containers[].image,spec.initContainers[].imagespec.activeDeadlineSecondsspec.tolerationsspec.terminationGracePeriodSeconds
kubectl explain looks up what any field in a Pod’s YAML actually does, straight from the API’s own docs:
kubectl explain pod.spec.restartPolicy
Pod networking
Create a Pod and confirm it’s running:
kubectl run nginx --image=nginx
kubectl get pods
Check its logs:
kubectl logs pod/nginx
-o wide adds the Pod’s IP - the one piece of information everything below needs:
kubectl get pods -o wide
Capture that IP into a variable:
NGINX_IP=$(kubectl get pods -o wide | awk '/nginx/ { print $6 }'); echo $NGINX_IP
A Pod’s IP is reachable directly on the cluster network, with no Service required:
ping -c 3 $NGINX_IP
port-forward tunnels a local port straight to the Pod - useful for a quick check without exposing anything:
kubectl port-forward pod/nginx 8080:80 # http://localhost:8080, ctrl-c to stop
A throwaway Pod can reach another Pod’s IP directly too - --rm deletes it the moment it exits:
kubectl run -it --rm curl --image=curlimages/curl --restart=Never -- http://$NGINX_IP
The same test works from a longer-lived Pod instead of a one-shot one - pass the IP in as an environment variable, then shell into it:
kubectl run ubuntu --image=ubuntu --env="NGINX_IP=$NGINX_IP" -- sleep infinity
kubectl exec -it ubuntu -- bash
Inside that shell, sleep infinity is PID 1, and the shell plus every command run in it are subprocesses of it:
ps -ef
The ubuntu base image doesn’t ship curl - install it, then reuse the variable passed in earlier:
apt update && apt install -y curl
curl $NGINX_IP
exit
Clean up both Pods:
kubectl delete pod/nginx pod/ubuntu --now
Working example with minikube
Run a small HTTP test server as a bare Pod:
kubectl run hello-node --image=registry.k8s.io/e2e-test-images/agnhost:2.39 -- /agnhost netexec --http-port=8080
Confirm it’s running:
kubectl get pods
A Deployment (covered in full in Chapter 3) is the more common way to run this in practice - it checks the Pod’s health and restarts the container if it terminates, rather than leaving a bare Pod to fail silently. Same image, as a Deployment instead:
kubectl create deployment hello-node --image=registry.k8s.io/e2e-test-images/agnhost:2.39 -- /agnhost netexec --http-port=8080
Confirm it’s running:
kubectl get deployments
ImagePullBackOff (image exists but can’t be pulled - registry auth, network, rate limit) or ErrImagePull (image reference itself is wrong). kubectl describe pod <pod_name> shows which one and why, in the Events section at the bottom.Draining a node for maintenance
Stop new Pods being scheduled here, without touching what’s already running:
kubectl cordon <node_name>
Evict the existing Pods - --ignore-daemonsets is required because DaemonSet Pods (covered in Chapter 5) are designed to run on every node and drain won’t evict them by default; the flag tells it to proceed anyway rather than blocking on them:
kubectl drain --ignore-daemonsets --delete-emptydir-data <node_name>
Also evict Pods with no controller managing them, which drain otherwise leaves alone:
kubectl drain --ignore-daemonsets --delete-emptydir-data --force <node_name>
Resume scheduling once maintenance is done:
kubectl uncordon <node_name>
Declarative commands
kubectl apply works differently from create/replace - it compares the local config file against the last-applied configuration to figure out what changed, then patches just that diff into the live object. Run it against a file that doesn’t exist yet and it creates the object:
kubectl apply -f pod-definition.yml
Run the exact same command again and it updates instead - only what changed gets applied:
kubectl apply -f pod-definition.yml
Point it at a directory instead of a single file to apply every manifest in it at once:
kubectl apply -f /path/to/manifest-config-files/
This is the pattern every later chapter in this series uses for ReplicaSets, Deployments, Namespaces, ResourceQuotas, and Services - kubectl apply -f <file>.yml, same comparison logic, just a different object underneath.
The difference between create and apply is concrete, not just theoretical. Create the object:
kubectl create -f nginx.yml
Run the exact same command again and it fails the second time, since the object already exists:
kubectl create -f nginx.yml
apply against the same file succeeds the first time too, the same as create:
kubectl apply -f nginx.yml
Run it again and it succeeds again - it’s comparing state rather than insisting the object is new, so nothing changed means nothing is patched (the first run prints a one-off warning about missing prior apply metadata):
kubectl apply -f nginx.yml
A single YAML file can hold more than one object, separated by --- - useful for keeping related Pods together instead of juggling separate files. Combine two existing manifests into one:
{ cat nginx.yml; echo "---"; cat ubuntu.yml; } | tee combined.yml
Apply it - this creates both objects in one call:
kubectl apply -f combined.yml
Delete it - this removes both the same way:
kubectl delete -f combined.yml --now
The sidecar pattern
A Pod can run more than one container, sharing network and storage - the most common reason is a sidecar that supports the main container without being part of the application itself. Extend myapp-pod from Chapter 1 with a second container that logs on a loop and exits if a specific file appears:
apiVersion: v1
kind: Pod
metadata:
name: myapp-pod
labels:
app: myapp
type: frontend
env: production
spec:
containers:
- name: nginx-container
image: nginx
ports:
- containerPort: 8080
- name: sidecar-container
image: ubuntu
args:
- /bin/sh
- -c
- while true; do echo "$(date +'%T') - hello from the sidecar"; sleep 5; if [ -f /tmp/crash ]; then exit 1; fi; done
Apply it:
kubectl apply -f myapp-pod.yml
2/2 in the Pod list means both containers are running:
kubectl get pods -o wide
describe shows both containers by name, plus the Pod’s IP:
kubectl describe pod/myapp-pod
Capture the Pod’s IP into a variable:
MYAPP_IP=$(kubectl get pods -o wide | awk '/myapp-pod/ { print $6 }'); echo $MYAPP_IP
Confirm it’s reachable, the same as any other Pod:
kubectl run -it --rm --image=curlimages/curl:8.4.0 --restart=Never curl -- http://$MYAPP_IP
-c picks a specific container’s logs when a Pod has more than one:
kubectl logs pod/myapp-pod -c sidecar-container
Trigger the sidecar’s exit condition by creating the file it’s watching for:
kubectl exec -it myapp-pod -c sidecar-container -- touch /tmp/crash
The Pod stays at 2/2, but the restart count on the sidecar container increments - Kubernetes restarted just that one container, not the whole Pod:
kubectl get pods -o wide
-p shows the previous container’s logs, from before the restart:
kubectl logs pod/myapp-pod -p -c sidecar-container
Clean up:
kubectl delete pod/myapp-pod --now
Notes
kubectl runcreates a bare Pod - useful for quick checks, but Chapter 3 covers why a Deployment is almost always the right object for anything meant to stay running.kubectl apply’s diff-based update is what every other chapter in this series relies on - it’s covered once here rather than re-explained per object.- KodeKloud’s kubectl delete deployment examples is a good reference if
deletebehaviour across object types gets confusing, alongside the official kubectl delete reference. - Next: Chapter 3 - Workload Controllers.