🧪 Ephemeral Containers — Debug Distroless Pods

Modern images are often distroless or scratch-based — they contain only the application binary, no shell, no curl, no netcat. You can't kubectl exec into a shell that doesn't exist. Ephemeral containers (GA since K8s 1.25) solve this by injecting a temporary debug container into a running pod — sharing its namespaces.

Pod: my-app-xyz App Container distroless — no shell PID ns · Net ns · IPC ns shared with ephemeral Ephemeral Container busybox / nicolaka/netshoot sees app processes via /proc kubectl debug injects this →

Basic ephemeral container usage

# Inject a busybox shell into a running pod
kubectl debug -it my-app-xyz \
  --image=busybox:1.36 \
  --target=app \           # share process namespace of the 'app' container
  -- sh

# Use nicolaka/netshoot for networking tools (curl, dig, tcpdump, ss)
kubectl debug -it my-app-xyz \
  --image=nicolaka/netshoot \
  --target=app \
  -- bash

# Inspect the app's process list (requires --target + shareProcessNamespace)
/ # ps aux
PID   USER   COMMAND
1     root   /app/server          ← the distroless app process
47    root    sh                  ← our ephemeral container

# Read the app's open files
/ # ls -la /proc/1/fd

# Inspect environment variables of the app process
/ # cat /proc/1/environ | tr '\0' '\n'
💡 Ephemeral containers are immutable once created You can't remove an ephemeral container from a pod — it stays until the pod is deleted. It also doesn't get resource requests/limits from the pod spec, so set --profile=restricted in secure clusters to avoid privilege escalation. The ephemeral container shares the pod's network namespace by default, and optionally the process namespace via --target.

Copy-and-modify pattern — debug a crashed pod

If a pod is in CrashLoopBackOff it restarts too fast to exec into. kubectl debug can copy the pod with a modified command or added debug container:

# Copy the crashing pod, replace its command with a sleep to keep it alive
kubectl debug my-app-xyz \
  --copy-to=my-app-debug \
  --image=my-app:v1 \
  --set-image=app=my-app:v1 \
  -- sleep infinity

# Now exec into the copy and investigate
kubectl exec -it my-app-debug -- sh

# Copy with an added ephemeral sidecar (keep original command running)
kubectl debug my-app-xyz \
  --copy-to=my-app-debug \
  --image=busybox \
  -it -- sh

🖊️ kubectl exec — Run Commands in Containers

kubectl exec runs a command inside a running container. It requires the container to have the command binary available — which is why ephemeral containers are needed for distroless images.

# Interactive shell
kubectl exec -it my-app-xyz -- bash
kubectl exec -it my-app-xyz -- sh   # if bash not available

# Run a single command (non-interactive)
kubectl exec my-app-xyz -- env | sort
kubectl exec my-app-xyz -- cat /etc/resolv.conf
kubectl exec my-app-xyz -- wget -qO- http://other-service/healthz

# Target a specific container in a multi-container pod
kubectl exec -it my-app-xyz -c sidecar -- sh

# Test DNS resolution from inside the cluster
kubectl exec -it my-app-xyz -- nslookup kubernetes.default.svc.cluster.local

# Test connectivity to another service
kubectl exec -it my-app-xyz -- nc -zv payments-svc 8080
kubectl exec -it my-app-xyz -- curl -v http://payments-svc:8080/healthz

# Check file permissions and ownership
kubectl exec my-app-xyz -- ls -la /app/config/

📜 kubectl logs — Advanced Patterns

# Current logs, last 100 lines
kubectl logs --tail=100 my-app-xyz

# Previous container instance (after restart)
kubectl logs --previous my-app-xyz

# All containers in a pod (prefix shows which container)
kubectl logs my-app-xyz --all-containers --prefix

# Stream logs from ALL pods matching a selector
kubectl logs -l app=my-app -f --all-containers --prefix --max-log-requests=10

# Since a specific time
kubectl logs my-app-xyz --since-time="2024-01-15T10:30:00Z"
kubectl logs my-app-xyz --since=1h

# Pipe through grep for fast filtering
kubectl logs -l app=my-app --tail=500 | grep -i error
kubectl logs -l app=my-app --tail=500 | grep -v '/healthz'  # remove noise

# Combine with jq for structured JSON logs
kubectl logs my-app-xyz --tail=200 | jq 'select(.level=="error")'
kubectl logs my-app-xyz --tail=200 | jq '{ts: .timestamp, msg: .message, err: .error}'

📋 Quick Reference — Debug Decision Tree

SymptomFirst commandNext step
Pod stuck in Pending kubectl describe pod <name> Check Events section — Insufficient CPU/memory? No matching node? PVC not bound?
CrashLoopBackOff kubectl logs --previous <pod> Startup error in logs? If no logs: kubectl debug --copy-to with sleep infinity
Pod Running but 0/1 Ready kubectl describe pod <name> Readiness probe failing — check Unhealthy events, test endpoint manually with exec
ImagePullBackOff kubectl describe pod <name> Wrong image name? Missing imagePullSecret? Registry unreachable? Check node network
Cannot connect to service kubectl exec -it <pod> -- nslookup <svc> DNS works? Try IP directly. Check NetworkPolicy, Endpoints: kubectl get endpoints <svc>
Node NotReady kubectl describe node <name> kubectl debug node/<name> → chroot /host → journalctl -u kubelet
OOMKilled container kubectl describe pod <name> — check lastState reason Increase memory limit or fix memory leak. Check: kubectl top pod <name>

