Hands-On Labs · The Capstone · Part 4 of 5

Capstone Part 4 — Storage & Stateful Apps

This is Part 4 of the five-part capstone running through this whole course: one continuously evolving cluster and app, carried from a bare kind cluster in Part 1 through a running checkout-api Deployment in Part 2 and a reachable Service and Ingress in Part 3. Every Pod in that story so far has been disposable on purpose — kill any checkout-api Pod and the Deployment simply replaces it, no state lost because there was none to lose. This part introduces the first workload that can't get away with that: checkout-db, a StatefulSet-backed Postgres instance with its own PersistentVolumeClaim, wired to the app you already have running. By the end you'll have deleted a database Pod on purpose, watched Kubernetes bring it back, and proven — not assumed — that the data survived, plus a CronJob that backs it up somewhere the database's own disk can't take it. Part 5 locks down who's allowed to touch any of it.

☺ Explain it like I'm 10

Every classroom helper you've met in this cluster so far is like a substitute teacher — if one calls in sick, any other substitute takes over from the same lesson plan, and nobody in class even notices the swap. A database doesn't work that way: it's the class's own bolted-down filing cabinet, full of everyone's actual grades, and you can't just wheel in an identical empty cabinet and call it the same thing. This part gives the cabinet a permanent parking spot that follows it even if the janitor (Kubernetes) has to replace the cabinet itself overnight — same drawer, same papers, right back where they were. And because even a bolted-down cabinet can be lost in a fire, there's now a photocopier running on a schedule too, so a copy of every paper lives somewhere the original cabinet's fire can't reach.

⚠ Where you are arriving from, and where you're headed

Arriving: a three-node checkout kind cluster from Part 1; a checkout-api Deployment in the checkout namespace reading from checkout-api-config and checkout-api-secrets from Part 2; and, from Part 3, a checkout-api ClusterIP Service sitting behind an ingress-nginx Ingress that makes the app reachable from outside the cluster. Nothing in that stack has ever needed to remember anything past its own restart. Leaving this page: a one-replica checkout-db StatefulSet running Postgres, each replica paired with its own per-ordinal PersistentVolumeClaim provisioned from kind's bundled standard StorageClass; checkout-api pointed at it over the StatefulSet's stable DNS identity; a second, independent PersistentVolumeClaim holding nightly pg_dump archives written by a CronJob; and a firsthand answer to the question "if I delete this Pod, do I still have my data" — because you'll have actually deleted it and checked. Part 5 starts by asking who's even allowed to run that kubectl exec in the first place.

🐘🦫Your hosts for this part: Ellie the Elephant and Benny the Beaver — Ellie never drops a byte of cluster state, so the durable-storage half of this part is her department; Benny writes the manifest that actually claims it.

What you're building on, and what this part adds

☺ Like you're 10: The cluster and the app already exist and already talk to the outside world — this part just gives the app somewhere to keep its memory.

This page assumes Part 1, Part 2, and Part 3 are all behind you: a kind cluster named checkout with one control-plane node and two workers; a checkout namespace; a checkout-api Deployment reading its configuration from a checkout-api-config ConfigMap and a checkout-api-secrets Secret, in the exact shape the Workloads & Scheduling blueprint page walks through; and a checkout-api Service fronted by an ingress-nginx Ingress, reachable at a local hostname. Every one of those objects is stateless by construction — kubectl rollout restart deployment/checkout-api is a completely safe thing to run at any time, because there was never anything inside a checkout-api Pod worth keeping. This page adds the first object in the whole capstone where that stops being true:

ThingName / shapeIntroduced
Clusterkind cluster checkout — 1 control-plane + 2 workersPart 1
NamespacecheckoutPart 1
Applicationcheckout-api Deployment, config from checkout-api-config / checkout-api-secretsPart 2
Reachabilitycheckout-api Service + ingress-nginx IngressPart 3
Databasecheckout-db — one-replica StatefulSet, Postgres 16Part 4 — this page
Primary storagedata-checkout-db-0 PVC, 5Gi, standard StorageClassPart 4 — this page
Backup storagecheckout-db-backups PVC, 2Gi — a completely separate volumePart 4 — this page
Backup jobcheckout-db-backup CronJob, nightly pg_dumpPart 4 — this page

