Index Ai Academy
Phase 11 · Professional practice · Shipping

Kubernetes basics

Desired state, Deployments and Services, labels, probes and rolling updates.

About 40 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

Why it matters

Compose runs a stack on one machine. When one machine isn't enough, or must never be a single point of failure, teams run containers on a cluster of many machines, and the standard way to do that is Kubernetes. Even if a platform team runs the cluster, application developers write and review its configuration files every week, and many incidents trace back to a handful of misreadings: a selector that matches nothing, a rollout that takes the service down, a container with no memory limit. This lesson builds the ideas in Python, so a manifest reads as a plan you can check.

What you'll be able to do

  • Explain Kubernetes' core idea: you declare the state you want, and controllers keep making it true
  • Read a Deployment and a Service: replicas, labels and selectors, images, probes and resources
  • Work through a rolling update with surge and unavailability limits
  • Check a manifest for the mistakes that cause outages

What it covers

  1. Say what you want, not what to do
  2. Reading a Deployment and its Service
  3. Which pods does a selector pick?
  4. Rolling updates without downtime
  5. Mistakes a manifest review catches
Quick check
With Kubernetes you say "I want 3 copies of the API running". A server dies, taking one copy with it. What happens?
  1. Nothing until someone notices and starts one by hand
  2. All the copies stop, since one of them failed
  3. The remaining two copies merge into one
  4. Kubernetes starts a new copy elsewhere to get back to 3
Show the answer

Kubernetes starts a new copy elsewhere to get back to 3

You declare the desired state, and Kubernetes keeps comparing it with what's running and fixes the difference. That's how clusters recover from failures and roll out updates without downtime.

Say what you want, not what to do

With Compose you run commands: start this, stop that. Kubernetes works the other way around. You write down the desired state ("three copies of shop-api version 1.4, reachable on port 80"), and controllers, small programs inside the cluster, compare it with what's actually running and act to close the gap, over and over. A crashed copy gets replaced, and a deleted one comes back, without anyone running a command. That loop is called reconciliation.

The objects you'll meet first:

objectwhat it is
Podone or more containers that run together on one machine; the unit Kubernetes starts and stops
Nodea machine in the cluster that runs pods
Deploymentthe desired state for a set of identical pods: which image, how many copies (replicas), how to update them
Serviceone stable address that spreads traffic across the matching pods, however often they're replaced
ConfigMap and Secretsettings and sensitive values handed to pods as environment variables or files
def reconcile(desired, running):
    """One pass of a controller: what to start and stop so running matches desired."""
    start = max(0, desired - len(running))
    stop = running[desired:] if len(running) > desired else []
    return start, stop

print(reconcile(3, ["pod-a", "pod-b"]))                    # one crashed: start one
print(reconcile(3, ["pod-a", "pod-b", "pod-c", "pod-d"]))  # someone scaled down: stop one
print(reconcile(3, ["pod-a", "pod-b", "pod-c"]))           # nothing to do
It runs right here in your browser, with nothing to install.

Reading a Deployment and its Service

Manifests are YAML, like Compose files. Here are the two that run the shop's API:

import yaml

manifest = """
apiVersion: apps/v1
kind: Deployment
metadata:
  name: shop-api
spec:
  replicas: 3
  selector:
    matchLabels: {app: shop-api}
  template:
    metadata:
      labels: {app: shop-api, tier: backend}
    spec:
      containers:
        - name: api
          image: shop-api:1.4
          ports: [{containerPort: 8000}]
          readinessProbe:
            httpGet: {path: /healthz, port: 8000}
          resources:
            requests: {cpu: 250m, memory: 256Mi}
            limits: {memory: 512Mi}
---
apiVersion: v1
kind: Service
metadata:
  name: shop-api
spec:
  selector: {app: shop-api}
  ports: [{port: 80, targetPort: 8000}]
"""

deployment, service = yaml.safe_load_all(manifest)
pod = deployment["spec"]["template"]
container = pod["spec"]["containers"][0]
print(deployment["kind"], deployment["spec"]["replicas"], "replicas of", container["image"])
print("pods are labeled", pod["metadata"]["labels"])
print("the Service sends port", service["spec"]["ports"][0]["port"], "to pods matching", service["spec"]["selector"])

The connections are all labels: small key-value tags. The Deployment's template stamps every pod it makes with app: shop-api, its selector says "the pods I manage are the ones labeled app: shop-api", and the Service's selector picks the same pods to send traffic to. Nothing refers to a pod by name, because pods come and go. Three more fields matter in every review:

  • The readiness probe is a check Kubernetes calls on each pod; until it passes, the Service sends that pod no traffic. (A liveness probe, separately, restarts a container that has hung.)
  • Requests reserve CPU and memory for scheduling a pod onto a node (250m is a quarter of a CPU), and the memory limit is where the container gets killed and restarted if it uses more.
  • The --- line separates two documents in one file, which is why the code uses safe_load_all.

Settings come from ConfigMaps and Secrets rather than being baked into the image, so one image runs in every environment. A container's env can say valueFrom: {secretKeyRef: {name: shop, key: db-password}}, and the cluster fills in the value when the pod starts. That keeps passwords out of the manifest, which lives in git, and lets an operator rotate a password without rebuilding anything (pods read environment variables when they start, so they need a restart to see the new value).

