Module 2: Pods Deployments And Services
4. Hands-on: your first Pod
Description
This is this module's first lesson where you actually run something against andes-cargo-cluster. You're going to create a Pod two different ways — first imperative, then declarative — inspect it in depth with kubectl describe, and confirm with your own hands lesson 2's central point: when you delete a loose Pod, no one replaces it. Everything you see here really ran to write this lesson — the names, the states, the events are literal, with the only expected variation in the Pod's IP and the exact node the scheduler assigns it to.
Connection to the module
This lesson is lesson 2's (the Pod concept) practical half and, at the same time, the exact preparation for lesson 5, where you're going to repeat the same "delete a Pod" experiment — but this time with andes-cargo-status-api running inside a Deployment. The contrast between what you see here (no one replaces the Pod) and what you're going to see there (the ReplicaSet replaces it on its own) is, on purpose, this whole module's pedagogical core.
Before starting: confirm the cluster is still up
kind get clusters
kubectl get nodes
What to expect (if your cluster is still in the same state you left it in at the end of Module 1; AGE is your variable value):
andes-cargo-cluster
NAME STATUS ROLES AGE VERSION
andes-cargo-cluster-control-plane Ready control-plane 10m v1.36.1
andes-cargo-cluster-worker Ready <none> 10m v1.36.1
andes-cargo-cluster-worker2 Ready <none> 10m v1.36.1
If kind get clusters returns nothing, your cluster didn't survive (for example, if you explicitly removed the Docker containers, not just stopped them) — go back to Module 1, lesson 5, and create it again before continuing. If the nodes show up, but at NotReady, wait a few seconds and confirm with Docker (docker ps --filter "name=andes-cargo-cluster") that all three containers are still Up.
Step 1 — The quick way: kubectl run (imperative)
kubectl run creates a Pod with a single line, with no need to write any YAML — the fastest way to have something running when you're exploring or debugging. You're going to use it exactly once in this lesson, with a minimal image that has no relation to Andes Cargo — nginx:alpine, a lightweight web server (25 MB), publicly available on Docker Hub, that andes-cargo-cluster can download with no additional registry because its nodes have internet access, same as any normal docker pull from your host:
kubectl run hello-pod --image=nginx:alpine
What to expect (literal, executed):
pod/hello-pod created
kubectl get pods
What to expect (right after creating it — still downloading the image; AGE is your variable value):
NAME READY STATUS RESTARTS AGE
hello-pod 0/1 ContainerCreating 0 0s
This is the command you're going to use constantly for the rest of this guide to verify anything you create — remember it, because it won't be re-explained every time it shows up.
Before continuing, clean up this first Pod — the reason not to leave it is what you see next:
kubectl delete pod hello-pod
What to expect:
pod "hello-pod" deleted from default namespace
Why this guide almost never uses kubectl run from here on. kubectl run is convenient, but it's imperative — no file is left behind describing what you created, so if you need to recreate exactly the same thing tomorrow, you'd have to remember (or rewrite) the full command. The rest of this guide uses YAML manifests applied with kubectl apply -f, following lesson 3's same declarative discipline — a versionable file you can review, diff, and, starting in Module 5, hand over to Git as the source of truth.
Step 2 — The real way: pod.yaml (declarative)
Create the manifest you're going to use for the rest of this lesson:
mkdir -p andes-cargo-k8s && cd andes-cargo-k8s
# pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: hello-pod
labels:
app: hello-pod
spec:
containers:
- name: hello-pod
image: nginx:alpine
ports:
- containerPort: 80
Every field in this file was already explained in depth by lesson 2 — nothing new to learn here, just confirming it really works:
kubectl apply -f pod.yaml
What to expect:
pod/hello-pod created
Wait a few seconds, and confirm it reached Running:
kubectl get pods
What to expect (AGE is your variable value; the rest, literal for this image):
NAME READY STATUS RESTARTS AGE
hello-pod 1/1 Running 0 5s
1/1 in the READY column means: of the containers declared inside this Pod (one, in this case), one is ready. If this Pod had two containers (lesson 2's sidecar pattern), you'd see 2/2 once both are ready.
Step 3 — Inspect the Pod in depth: kubectl describe
kubectl get pods gives you a one-line summary; kubectl describe pod gives you everything Kubernetes knows about that specific object — the command you're going to use constantly to diagnose any problem for the rest of this guide:
kubectl describe pod hello-pod
What to expect (Node, IP, Start Time, Container ID, and Image ID are your variable value — they depend on which node the scheduler assigned this Pod to and the image's exact hash; the rest is literal for this manifest):
Name: hello-pod
Namespace: default
Priority: 0
Service Account: default
Node: andes-cargo-cluster-worker/172.19.0.4
Start Time: Fri, 14 Aug 2026 13:32:55 -0600
Labels: app=hello-pod
Annotations: <none>
Status: Running
IP: 10.244.2.3
IPs:
IP: 10.244.2.3
Containers:
hello-pod:
Container ID: containerd://2b26d030a64387ef61b50dae0542740a2c351610334b1eb58ea3e05ddf790596
Image: nginx:alpine
Image ID: docker.io/library/nginx@sha256:4a73073bd557c65b759505da037898b61f1be6cbcc3c2c3aeac22d2a470c1752
Port: 80/TCP
Host Port: 0/TCP
State: Running
Started: Fri, 14 Aug 2026 13:32:55 -0600
Ready: True
Restart Count: 0
Environment: <none>
Mounts:
/var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-rqd87 (ro)
Conditions:
Type Status
PodReadyToStartContainers True
Initialized True
Ready True
ContainersReady True
PodScheduled True
Volumes:
kube-api-access-rqd87:
Type: Projected (a volume that contains injected data from multiple sources)
TokenExpirationSeconds: 3607
ConfigMapName: kube-root-ca.crt
Optional: false
DownwardAPI: true
QoS Class: BestEffort
Node-Selectors: <none>
Tolerations: node.kubernetes.io/not-ready:NoExecute op=Exists for 300s
node.kubernetes.io/unreachable:NoExecute op=Exists for 300s
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Scheduled 5s default-scheduler Successfully assigned default/hello-pod to andes-cargo-cluster-worker
Normal Pulled 5s kubelet spec.containers{hello-pod}: Container image "nginx:alpine" already present on machine and can be accessed by the pod
Normal Created 5s kubelet spec.containers{hello-pod}: Container created
Normal Started 5s kubelet spec.containers{hello-pod}: Container started
Three sections deserve a close read, because you're going to use them to diagnose real problems for the rest of this guide:
Node— tells you, unambiguously, which ofandes-cargo-cluster's three nodes this Pod ended up on. In this case,andes-cargo-cluster-worker—kube-scheduler(Module 1, lesson 6) made that decision with no one explicitly asking; in lesson 5 you're going to see that, with more than one Pod, the scheduler spreads them across the available nodes.Conditions— five independent checks, allTruewhen a Pod is fully healthy.PodScheduled(the scheduler already assigned it to a node),Initialized,PodReadyToStartContainers,ContainersReady, andReady(the final summary). When something fails, this table is the first place you see which of the five steps got stuck, not just that "something's wrong."Events, at the end — the complete timeline of what happened to this Pod, in order.Scheduled→Pulled→Created→Started, four steps, each with its own message. Notice thePulledmessage: "already present on machine" — because you already downloadednginx:alpinein Step 1 withkubectl run, so this second Pod didn't need to download it again. ThisEventssection is, without exception, the first place you're going to look when a Pod doesn't start as expected for the rest of this guide — including theImagePullBackOffalready named in Module 1.
Step 4 — Delete the Pod, and watch that no one recreates it
Here's this lesson's central moment — real-evidence confirmation of everything lesson 2 explained in theory:
kubectl delete pod hello-pod
What to expect:
pod "hello-pod" deleted from default namespace
Confirm right away:
kubectl get pods
What to expect:
No resources found in default namespace.
Wait a few more seconds, and run the same command again — not out of distrust of the previous result, but because it's exactly the discipline you need for lesson 5, where this same wait is going to show a change:
kubectl get pods
What to expect (identical to before, no matter how long you wait):
No resources found in default namespace.
Nothing is ever going to change, no matter how long you wait. There's no ReplicaSet, no Deployment, no controller watching this specific Pod — you declared it, it ran, you deleted it, and that ended its entire existence. Keep this exact result in mind: it's what you're going to directly contrast in lesson 5.
Analogy: the office with no administration department
Picking back up the module's analogy: hello-pod was, for the minutes it ran, a complete individual office — with everything it needed inside (the nginx:alpine container, its own network address) — but with no building administration department (Deployment) keeping track of whether that office should stay occupied or not. When you "closed" that office (kubectl delete pod), no one in the building noticed, because no one had instructions to watch it. Lesson 5 adds exactly that missing piece: a real administration department, with explicit instructions to keep a fixed number of offices occupied, no matter what happens to any single one of them.
Common mistakes
Expecting kubectl get pods to show something related to kubectl run after deleting it under another name (workflow, an easy slip specific to this lesson). What happens: someone runs kubectl run hello-pod in Step 1, deletes it, and then in Step 2 applies pod.yaml — which declares the same name, hello-pod — and gets confused thinking it's "the same Pod still alive" instead of a completely new object. Why it happens: the name matches on purpose in this lesson, so Step 2 feels like a natural continuation of Step 1, not an unrelated object. How to spot it: if you compare Step 3's kubectl describe pod's Start Time against when you ran Step 1 — they're going to be different moments, because they're different objects, even though they share a name. How to fix it: it's not a functional error (Kubernetes has no problem reusing a name after the previous object was deleted), just a conceptual misunderstanding — every kubectl apply/kubectl run you see in this lesson creates a new object, with no memory of a previous one with the same name.
Confusing READY: 0/1 with an error, instead of a normal transitional state (conceptual, you already saw a variant of this in Module 1 with NotReady on the nodes). What happens: someone runs kubectl get pods right after Step 1 or Step 2, sees 0/1 in the READY column and ContainerCreating in STATUS, and assumes something failed. Why it happens: it's easy to read "0 of 1" as an incomplete fraction in a negative light. How to spot it: if the same command, run a few seconds later, shows 1/1 and Running — that confirms it was transitional, not an error. How to fix it: always wait a few moments after creating a Pod, before diagnosing a problem; ContainerCreating literally means the startup process (downloading the image if needed, creating the container) is still ongoing — it's the same pattern you already saw with NotReady on the nodes in Module 1.
Not checking kubectl describe pod's Events section before assuming something's broken (workflow, this lesson's most valuable habit for the rest of the guide). What happens: someone sees a Pod that doesn't reach Running and starts guessing at the cause — checking the image, the YAML, anything — before looking at the Events section, which almost always already has the explicit answer. Why it happens: Events appears at the end of describe, after more technical sections (Volumes, Tolerations) that can feel more "important" at first glance. How to spot it: if you spent more than a minute checking a problem Pod's YAML before running kubectl describe pod and reading its full Events section. How to fix it: install the habit starting with this lesson — kubectl describe pod <name>, and read Events first, top to bottom. You're going to repeat this exact reflex, without exception, every time something doesn't start as expected for the rest of this guide.
Exercises
Exercise 1 — Reconstruct the whole flow from memory. Without going back to the lesson, list the four steps, in order, you followed in this lesson, and what each one demonstrated.
See solution
kubectl run hello-pod --image=nginx:alpine— the imperative, quick way to create a Pod with no YAML.kubectl apply -f pod.yaml— the declarative way, with a versionable manifest, that this guide uses from here on.kubectl describe pod hello-pod— full inspection: assigned node, conditions, and theEventstimeline.kubectl delete pod hello-pod, followed by repeatedkubectl get pods— demonstrated that a loose Pod, with no controller behind it, never replaces itself.
Exercise 2 — Interpret someone else's describe pod. A coworker sends you the output of a kubectl describe pod whose Conditions section shows PodScheduled: True but Initialized: False, with the rest blank. What does this tell you about the exact point that Pod got stuck at, and which section would you check first to find out why?
See solution
It tells you kube-scheduler did assign the Pod to a node (PodScheduled: True), but that Pod's initialization process on that node hasn't finished (Initialized: False) — the problem is at some step after the assignment, probably related to downloading the image or preparing the container, not to finding an available node. The section to check first is always Events, at the end of describe — that's where the explicit message about what's blocking that step should appear (for example, a Pulling/Pulled error if the image can't be downloaded).
Exercise 3 — Predict a case with two containers. If pod.yaml declared two containers inside spec.containers instead of one, what would you expect to see in kubectl get pods's READY column while only one of the two has finished starting? And when both are ready?
See solution
While only one of the two containers is ready: READY would show 1/2 — the first number counts how many of the Pod's containers are ready, the second how many are declared in total. Once both finish starting: READY would show 2/2, and only then would the whole Pod be considered ready (Ready: True in Conditions) — a Pod with multiple containers isn't considered "ready" until all its containers are, one working isn't enough.
Summary and next step
In this lesson you created your first real Pod two ways — imperative with kubectl run, declarative with kubectl apply -f pod.yaml — inspected it in depth with kubectl describe pod (assigned node, conditions, event timeline), and confirmed with real evidence lesson 2's central point: a loose Pod, with no controller behind it, disappears forever the moment you delete it — no one replaces it, no matter how long you wait.
Before moving on you should be able to: create a Pod with both methods and explain when to use each one; read a kubectl describe pod's Events section to diagnose which step a problem Pod got stuck at; and describe, with your own evidence, why a loose Pod doesn't self-heal.
Next lesson: hands-on, andes-cargo-status-api's Deployment. There you repeat the same "delete a Pod" experiment — but this time with Andes Cargo's real service, running inside a Deployment. The result is going to be the exact opposite of what you just confirmed here.
Resources
- Kubernetes — kubectl run — official reference for the imperative command used in Step 1.
- Kubernetes — Debug Running Pods — the official diagnostics guide with
kubectl describe, this lesson's central tool. - Kubernetes — Pod Lifecycle: Container states — official reference for the states you saw in
Conditionsand theSTATUScolumn.