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.

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:

FlagOutput
-o jsonJSON-formatted API object, e.g. kubectl create namespace test-123 --dry-run -o json
-o yamlYAML-formatted API object, e.g. kubectl create namespace test-123 --dry-run -o yaml
-o widePlain text plus additional columns, e.g. kubectl get pods -o wide
-o nameJust the resource name, nothing else

Imperative vs Declarative commands

Every command below falls into one of these two styles:

ImperativeDeclarative
How it worksDirect commands - run, create, replace, delete, editDescribe the desired state in YAML; kubectl apply works out the diff and patches it in
Source of truthWhatever’s currently live in the clusterThe YAML file
Best forQuick one-off changes, debugging, explorationAnything 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[].image
  • spec.activeDeadlineSeconds
  • spec.tolerations
  • spec.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
A typo’d or unreachable image reference surfaces as a Pod stuck in 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

  1. kubectl run creates 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.
  2. 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.
  3. KodeKloud’s kubectl delete deployment examples is a good reference if delete behaviour across object types gets confusing, alongside the official kubectl delete reference.
  4. Next: Chapter 3 - Workload Controllers.