Everything below assumes the object-model and PVC/PV mechanics from The object model and the CKA blueprint's Storage domain — StorageClasses, access modes, reclaim policy, and how a claim actually binds. This page puts those objects to work on a real (if small) database instead of re-deriving them from scratch; read Storage & the CSI first if volumeClaimTemplates or "stable per-replica storage" are unfamiliar phrases.

What kind's default StorageClass actually gives you

☺ Like you're 10: Ask for storage and one small robot on the exact same node quietly carves out a folder on that node's own disk and hands it over.

Nothing in Parts 1–3 ever asked for a PersistentVolume, so this is the first page in the capstone where kind's bundled storage driver actually does anything. Every kind cluster ships with Rancher's local-path-provisioner pre-installed, exposed as a StorageClass named standard and marked default — no separate install step, no cloud account, nothing to provision by hand:

$ kubectl get storageclass
NAME                 PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      DEFAULT
standard (default)   rancher.io/local-path   Delete          WaitForFirstConsumer   true

rancher.io/local-path is deliberately simple: when a PVC using this class is claimed, the provisioner creates a plain directory on one specific node's local filesystem and backs the PersistentVolume with it — no network storage layer, no CSI driver attach/detach dance, just a folder. volumeBindingMode: WaitForFirstConsumer is exactly the behavior the Storage blueprint page calls the exam's default-correct answer, and here it matters for a very concrete reason: the provisioner can't pick which node's disk to use until it knows which node the Pod claiming the volume actually lands on. That single design choice — a local folder, pinned to whichever node happened to get the Pod first — is the reason the reschedule drill further down this page behaves the way it does, and it's worth understanding now rather than being surprised by it later.

⚠ Fine for this capstone, not fine for real production

local-path-provisioner exists to make kind usable without a cloud account attached to it — it is explicitly not meant for production. A real cluster's PersistentVolumes are backed by a real CSI driver talking to genuinely networked storage (an EBS volume, a Ceph RBD image, a GCE Persistent Disk), which is what lets a volume follow a Pod to a different node. Storage & the CSI covers that full plugin contract; this page deliberately stays on the simple provisioner so the mechanics — PVC, StatefulSet, reschedule, backup — stay visible without a cloud bill attached.

The StatefulSet: one replica, one stable disk

☺ Like you're 10: Instead of a Deployment's identical, interchangeable copies, a StatefulSet hands out numbered nametags — and whichever Pod is wearing nametag zero always gets drawer zero back.

checkout-api's Deployment gives every replica an interchangeable, randomly-suffixed name because none of them need to be told apart. A StatefulSet exists for the opposite case: checkout-db-0 keeps that exact name across every restart, and its volumeClaimTemplates entry stamps out a PersistentVolumeClaim per ordinal — data-checkout-db-0 — that the same Pod identity reattaches to every time, rather than a fresh empty volume. A StatefulSet also needs a headless Service (clusterIP: None) to exist first; that's what gives each ordinal its own resolvable DNS name instead of one shared, load-balanced address:

# checkout-db-headless-svc.yaml
apiVersion: v1
kind: Service
metadata:
  name: checkout-db
  namespace: checkout
spec:
  clusterIP: None            # headless — required by StatefulSet's serviceName
  selector:
    app: checkout-db
  ports:
    - name: postgres
      port: 5432
# checkout-db-secret.yaml — Part 5 replaces this plaintext Secret with something properly managed
apiVersion: v1
kind: Secret
metadata:
  name: checkout-db-secrets
  namespace: checkout
type: Opaque
stringData:
  POSTGRES_PASSWORD: "capstone-local-only"
# checkout-db-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: checkout-db
  namespace: checkout
spec:
  serviceName: checkout-db      # must match the headless Service above
  replicas: 1
  selector:
    matchLabels: { app: checkout-db }
  template:
    metadata:
      labels: { app: checkout-db }
    spec:
      containers:
        - name: postgres
          image: postgres:16.4
          ports:
            - containerPort: 5432
              name: postgres
          env:
            - name: POSTGRES_DB
              value: checkout
            - name: POSTGRES_USER
              value: checkout
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef: { name: checkout-db-secrets, key: POSTGRES_PASSWORD }
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata   # see the subPath note below
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
          readinessProbe:
            exec: { command: ["pg_isready", "-U", "checkout"] }
            periodSeconds: 5
          livenessProbe:
            exec: { command: ["pg_isready", "-U", "checkout"] }
            initialDelaySeconds: 30
            periodSeconds: 10
  persistentVolumeClaimRetentionPolicy:
    whenDeleted: Retain           # deleting the StatefulSet must NOT delete the data
    whenScaled: Retain
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: [ReadWriteOnce]
        storageClassName: standard
        resources:
          requests:
            storage: 5Gi

