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 of andes-cargo-cluster's three nodes this Pod ended up on. In this case, andes-cargo-cluster-workerkube-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, all True when a Pod is fully healthy. PodScheduled (the scheduler already assigned it to a node), Initialized, PodReadyToStartContainers, ContainersReady, and Ready (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. ScheduledPulledCreatedStarted, four steps, each with its own message. Notice the Pulled message: "already present on machine" — because you already downloaded nginx:alpine in Step 1 with kubectl run, so this second Pod didn't need to download it again. This Events section 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 the ImagePullBackOff already 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
  1. kubectl run hello-pod --image=nginx:alpine — the imperative, quick way to create a Pod with no YAML.
  2. kubectl apply -f pod.yaml — the declarative way, with a versionable manifest, that this guide uses from here on.
  3. kubectl describe pod hello-pod — full inspection: assigned node, conditions, and the Events timeline.
  4. kubectl delete pod hello-pod, followed by repeated kubectl 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

  1. Kubernetes — kubectl run — official reference for the imperative command used in Step 1.
  2. Kubernetes — Debug Running Pods — the official diagnostics guide with kubectl describe, this lesson's central tool.
  3. Kubernetes — Pod Lifecycle: Container states — official reference for the states you saw in Conditions and the STATUS column.