Useful one-liners for production incidents

# Find all pods NOT in Running or Completed state
kubectl get pods -A --field-selector='status.phase!=Running,status.phase!=Succeeded'

# Find pods with high restart counts
kubectl get pods -A --sort-by='.status.containerStatuses[0].restartCount' | tail -20

# Get all events sorted by time (cluster-wide)
kubectl get events -A --sort-by='.lastTimestamp' | tail -30

# Describe every pod in a namespace (batch review)
kubectl describe pods -n production | grep -E 'Name:|State:|Reason:|Message:'

# Watch pod status in real time
kubectl get pods -n production -w

# Copy a file out of a container
kubectl cp my-app-xyz:/app/config/settings.json ./settings-debug.json

# Copy a file INTO a container
kubectl cp ./debug-config.json my-app-xyz:/tmp/debug-config.json

🖥️ kubectl debug node — Node-Level Inspection

Sometimes the problem is at the node level — a failing kubelet, full disk, networking issue, or kernel problem. kubectl debug node creates a privileged pod on the node and mounts the host filesystem, giving you root access to the node without SSH.

# Open a debug shell on node "worker-1"
kubectl debug node/worker-1 \
  --image=nicolaka/netshoot \
  -it -- bash

# Inside the debug pod — host filesystem is at /host
root@node-debugger:/# chroot /host   # enter the host OS

# Check node-level logs (kubelet, containerd)
root@worker-1:/# journalctl -u kubelet -f --no-pager
root@worker-1:/# journalctl -u containerd --since "5 minutes ago"

# Check disk usage (full disk kills the kubelet)
root@worker-1:/# df -h
root@worker-1:/# du -sh /var/lib/containerd/*

# List all running containers on the node
root@worker-1:/# crictl ps

# Inspect a specific container's logs via crictl
root@worker-1:/# crictl logs <container-id>

# Check network interfaces and routing table
root@worker-1:/# ip route
root@worker-1:/# ip addr show
root@worker-1:/# iptables -t nat -L -n | grep KUBE
🔴 Node debug pods are highly privileged kubectl debug node creates a pod with hostPID: true, hostNetwork: true, and a hostPath volume mounting /. It requires cluster-admin or equivalent. The debug pod persists until you delete it — always clean up: kubectl delete pod node-debugger-worker-1-xxxxx.

Inspecting node resource pressure

# Check node conditions (MemoryPressure, DiskPressure, PIDPressure)
kubectl describe node worker-1 | grep -A5 Conditions

# Watch node events
kubectl get events --field-selector involvedObject.name=worker-1 --sort-by='.lastTimestamp'

# Check kubelet eviction thresholds in use
kubectl get --raw /api/v1/nodes/worker-1/proxy/configz | jq '.kubeletconfig.evictionHard'

🔌 kubectl port-forward — Reach Internal Services

kubectl port-forward creates a tunnel from your local machine to any pod or service port inside the cluster. Essential for accessing dashboards, databases, or debugging services not exposed via Ingress.

# Forward local port 8080 → pod port 80
kubectl port-forward pod/my-app-xyz 8080:80

# Forward to a Service (picks a random ready pod endpoint)
kubectl port-forward svc/my-service 8080:80

# Forward to a Deployment
kubectl port-forward deploy/my-app 8080:80

# Multiple ports at once
kubectl port-forward pod/postgres-0 5432:5432 9187:9187

# Bind to all interfaces (access from other machines on LAN)
kubectl port-forward --address 0.0.0.0 svc/grafana 3000:80 -n monitoring

# Quick one-liners for common dashboards
kubectl port-forward svc/kube-prometheus-stack-grafana 3000:80 -n monitoring &
kubectl port-forward svc/jaeger-query 16686:16686 -n observability &
🔵 Port-forward is not load-balanced When forwarding to a Service, kubectl picks a single pod and tunnels to it directly — it doesn't cycle through all endpoints. For load-balanced access use Ingress. Port-forward is purely for debugging and development.

🧠 Knowledge Check

Q1. A pod uses a distroless image with no shell. How do you get an interactive shell to inspect it?

A) kubectl exec -it <pod> -- /bin/sh — all images include /bin/sh
B) Rebuild the image adding a shell, then redeploy
C) kubectl debug -it <pod> --image=busybox --target=app -- sh — injects an ephemeral container
D) SSH to the node and use docker exec on the container

Q2. A pod is in CrashLoopBackOff and restarts too fast to exec into. What is the best approach?

A) Scale the deployment to 0 then back to 1 to get a fresh start
B) First check kubectl logs --previous; if insufficient, use kubectl debug --copy-to with sleep infinity
C) Increase the liveness probe initialDelaySeconds to give it more time
D) Use kubectl port-forward to connect to the crashing process

Q3. What does kubectl debug node/worker-1 --image=busybox -it -- chroot /host give you access to?

A) A shell inside the kubelet process namespace
B) Read-only access to the node filesystem
C) A root shell on the node OS — journalctl, iptables, crictl, kubelet config all accessible
D) A shell that can only inspect other pods on that node

Q4. You run kubectl port-forward svc/my-svc 9090:80 and access localhost:9090. Which pod receives the traffic?

A) Traffic is load-balanced across all healthy pods in the Service
B) One specific pod is selected — port-forward does NOT load-balance across endpoints
C) The kube-proxy iptables rules handle the load balancing as normal
D) Traffic goes to the node running the Service's ClusterIP