Two details are deliberate, not incidental. PGDATA is set one directory level below the mount point instead of at the mount root — Postgres refuses to initialize a data directory that already contains a filesystem's own reserved entries (like lost+found on a fresh ext4 volume), and a nested pgdata subdirectory sidesteps that entirely without needing a separate subPath volume mount. And persistentVolumeClaimRetentionPolicy is set explicitly rather than left at its default — as the Storage blueprint page covers, StatefulSet PVCs already outlive Pod deletion and scale-down by default, but being explicit here says out loud that deleting checkout-db itself, on purpose or by accident, must never be the thing that deletes the data too.

$ kubectl apply -f checkout-db-headless-svc.yaml -f checkout-db-secret.yaml -f checkout-db-statefulset.yaml
$ kubectl get pods -n checkout -l app=checkout-db -w
NAME             READY   STATUS    RESTARTS   AGE
checkout-db-0    1/1     Running   0          38s

$ kubectl get pvc -n checkout
NAME                    STATUS   VOLUME       CAPACITY   ACCESS MODES   STORAGECLASS
data-checkout-db-0      Bound    pvc-7a3f...  5Gi        RWO            standard
StatefulSet checkout-db, replicas: 1 serviceName: checkout-db Pod checkout-db-0 stable identity PVC data-checkout-db-0 5Gi · RWO class: standard PV folder on node checkout-worker rancher.io/ local-path owns claims bound The Pod name, the claim name, and the node are locked together the moment local-path-provisioner first creates this volume.

Wiring checkout-api to its new database

☺ Like you're 10: The app just needs one address to knock on — the StatefulSet's own headless Service already hands out a name that always means "the current Pod zero."

The headless Service above gives checkout-db-0 a resolvable DNS name in the shape every StatefulSet Pod gets: <pod-name>.<service-name>.<namespace>.svc.cluster.local. Patch that address straight into the same checkout-api-config ConfigMap Part 2 already created, and restart the Deployment so every Pod picks up the new value — a plain ConfigMap update never restarts anything on its own, exactly the gotcha the Workloads & Scheduling blueprint page already warned about:

$ kubectl patch configmap checkout-api-config -n checkout --type merge \
    -p '{"data":{"DB_HOST":"checkout-db-0.checkout-db.checkout.svc.cluster.local","DB_PORT":"5432"}}'

$ kubectl rollout restart deployment/checkout-api -n checkout
$ kubectl rollout status deployment/checkout-api -n checkout

$ kubectl exec -it deploy/checkout-api -n checkout -- curl -s localhost:8080/healthz/db
{"db":"connected"}

Proving state survives a reschedule

☺ Like you're 10: Write something down, knock the whole desk over on purpose, and check the notebook is still readable once the desk gets put back.

Don't take a StatefulSet's promise on faith — write a row, delete the Pod that wrote it, and read the row back once Kubernetes brings the Pod back. This is the entire point of this section, and it takes about ninety seconds:

# write something real, and note which node the Pod is currently on
$ kubectl exec -it checkout-db-0 -n checkout -- psql -U checkout -d checkout -c \
    "CREATE TABLE IF NOT EXISTS orders (id serial primary key, note text);
     INSERT INTO orders (note) VALUES ('part-4-proof');"
$ kubectl get pod checkout-db-0 -n checkout -o jsonpath='{.spec.nodeName}{"\n"}'
checkout-worker

# delete the Pod on purpose — not the StatefulSet, not the PVC
$ kubectl delete pod checkout-db-0 -n checkout
pod "checkout-db-0" deleted

$ kubectl get pod checkout-db-0 -n checkout -w
NAME             READY   STATUS              RESTARTS   AGE
checkout-db-0    0/1     ContainerCreating   0          4s
checkout-db-0    1/1     Running             0          11s

# same claim, same node, same row
$ kubectl get pvc data-checkout-db-0 -n checkout
$ kubectl get pod checkout-db-0 -n checkout -o jsonpath='{.spec.nodeName}{"\n"}'
checkout-worker
$ kubectl exec -it checkout-db-0 -n checkout -- psql -U checkout -d checkout -c "SELECT * FROM orders;"
 id |     note