Quick check
The pods are labeled {app: shop-api}, but a new Service was written with selector: {app: shopapi}. What happens to requests sent to the Service?
  1. Kubernetes corrects the Service's selector
  2. They fail: the Service matches no pods
  3. The Deployment stops all its pods
  4. They reach the most similar labels
Show the answer

They fail: the Service matches no pods

Selectors match labels exactly, and nothing checks that a Service's selector matches any pod: the pods run fine, but none are behind it. (A Deployment whose own selector doesn't match its template is rejected outright, which is the one case Kubernetes catches for you.)

Quick check
What does this print?
def reconcile(desired, running):
    start = max(0, desired - len(running))
    stop = running[desired:] if len(running) > desired else []
    return start, stop

running = ["api-a", "api-b", "api-c"]
running.remove("api-b")            # a node fails and takes this pod with it
print(reconcile(3, running))
  1. (1, ['api-b'])
  2. (1, [])
  3. (2, [])
  4. (0, [])
Show the answer

(1, [])

Two pods are left and three are wanted, so the controller starts one more. Nobody had to notice the failure: the next reconcile pass closes the gap.

Drill

Which pods does a selector pick?

A selector matches a pod when every key-value pair in the selector appears in the pod's labels; the pod may have extra labels. Write matches(selector, labels) and picked(selector, pods), where pods maps pod names to their labels, returning the sorted names that match.

def matches(selector, labels):
    return selector == labels


def picked(selector, pods):
    return []


pods = {
    "api-1": {"app": "shop-api", "tier": "backend", "version": "1.4"},
    "api-2": {"app": "shop-api", "tier": "backend", "version": "1.5"},
    "web-1": {"app": "shop-web", "tier": "frontend"},
}
print(picked({"app": "shop-api"}, pods))
In the lesson, your code is checked as you write it.

Rolling updates without downtime

Change the image to shop-api:1.5 and the Deployment replaces its pods gradually, a rolling update, governed by two numbers:

  • maxSurge: how many pods above the replica count may exist during the update (extra capacity, briefly).
  • maxUnavailable: how many pods below the replica count may be missing or not ready.

With 3 replicas, a surge of 1 and no unavailability allowed, the cluster starts one new pod (4 running), waits for its readiness probe, stops one old pod (back to 3), and repeats. Traffic never drops below 3 ready pods. A bigger surge is faster but needs spare room in the cluster; allowing unavailability is faster still but runs with less capacity for a while. Both at zero is impossible: there'd be no way to start a new pod or stop an old one.

If the new version's readiness probe never passes, the rollout stalls with the old pods still serving, which is the safety net that makes probes so important, and kubectl rollout undo goes back to the previous version.

Try it now

replicas 3, maxSurge 1, maxUnavailable 0: never fewer than 3 ready pods.

Quick check
A Deployment has replicas: 3, maxSurge: 0 and maxUnavailable: 1. During an update, what's the most and fewest pods that can exist?
  1. At most 3 pods, and at least 2 ready
  2. At most 4 pods, and at least 2 ready
  3. At most 3 pods, and at least 3 ready
  4. At most 4 pods, and at least 3 ready
Show the answer

At most 3 pods, and at least 2 ready

No surge means never more than 3 pods. Allowing 1 unavailable means it may drop to 2 ready while a replacement starts. It needs no spare room in the cluster, but runs a third short for a while.

Mistakes a manifest review catches

Many Kubernetes incidents trace back to a few lines that a careful review, or a checker, would have caught:

  • An image with no tag, or the tag latest: two pods can end up running different code, and you can't roll back to "the previous latest".
  • No readiness probe: new pods get traffic before they can answer, so every rollout causes a burst of errors.
  • No memory limit: one leaking pod can starve everything else on its node.
  • One replica: a crash, a failed machine or a node drained for maintenance takes the service down until the pod is replaced.
  • A password written as a plain value in the manifest, which lives in git, instead of a reference to a Secret.

Day to day you talk to a cluster with kubectl. kubectl apply -f api.yaml sends the desired state; kubectl get pods lists what's running and whether each pod is ready; kubectl rollout status deployment/shop-api watches an update; kubectl describe pod <name> explains why a pod won't start (a failing probe, an image that can't be pulled, no node with enough memory); and kubectl logs <name> shows what the app printed. When a rollout goes wrong, kubectl rollout undo deployment/shop-api returns to the previous version while you investigate. apply and rollout undo change the desired state and get, describe and logs only read; none of them starts a pod itself. The controllers do that.

Practice what you just read

  • 2 code examples you can edit and run
  • a practice exercise that checks your code
  • a 5-question quiz
  • 5 review cards for spaced repetition

Create a free account

Try a quiz question

Why do new pods need a readiness probe for a rolling update to be safe?

  1. Otherwise pods get traffic before they can answer
  2. Kubernetes refuses to update Deployments without probes
  3. Probes make each new pod start up faster
  4. The probe decides which node runs the pod

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson