Swarm gave you a cluster with just enough ideas and a gentle learning curve. Kubernetes gives you the industry standard in exchange for a new vocabulary. The good news is that you already have the concepts: desired state, reconciliation, replicas, networking between nodes and secrets mounted as files. This lesson puts new names on familiar ideas and adds the ones that are missing, without deploying the whole of Aurora Libros just yet.
Contents
- Why Kubernetes exists and what it adds
- The declarative model and controllers
- Cluster architecture
- The runtime after dockershim: CRI and OCI
- The fundamental objects at a glance
Pod: the unit of deploymentReplicaSetandDeploymentService, its types andIngressConfigMapandSecretNamespacePersistentVolume,PersistentVolumeClaimandStorageClassJob,CronJob,StatefulSetandDaemonSet- Anatomy of a manifest: labels and selectors
- Essential
kubectl, translated from Docker - A local cluster to practice on
- Docker → Kubernetes glossary
- Why Kubernetes exists and what it adds
Kubernetes was born at Google out of the experience with Borg and was donated to the CNCF in 2015. Its goal was not to improve on Swarm but to solve orchestration at the scale of thousands of nodes and teams, which is why its design is more ambitious and also more complex.
| Capability | Swarm | Kubernetes |
|---|---|---|
| Minimum unit | One container per task | Pod: several tightly coupled containers |
| Extensibility | Fixed | CRDs and operators: objects of your own |
| Autoscaling | No | HPA, VPA and node autoscaling (06-06) |
| Storage | Local volumes or plugins | CSI, PVCs and dynamic provisioning |
| Access control | Roles: manager/worker | RBAC by user, resource and verb |
| HTTP routing | Nginx you set up yourself | Native Ingress and Gateway API |
| Stateful workloads | No specific support | StatefulSet with a stable identity |
| Learning curve | Days | Weeks |
| Ecosystem | Small | Enormous: Helm, Argo, Prometheus, meshes |
The last row is what decides it in most organizations. The first is what changes your way of thinking the most, and it is the one we will look at in depth.
- The declarative model and controllers
The conceptual core is identical to Swarm's: you describe the desired state and the system maintains it. The difference is that Kubernetes generalizes it: everything is an object in a database (etcd), and for every type of object there is a controller running the same loop.
That pattern is called the reconciliation loop, and it explains behaviors that are baffling at first:
- You delete a Pod created by a Deployment and it comes back: its controller sees that replicas are missing.
- You edit a Pod by hand and your change disappears at the next deployment: the source of truth is the Deployment's template.
- You apply a manifest twice and nothing happens: you are not describing actions, you are describing destinations. The operation is idempotent.
- Cluster architecture
flowchart TB
U["kubectl / CI"] -->|REST API| API
subgraph CP["Control plane"]
API[kube-apiserver] <--> ETCD[(etcd)]
SCH[kube-scheduler] --> API
CM[kube-controller-manager] --> API
end
API --> K1[kubelet · node-1]
API --> K2[kubelet · node-2]
K1 --> R1["containerd (CRI) → runc"]
K2 --> R2["containerd (CRI) → runc"]
style API fill:#e8f0fe,stroke:#3367d6
| Component | Where | Responsibility |
|---|---|---|
kube-apiserver |
Control plane | The only way in; it validates, authenticates and writes to etcd |
etcd |
Control plane | A key-value database holding all the state. Its backup is the cluster's backup |
kube-scheduler |
Control plane | Decides which node each new Pod goes on based on resources, affinities and taints |
kube-controller-manager |
Control plane | Runs the controllers (Deployment, ReplicaSet, Node...) |
kubelet |
Every node | Talks to the runtime to make the assigned Pods exist and reports their state |
kube-proxy |
Every node | Programs the network rules that make Services work |
| Runtime (CRI) | Every node | Runs the containers: containerd, CRI-O |
Notice the property that makes the design robust: nobody talks to anybody except the kube-apiserver. The scheduler does not call the kubelet; it writes into the API that a Pod belongs on a certain node, and the kubelet on that node, which is watching the API, acts. Everything goes through the same point of authentication and auditing.
- The runtime after dockershim: CRI and OCI
In 2022, Kubernetes 1.24 removed dockershim, the adapter that let it use Docker Engine as a runtime. That produced "Kubernetes drops Docker" headlines that confused everybody.
What actually happened is far less dramatic:
- Kubernetes talks to runtimes through an interface called CRI (Container Runtime Interface).
- Docker Engine does not implement CRI, so an adapter (
dockershim) maintained by the project itself was needed. - That adapter was retired, and nodes use containerd or CRI-O directly, both of which do speak CRI. And containerd is, precisely, the same component Docker Engine has been using internally since 01-03.
Your images are still exactly as valid as before. What you build with docker build is an OCI image, a standard format, not a "Docker" format. The ghcr.io/auroralibros/aurora-api:2.0.0 your pipeline produced runs on containerd with no modification whatsoever. The only thing that changed is which program starts it on the node.
- The fundamental objects at a glance
| Object | What for | Do you create it by hand? |
|---|---|---|
Pod |
One or several containers sharing network and storage | Almost never |
ReplicaSet |
Keeps N identical Pods | No: the Deployment creates it |
Deployment |
Manages ReplicaSets and rolling updates | Yes |
Service |
A stable IP and DNS name in front of a set of Pods | Yes |
Ingress |
Inbound HTTP/HTTPS routing by host and path | Yes |
ConfigMap |
Non-sensitive configuration | Yes |
Secret |
Sensitive configuration (base64-encoded) | Yes |
Namespace |
A logical name space inside the cluster | Yes |
PersistentVolumeClaim |
A request for persistent storage | Yes |
StorageClass |
How that storage is provisioned | The cluster supplies it |
Job / CronJob |
A task that finishes / a periodic task | Yes |
StatefulSet |
Pods with a stable identity and disk | Yes |
DaemonSet |
One Pod per node | Yes |
Pod: the unit of deployment
Pod: the unit of deploymentA Pod is a group of containers that share a network namespace (same IP, they talk over localhost), an IPC namespace and, optionally, volumes. It is the unit the scheduler places and the unit that scales: in Kubernetes you do not replicate containers, you replicate Pods.
apiVersion: v1
kind: Pod
metadata:
name: aurora-cache
labels: { app: aurora-cache }
spec:
containers:
- name: redis
image: redis:7-alpine
args: ["--maxmemory", "200mb", "--maxmemory-policy", "allkeys-lru"]
ports: [{ containerPort: 6379 }]
resources:
requests: { memory: 64Mi, cpu: 50m }
limits: { memory: 256Mi, cpu: 500m }You almost never create a Pod by hand, for one compelling reason: a Pod does not recover. If the node dies, the Pod dies with it and nobody recreates it; it is the exact equivalent of a docker run without --restart. What you create is a Deployment, which through its ReplicaSet guarantees there are always as many Pods as you asked for.
Most Pods have a single container. The multi-container case is the sidecar pattern: a helper container accompanying the main one —a log collector, a service mesh proxy, a file synchronizer— that benefits from sharing the network and volumes.
ReplicaSet and Deployment
ReplicaSet and DeploymentapiVersion: apps/v1
kind: Deployment
metadata:
name: aurora-cache
spec:
replicas: 1
selector:
matchLabels: { app: aurora-cache } # which Pods belong to me
template: # the Pod template (no apiVersion, no kind)
metadata:
labels: { app: aurora-cache } # MUST match the selector
spec:
containers:
- name: redis
image: redis:7-alpine
ports: [{ containerPort: 6379 }]The chain of responsibility has three links, and understanding it avoids a lot of confusion:
The ReplicaSet only knows how to count: it keeps N Pods matching its selector. The Deployment manages ReplicaSets: when the image changes it creates a new one, fills it while draining the old one, and keeps the old one at zero replicas so the change can be undone. That empty ReplicaSet you see in kubectl get rs is not garbage: it is your rollback history (06-07).
A warning: a Deployment's selector is immutable. If you change it, the object has to be deleted and recreated. It is worth choosing the labels well the first time.
Service, its types and Ingress
Service, its types and IngressPods are cattle, not pets: they are born and die with different IPs. A Service provides a stable virtual IP and DNS name in front of a changing set of Pods, selected —again— by labels.
apiVersion: v1
kind: Service
metadata:
name: aurora-cache
spec:
type: ClusterIP
selector: { app: aurora-cache } # any Pod with this label joins the balancing
ports:
- port: 6379 # the Service's port
targetPort: 6379 # the container's portPods reach it over DNS as aurora-cache inside the namespace, or aurora-cache.aurora.svc.cluster.local from outside it: the equivalent of the DNS-by-service-name you had in Compose and Swarm.
| Type | What it exposes | Reach | Use in Aurora Libros |
|---|---|---|---|
ClusterIP (default) |
An internal virtual IP | Inside the cluster only | aurora-db, aurora-cache, aurora-api |
NodePort |
A port (30000-32767) on every node | External, crude | Local testing |
LoadBalancer |
A cloud provider load balancer | External, one per service | aurora-web only |
ExternalName |
A CNAME to an external domain |
— | Migrating to a managed DB |
Headless (clusterIP: None) |
No virtual IP: DNS to each Pod | Internal | aurora-db in a StatefulSet |
One LoadBalancer per service gets expensive fast: each one is a billed load balancer at the provider. The standard answer is an Ingress: a single entry point that routes by host and path towards several Services.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: aurora
annotations: { nginx.ingress.kubernetes.io/proxy-body-size: "8m" }
spec:
ingressClassName: nginx # which controller serves it
rules:
- host: libros.aurora.example
http:
paths:
- path: /api
pathType: Prefix
backend: { service: { name: aurora-api, port: { number: 3000 } } }
- path: /
pathType: Prefix
backend: { service: { name: aurora-web, port: { number: 80 } } }A detail that throws people the first time: the Ingress object does nothing on its own. It is a statement of intent that needs an ingress controller installed in the cluster (ingress-nginx, Traefik, HAProxy...) to read it and configure the real proxy. Without a controller, the object exists, raises no error and the traffic goes nowhere.
ConfigMap and Secret
ConfigMap and SecretapiVersion: v1
kind: ConfigMap
metadata: { name: aurora-config }
data:
DB_HOST: aurora-db
DB_NAME: aurora_books
CACHE_TTL: "60"
SHUTDOWN_TIMEOUT_MS: "15000"
---
apiVersion: v1
kind: Secret
metadata: { name: aurora-secrets }
type: Opaque
stringData: # stringData accepts plain text; Kubernetes encodes it
DB_PASSWORD: aurora-dummy-secretBoth are consumed the same way, and there are two forms with different implications:
envFrom: # every key as a variable
- configMapRef: { name: aurora-config }
env: # one specific key
- name: DB_PASSWORD
valueFrom:
secretKeyRef: { name: aurora-secrets, key: DB_PASSWORD }
volumeMounts: # or as files: the _FILE pattern from 06-01
- { name: secrets, mountPath: /run/secrets, readOnly: true }Important warning about
Secret. A Kubernetes Secret is in base64, which is not encryption: anybody with read permission on the object sees the value with abase64 -d. On top of that,etcddoes not encrypt at rest unless that is explicitly enabled. For production you have to enable encryption at rest, restrict access with RBAC and consider an external manager (Vault, Sealed Secrets, your cloud provider's secret manager via the CSI driver). This is a decision you must agree with the security officer in your organization, not an acceptable default.
And one practical difference from Swarm: mounted as a volume, an updated Secret or ConfigMap propagates to the Pod's file without restarting it (with a delay of up to a minute); injected as an environment variable, it never updates until the Pod is recreated.
Namespace
NamespaceA Namespace is a logical partition of the cluster. It gives you three things: names that do not collide (there can be an aurora-api in aurora and another in aurora-staging), a scope for RBAC and quotas, and a network boundary if you use NetworkPolicy. It is not a strong security boundary in itself: it isolates the API, not the kernel.
With kubectl config set-context --current --namespace=aurora you save yourself typing -n aurora on every command.
PersistentVolume, PersistentVolumeClaim and StorageClass
PersistentVolume, PersistentVolumeClaim and StorageClassKubernetes separates who asks for storage from who provides it, which is exactly what Swarm was missing.
| Object | Who writes it | What it says |
|---|---|---|
StorageClass |
The platform | How the disk is created (type, deletion policy) |
PersistentVolumeClaim (PVC) |
You | "I want 10 GiB with ReadWriteOnce access" |
PersistentVolume (PV) |
The provisioner | The real disk, already created |
apiVersion: v1
kind: PersistentVolumeClaim
metadata: { name: aurora-data }
spec:
accessModes: [ReadWriteOnce]
storageClassName: standard
resources: { requests: { storage: 10Gi } }| Access mode | Meaning | Who supports it |
|---|---|---|
ReadWriteOnce (RWO) |
Writing from one node | Block disks: EBS, PD, iSCSI |
ReadOnlyMany (ROX) |
Reading from several nodes | NFS, object storage |
ReadWriteMany (RWX) |
Writing from several nodes | NFS, CephFS, EFS |
Almost every cloud provider's block disk is RWO, and from that comes a very real limit: a PVC of that kind cannot be mounted simultaneously in Pods on different nodes. It is the technical reason aurora-db does not scale by replicating Pods, and we will deal with it in 06-06.
Job, CronJob, StatefulSet and DaemonSet
Job, CronJob, StatefulSet and DaemonSet| Object | Guarantee | In Aurora Libros |
|---|---|---|
Job |
Runs until it completes successfully N times | Schema migration before a deployment |
CronJob |
Creates Jobs on a cron expression | The nightly pg_dump backup |
StatefulSet |
A stable name and identity (-0, -1), its own PVC per Pod, ordered startup and shutdown |
aurora-db |
DaemonSet |
One Pod on every node (and on any that get added) | Promtail, node-exporter |
The DaemonSet is literally Swarm's mode: global. The StatefulSet is what Swarm did not have: each replica gets a stable name and its own volume, survives a restart with the same identity and starts and stops in order. It is what anything stateful needs, and you will use it in 06-05.
- Anatomy of a manifest: labels and selectors
Every object shares four top-level fields:
| Field | What it is | Example |
|---|---|---|
apiVersion |
API group and version | apps/v1, v1, networking.k8s.io/v1 |
kind |
The object type | Deployment, Service |
metadata |
Name, namespace, labels, annotations | name: aurora-api |
spec |
The desired state | replicas: 3 |
status |
The actual state (written by the system, not by you) | readyReplicas: 3 |
And now the most important thing in the whole lesson. In Kubernetes objects are not referenced by name, they are referenced by labels. A Service does not say "send traffic to the Pods of the aurora-api Deployment"; it says "send traffic to every Pod with the label app: aurora-api".
flowchart LR
S["Service<br/>selector: app=aurora-api"] -.->|selects| P1["Pod app=aurora-api"]
S -.-> P2["Pod app=aurora-api"]
S -.-x P3["Pod app=aurora-web"]
D["Deployment<br/>selector: app=aurora-api"] -->|creates| P1
D --> P2
That loose coupling is both powerful and fragile. Powerful because it allows things like directing traffic to Pods from two different Deployments at once, which is the basis of the blue-green and canary deployments in 06-07. Fragile because a typo in a label produces no error at all: the Service simply finds no Pods, ends up with no endpoints and the traffic returns 503. It is the most common silent failure, and it is always diagnosed the same way: kubectl get endpoints <service>.
The labels recommended by the community, which are worth adopting from the start:
labels:
app.kubernetes.io/name: aurora-api
app.kubernetes.io/component: backend
app.kubernetes.io/part-of: aurora-libros
app.kubernetes.io/version: "2.0.0"
- Essential
kubectl, translated from Docker
kubectl, translated from Docker| Task | Docker / Compose | kubectl |
|---|---|---|
| Create or update | docker compose up -d |
kubectl apply -f manifests/ |
| List | docker ps |
kubectl get pods |
| See everything | docker compose ps |
kubectl get all -n aurora |
| Detail and events | docker inspect |
kubectl describe pod <p> |
| Logs | docker logs -f |
kubectl logs -f <p> |
| Logs from the previous attempt | — | kubectl logs <p> --previous |
| A shell inside | docker exec -it c sh |
kubectl exec -it <p> -- sh |
| Reach a port | -p 8080:3000 |
kubectl port-forward svc/aurora-api 8080:3000 |
| Delete | docker compose down |
kubectl delete -f manifests/ |
| Scale | docker service scale |
kubectl scale deploy/aurora-api --replicas=3 |
| Resource usage | docker stats |
kubectl top pods |
| Query the schema | docker run --help |
kubectl explain deployment.spec.strategy |
kubectl get pods -o wide # adds node and IP
kubectl get deploy aurora-api -o yaml # the whole object, with its status
kubectl get pods -o jsonpath='{.items[*].spec.containers[*].image}'
kubectl get pods -w # watch changes live
kubectl describe pod aurora-api-7d9f -n aurora # the Events section is 80 % of the diagnosisTwo commands deserve highlighting. kubectl explain is the official documentation without leaving the terminal, and it works for any field of any object, including ones you install later. And kubectl describe ends with a list of events that almost always contains the exact cause of the problem: why the scheduler cannot find a node, why the image pull fails or why a probe is not passing.
- A local cluster to practice on
| Tool | How it works | Startup | Multi-node | Advantage |
|---|---|---|---|---|
| kind | Nodes as Docker containers | ~30 s | Yes, trivially | Ideal for CI; loads local images |
| minikube | A VM or a container | ~60 s | Limited | Built-in addons (Ingress, dashboard) |
| k3d | k3s (lightweight Kubernetes) in containers | ~20 s | Yes | The lightest; very fast |
| Docker Desktop | A built-in single-node cluster | One click | No | Zero extra installation |
# kind-aurora.yaml — three nodes and port 80 mapped for the Ingress
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
kubeadmConfigPatches:
- |
kind: InitConfiguration
nodeRegistration:
kubeletExtraArgs: { node-labels: "ingress-ready=true" }
extraPortMappings:
- { containerPort: 80, hostPort: 8080, protocol: TCP }
- role: worker
- role: workerkind create cluster --name aurora --config kind-aurora.yaml
kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# aurora-control-plane Ready control-plane 48s v1.31.0
# aurora-worker Ready <none> 31s v1.31.0
# aurora-worker2 Ready <none> 31s v1.31.0
# First contact, with the simplest service in Aurora Libros
kubectl create namespace aurora
kubectl run aurora-cache --image=redis:7-alpine -n aurora --port=6379 --labels=app=aurora-cache
kubectl exec -it aurora-cache -n aurora -- redis-cli ping # PONG
- Docker → Kubernetes glossary
| What you already know | In Kubernetes | Nuance |
|---|---|---|
| Container | Pod | A Pod can hold several containers |
compose.yaml |
YAML manifests | One per object, or several separated by --- |
docker compose up -d |
kubectl apply -f . |
Declarative in both cases |
docker compose down |
kubectl delete -f . |
|
| The Compose network / DNS by name | Service | The DNS comes from the Service, not the network |
| A published port | Service + Ingress |
Exposing and routing are separated |
| A named volume | PVC + StorageClass |
It is requested, not created |
Swarm's deploy.replicas |
The Deployment's spec.replicas |
Identical concept |
mode: global |
DaemonSet | Identical |
| A Swarm secret | Secret | Base64, not encrypted by default |
docker service ps |
kubectl get pods + describe |
The events are in describe |
healthcheck |
livenessProbe + readinessProbe + startupProbe |
The three from 06-01 |
| A Compose project | Namespace | A name space |
Common Mistakes and Tips
- Creating standalone Pods. Nobody recreates them if the node dies. Except for a thirty-second test, always use a Deployment.
- A selector and labels that do not match. The most frequent and the most silent error: the Deployment creates Pods its Service cannot find. Check with
kubectl get endpoints. - Believing a
Secretis encrypted. It is base64. Turn on encryption at rest and protect it with RBAC. - Expecting an
Ingressto work with no controller. The object gets created without complaint and routes nothing. - Forgetting the namespace.
kubectl get podslooks at the default namespace and it seems as if there is nothing there. Set the context or use-n. - Defining
limitswithoutrequests. Kubernetes copies the limit into the request and you reserve far more than you need. - Editing Pods with
kubectl edit. The change lasts until the next deployment. Modify the Deployment instead. - Tip:
kubectl describebeforekubectl logs. If the Pod does not start, there are no logs; the cause is in the events. - Tip:
kubectl apply --dry-run=server -f .validates the manifests against the real API without creating anything. Use it in your pipeline.
Exercises
Exercise 1. Set up a three-node kind cluster, deploy aurora-cache with a Deployment and its ClusterIP Service, and check from another Pod that the name aurora-cache resolves and responds.
Exercise 2. Demonstrate the reconciliation loop and the difference between a standalone Pod and a Deployment: create a Pod by hand and another managed by a Deployment, delete both and observe what happens to each.
Exercise 3. Trigger the silent label failure on purpose: change a Service's selector so it matches no Pod and diagnose it without looking at the manifest.
Solutions
Solution 1.
kind create cluster --name aurora --config kind-aurora.yaml
kubectl create namespace aurora
kubectl config set-context --current --namespace=aurora
kubectl apply -f k8s/cache.yaml # the Deployment and Service from sections 7 and 8
kubectl get deploy,rs,pods,svc
# deployment.apps/aurora-cache 1/1 1 1 12s
# replicaset.apps/aurora-cache-6b8d4c9f7 1 1 1 12s
# pod/aurora-cache-6b8d4c9f7-x2klp 1/1 Running 0 12s
# service/aurora-cache ClusterIP 10.96.184.22 <none> 6379/TCP 12sA single output contains the whole chain from section 7: a Deployment that created a ReplicaSet whose name carries the suffix 6b8d4c9f7 —the hash of the Pod template—, and that ReplicaSet created a Pod that inherits the hash and adds a random suffix. That hash is the key piece of the rolling update: changing the image produces a different hash and therefore a new ReplicaSet.
kubectl run client --rm -it --image=redis:7-alpine --restart=Never -- sh -c '
nslookup aurora-cache | tail -2
redis-cli -h aurora-cache ping
redis-cli -h aurora-cache.aurora.svc.cluster.local set book "Rayuela"'
# Name: aurora-cache.aurora.svc.cluster.local
# Address: 10.96.184.22
# PONG
# OKThe short name and the full one resolve to the same IP, which is the Service's virtual IP, not the Pod's. That distinction is what lets Pods die and be reborn with different addresses without anybody having to find out: the client always talks to the Service. It is the same service the internal DNS of Compose and Swarm gave you, with one important difference: here it is provided by an explicit object you can inspect and modify.
Solution 2.
kubectl run standalone --image=redis:7-alpine --labels=app=test
kubectl create deployment managed --image=redis:7-alpine --replicas=2
kubectl get pods -o custom-columns=NAME:.metadata.name,OWNER:.metadata.ownerReferences[0].kind
# NAME OWNER
# standalone <none>
# managed-5f7c8d9b4-hq2vn ReplicaSet
# managed-5f7c8d9b4-t8plw ReplicaSet
kubectl delete pod standalone managed-5f7c8d9b4-hq2vn && sleep 5 && kubectl get pods
# NAME READY STATUS RESTARTS AGE
# managed-5f7c8d9b4-t8plw 1/1 Running 0 2m
# managed-5f7c8d9b4-vk9xz 1/1 Running 0 5sYou deleted two Pods and only one came back, with a different name and five seconds of age. The OWNER column explains exactly why: the standalone Pod has no ownerReferences, so when you deleted it nobody missed anything; the others belong to a ReplicaSet whose controller watches the API, saw it had one Pod where there should be two and created a new one.
That is the practical reason behind the rule in section 6. A standalone Pod is equivalent to a docker run with no restart policy: it survives as long as nothing bothers it, and it disappears along with its node. The detail of the different name matters too, because it reveals that the Pod is not "restarted": another one is created, with another IP, and that is why no configuration can depend on the identity of one specific Pod. When that identity is genuinely needed —as with aurora-db— there is the StatefulSet.
Solution 3.
kubectl patch svc aurora-cache -p '{"spec":{"selector":{"app":"aurora-cach"}}}' # typo
kubectl run client --rm -it --image=redis:7-alpine --restart=Never -- \
redis-cli -h aurora-cache -t 3 ping
# Could not connect to Redis at aurora-cache:6379: Connection refusedThe diagnosis, without opening a single file:
kubectl get endpoints aurora-cache
kubectl get svc aurora-cache -o jsonpath='{.spec.selector}'; echo
kubectl get pods --show-labelsNAME ENDPOINTS AGE
aurora-cache <none> 14m
{"app":"aurora-cach"}
aurora-cache-6b8d4c9f7-x2klp 1/1 Running app=aurora-cache,pod-template-hash=6b8d4c9f7The ENDPOINTS reading <none> is the decisive symptom, and the three-command sequence is the one to memorize: the Service exists, it has its virtual IP, the DNS resolves perfectly... and there is not one Pod behind it. Comparing the selector (app=aurora-cach) with the actual labels (app=aurora-cache) reveals the missing letter.
kubectl patch svc aurora-cache -p '{"spec":{"selector":{"app":"aurora-cache"}}}'
kubectl get endpoints aurora-cache
# aurora-cache 10.244.1.7:6379 15mWhat is instructive about the exercise is what did not happen: neither the kubectl patch nor the kubectl get svc nor the Deployment's describe gave the slightest warning. Kubernetes does not validate that a selector finds anything, because a Service that does not yet have Pods is a legitimate situation —it will have them once they are deployed. The price of that loose coupling, which in 06-07 will let you switch traffic between two versions by changing a label, is that a typo shows up as a 503 in production rather than as an error when applying the manifest.
Hence the operating rule: after every kubectl apply that touches a Service, check its endpoints. It is one command and it avoids the most frustrating class of incident on this platform.
Conclusion
You speak Kubernetes now. You know its heart is the same reconciliation loop as Swarm's, generalized to every object in etcd, and that this explains why a deleted Pod comes back and why a repeated apply does nothing. You know the architecture —kube-apiserver as the only door, etcd as the state, the scheduler, the controllers, and on every node the kubelet and the kube-proxy— and the property that makes it robust: nobody talks to anybody except the API. And you have the dockershim misunderstanding settled: Kubernetes runs containerd over CRI, but your images are just as valid because they are OCI.
You can handle the catalog of objects with their minimal YAML: the Pod as the unit and why you do not create one by hand, the Deployment → ReplicaSet → Pods chain where the empty ReplicaSet is your rollback history, the Service with its five variants and the Ingress that routes nothing without an installed controller, ConfigMap and Secret —with the warning that base64 is not encryption—, the Namespace, the PVC/PV/StorageClass triad with the access modes that will explain in 06-06 why a database cannot simply be replicated, and the four remaining controllers, among them the DaemonSet that is Swarm's mode: global and the StatefulSet that Swarm did not have.
Above all, you have internalized the central piece: labels and selectors. Everything connects by labels, never by name, and you proved it by breaking a selector on purpose to see the platform's most silent failure —a perfectly healthy Service with ENDPOINTS <none>— and diagnosing it with three commands. You can translate each Docker operation into its kubectl equivalent, you have a three-node kind cluster with aurora-cache running inside it, and a glossary that turns what you already knew into what you have just learned.
In the next lesson, Deploying Docker Containers in Kubernetes, the whole application arrives. You will write the real manifests for the four services: the StatefulSet for aurora-db with its volumeClaimTemplates and its headless Service, the Deployment for aurora-api with the three probes from 06-01, its hardened securityContext and its injected configuration, the Ingress for aurora-web, and you will organize them with Kustomize into bases and per-environment overlays, along with the table of Pod states —Pending, ImagePullBackOff, CrashLoopBackOff, OOMKilled— and the diagnostic command for each, ready for when something refuses to start.
Docker: From Beginner to Advanced
Module 1: Introduction to Docker
- What Is Docker?
- Installing Docker
- Docker Architecture
- Basic Docker Commands
- Understanding Docker Images
- Creating Your First Docker Container
- The Course Project: The Aurora Libros Platform
Module 2: Working with Docker Images
- Docker Hub and Repositories
- Building Docker Images
- Dockerfile Basics
- Advanced Dockerfile Instructions
- Managing Docker Images
- Tagging and Publishing Images
Module 3: Docker Containers
- Running Containers
- Container Lifecycle
- Managing Containers
- Inspecting and Debugging Containers
- Docker Networking
- Data Persistence with Volumes
- Resource Limits and Restart Policies
Module 4: Docker Compose
- Introduction to Docker Compose
- Defining Services in Docker Compose
- Docker Compose Commands
- Multi-Container Applications
- Environment Variables in Docker Compose
- Profiles, Overrides and Multiple Environments
- Local Development with Docker Compose
Module 5: Advanced Docker Concepts
- Docker Networking Deep Dive
- Docker Storage Options
- Docker Security Best Practices
- Optimizing Docker Images
- Advanced Builds with BuildKit and Buildx
- Logging and Monitoring in Docker
- The Runtime Inside: Namespaces, Cgroups and Layers
Module 6: Docker in Production
- Preparing an Image for Production
- CI/CD with Docker
- Orchestrating Containers with Docker Swarm
- Introduction to Kubernetes
- Deploying Docker Containers in Kubernetes
- Scaling and Load Balancing
- Deployment Strategies and Rollback