----+---------------
  1 | part-4-proof

Nothing about that sequence is a coincidence. The StatefulSet controller — the same reconcile-loop pattern the controller pattern page covers in general — notices checkout-db-0 is gone and recreates a Pod with that exact name, which the Storage blueprint page's binding rules then match straight back to the one PVC already named data-checkout-db-0: no new claim gets created, because one with that name already exists and is already bound. And the node stayed the same for a reason worth naming directly, not glossing over.

⚠ "Reschedule" here means the same node, every time — and that's not a bug

local-path-provisioner bakes a nodeAffinity requirement onto every PersistentVolume it creates, pinned to whichever node the folder actually lives on. checkout-db-0 can only ever be scheduled back onto checkout-worker — if that node is cordoned or gone, the Pod sits Pending instead of moving anywhere else, no matter how much capacity the other worker has free. That's a genuine limitation of local-path storage, not a StatefulSet limitation: a real CSI driver backed by networked storage (an EBS volume, a Ceph RBD image) can detach from a dead node and reattach to a healthy one, letting the Pod actually move. Storage & the CSI covers that attach/detach machinery in full — this capstone deliberately trades that mobility away for a setup that needs no cloud account.

A backup CronJob: a second, independent volume

☺ Like you're 10: The filing cabinet surviving a Pod restart is not the same thing as the filing cabinet surviving the whole room burning down — for that you need a photocopy living somewhere else entirely.

The drill above proves checkout-db-0's data survives a Pod being deleted. It proves nothing about the PVC itself being lost — a node failure, an accidental kubectl delete pvc, or the whole kind cluster being torn down all take data-checkout-db-0 with them. A backup has to live on different storage, so its failure is independent of the thing it's backing up. Create a second PVC that has nothing to do with the StatefulSet at all, and a CronJob that periodically dumps into it:

# checkout-db-backups-pvc.yaml — a completely separate volume from checkout-db-0's own disk
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: checkout-db-backups
  namespace: checkout
spec:
  accessModes: [ReadWriteOnce]
  storageClassName: standard
  resources:
    requests:
      storage: 2Gi
# checkout-db-backup-cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: checkout-db-backup
  namespace: checkout
spec:
  schedule: "0 3 * * *"          # 03:00 daily — the cluster's clock, not yours
  jobTemplate:
    spec:
      backoffLimit: 2
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: pg-dump
              image: postgres:16.4
              command: ["/bin/sh", "-c"]
              args:
                - >
                  pg_dump -h checkout-db-0.checkout-db.checkout.svc.cluster.local
                  -U checkout -d checkout -F c
                  -f /backups/checkout-$(date +%Y%m%d-%H%M%S).dump
              env:
                - name: PGPASSWORD
                  valueFrom:
                    secretKeyRef: { name: checkout-db-secrets, key: POSTGRES_PASSWORD }
              volumeMounts:
                - name: backups
                  mountPath: /backups
          volumes:
            - name: backups
              persistentVolumeClaim:
                claimName: checkout-db-backups

Don't wait until 3 a.m. to find out whether this works. Every CronJob can be triggered on demand by stamping out one ordinary Job from its template — exactly how you'd rehearse an alert firing before trusting it, applied here to a backup instead:

$ kubectl apply -f checkout-db-backups-pvc.yaml -f checkout-db-backup-cronjob.yaml
$ kubectl create job --from=cronjob/checkout-db-backup checkout-db-backup-manual -n checkout
job.batch/checkout-db-backup-manual created

$ kubectl wait --for=condition=complete job/checkout-db-backup-manual -n checkout --timeout=60s
$ kubectl run -it --rm backup-check -n checkout --image=busybox --restart=Never \
    --overrides='{"spec":{"containers":[{"name":"backup-check","image":"busybox","command":["ls","-la","/backups"],"volumeMounts":[{"name":"b","mountPath":"/backups"}]}],"volumes":[{"name":"b","persistentVolumeClaim":{"claimName":"checkout-db-backups"}}]}}'
-rw-r--r--    1 root     root         8192 checkout-20260827-030112.dump
checkout-db-0 the running Pod PVC: data-checkout-db-0 survives Pod delete + recreate not the PVC's own loss CronJob checkout-db-backup, 03:00 Job runs pg_dump once PVC: checkout-db-backups survives losing the primary PVC a wholly separate volume two independent failure domains
◆ This is a floor, not the finished thing

A single pg_dump onto a PVC in the same cluster is real, and it's genuinely more than most weekend projects ever set up — but it's still one cluster failure away from taking the backup down with the original. Velero covers the production version of this exact idea: shipping both the API objects and the volume data to object storage outside the cluster entirely, with pre-backup hooks for exactly the "quiesce Postgres before snapshotting" problem a raw pg_dump sidesteps by dumping logically instead of snapshotting the raw files. And once checkout-db needs more than one replica with real failover, Stateful Workloads & Database Operators is where quorum-aware PodDisruptionBudgets and an operator-owned backup loop pick up from here.

✎ Try it

Delete the entire checkout-db StatefulSet — not the PVC — with kubectl delete statefulset checkout-db -n checkout, then confirm with kubectl get pvc -n checkout that data-checkout-db-0 is still sitting there, Bound, untouched. That's persistentVolumeClaimRetentionPolicy: Retain doing its job. Re-apply the StatefulSet manifest and confirm checkout-db-0 comes back up and your orders row is still readable — same proof as the Pod-delete drill, one level further up the object hierarchy. Then, separately, restore the checkout-db-backups dump into a throwaway database (pg_restore against a fresh, empty Postgres container) and confirm the restored row matches — a backup nobody has ever restored is a hope, not a plan.

🦆 Dot's-eye view

"I don't want to reason about node affinity or which provisioner backs my StorageClass. I want my StatefulSet's YAML to say 'give me 5 gigs, ReadWriteOnce' and have it work the same way whether it's this laptop's kind cluster or a real EKS cluster next quarter. That's exactly what happened here — nothing in checkout-db-statefulset.yaml mentions local-path-provisioner by name anywhere. The day this workload moves to a real cluster, that manifest doesn't change; only the StorageClass underneath it does."

What "done" looks like for Part 4

☺ Like you're 10: A database that remembers things even after being knocked over, plus a photocopy of everything living somewhere else entirely.

At the end of this part, checkout-db runs as a one-replica StatefulSet with its own per-ordinal PersistentVolumeClaim, provisioned dynamically from kind's standard StorageClass; checkout-api talks to it over its stable per-Pod DNS name; a deliberately separate PersistentVolumeClaim holds nightly pg_dump archives written by a CronJob you've already triggered and verified by hand; and you've personally deleted checkout-db-0 and watched its data come back rather than taking any of this on faith. Nothing here gets thrown away — Part 5 builds directly on it:

PartWhat it does with today's storage
5 — Security & RBACLocks down exactly who can kubectl exec into checkout-db-0, read checkout-db-secrets, or trigger the backup CronJob by hand — none of which anything before this page ever restricted
🎬 At the Pod Squad
🦫

Benny the Beaver: First time I've written a manifest where deleting the wrong thing actually loses something real. The Secret, the retention policy, the subPath trick for Postgres's data dir — every line mattered more than usual.

👺

Gizmo the Gremlin: Overthinking it. Skip the PVC entirely, use emptyDir — same YAML shape, way less typing, and it's still "storage." 🤑

🐘

Ellie the Elephant: An emptyDir dies with the Pod on purpose — that's not a shortcut on a PVC, it's the opposite of one. Every row Benny writes would vanish the instant checkout-db-0 restarted for any reason at all.

🦊

Foxy: Speaking of restarting — if checkout-worker just died outright instead of the Pod restarting normally, does checkout-db-0 come back anywhere else?

🐘

Ellie: No — and that's the honest limit of this setup, not a secret. local-path-provisioner pins the volume to that one node. It sits Pending until the node's back. A real CSI driver would let it move; this one trades that away for zero cloud cost.

🤖

Recon the Robot: Which is exactly why the backup CronJob isn't optional decoration. It's the one piece of this whole part that doesn't care what happens to checkout-worker at all.

🐢 Timmy's checkpoint

1. Why does a StatefulSet need a headless Service, and what does that give each Pod that a normal ClusterIP Service doesn't? 2. When checkout-db-0 is deleted and recreated, why does it reattach to the same PVC instead of getting a fresh, empty one? 3. Why is checkout-db-0 only ever rescheduled onto the exact same node, and what kind of storage would remove that restriction? 4. Why does the backup CronJob write to a completely separate PersistentVolumeClaim instead of a second file on data-checkout-db-0? 5. What does setting persistentVolumeClaimRetentionPolicy: Retain explicitly protect against that the field's mere existence doesn't guarantee on its own?

Check your answers
  1. A StatefulSet's serviceName must point at a headless Service (clusterIP: None) so each Pod gets its own individually resolvable DNS name — checkout-db-0.checkout-db.checkout.svc.cluster.local — rather than being load-balanced behind one shared cluster IP the way ordinary Deployment Pods are.
  2. The volume claim template names the PVC deterministically from the StatefulSet name and the Pod's ordinal — data-checkout-db-0. Recreating the Pod with the same name means the same claim name already exists and is already Bound, so binding just reattaches it instead of provisioning a new one.
  3. local-path-provisioner bakes a nodeAffinity onto the PersistentVolume, pinned to whichever node's local disk it actually used. A real CSI driver backed by networked storage (EBS, Ceph RBD, a cloud Persistent Disk) can detach from one node and reattach to another, letting the Pod genuinely move.
  4. So the backup's failure is independent of the primary volume's failure. A second file on the same PVC would be lost the instant that PVC itself is lost — a node failure, an accidental deletion — which defeats the entire purpose of a backup.
  5. It protects against the StatefulSet object itself being deleted (intentionally or by mistake) taking the PVC with it. Without an explicit policy, current Kubernetes versions already default to keeping the PVC — but stating it in the manifest makes that guarantee an explicit, reviewable decision instead of an implicit default someone could quietly change.

Part 4 gave checkout-db a memory that survives more than it looks like it should, and a backup that doesn't share its fate. Continue to Capstone Part 5 — Security & RBAC, where access to everything built so far finally gets locked down. Or step back to the full lab track to see how this capstone fits the rest of the hands-on labs, revisit the CKA blueprint's own Storage domain for the exam-scoped version of everything on this page, and read Storage & the CSI or Velero for what this setup deliberately left for later.

Milestones

☺ Like you're 10: Tick each box only once you've actually watched it happen on your own cluster, not because the step "sounds right."

Work these in order — each depends on the cluster state from the one before. Progress saves in this browser.

0 / 9 milestones complete
1Confirm Part 3's checkout-api Service and Ingress are still reachable
Hit the app's Ingress hostname from outside the cluster before touching storage — you want a known-good starting point.
Done when: the Part 3 health endpoint returns a normal response through the Ingress.
2Confirm the standard StorageClass and its binding mode
Run kubectl get storageclass and read its provisioner and VOLUMEBINDINGMODE columns.
Done when: you can say out loud why WaitForFirstConsumer matters for a node-local provisioner specifically.
3Apply checkout-db-secrets, the headless Service, and the StatefulSet
Apply all three manifests from this page in the order shown.
Done when: checkout-db-0 shows 1/1 Running and data-checkout-db-0 shows Bound.
4Point checkout-api at checkout-db and confirm a real connection
Patch checkout-api-config with the StatefulSet's stable DNS name, restart the Deployment, and hit its DB health check.
Done when: the app reports a live database connection, not a cached or mocked one.
Concept: Service DNS
5Write a row, delete the Pod, and read the row back
Run the exact psql / kubectl delete pod / psql sequence from this page — no shortcuts.
Done when: SELECT * FROM orders; shows your row after the Pod has fully restarted.
6Confirm the node pinning, and say why it exists
Compare spec.nodeName before and after the Pod delete in the previous step.
Done when: the node name is identical both times, and you can explain the nodeAffinity mechanism causing it without looking it up.
7Apply the backup PVC and CronJob, then trigger one manually
Apply checkout-db-backups-pvc.yaml and the CronJob, then run kubectl create job --from=cronjob/....
Done when: a .dump file with a real, non-zero size shows up on the backup PVC.
8Delete and reapply the whole StatefulSet, then restore the backup once
Work through the "Try it" callout on this page in full — both the retention-policy check and the restore.
Done when: the PVC survives the StatefulSet's deletion, and a restore of the backup dump reproduces your row in a throwaway database.
9Say out loud what state you're leaving for Part 5
Confirm: checkout-db running with a bound PVC, checkout-api wired to it, a working backup CronJob, and a Secret still sitting in plaintext.
Done when: you can describe this state without looking anything up — including exactly what's still wide open that Part 5 has to close.