Education › DevOps › Stage 3: Run at scale

Kubernetes core

Pods, Deployments, Services, Ingress, ConfigMaps, probes, requests and limits.

Intermediate ~40 min read Module 9 of 17

Docker runs a container on one machine. Production needs many copies across many machines, restarted when they die, replaced without downtime, and found by other services wherever they land. Kubernetes does that, and it has become the common platform that the rest of this track builds on. This module teaches the handful of objects that make up ninety percent of daily work, and the debugging loop you will use every day.

After this module you can
  • Explain the declarative model: desired state, controllers and the reconciliation loop
  • Write manifests for a Deployment, a Service and an Ingress, and explain how labels connect them
  • Inject configuration with ConfigMaps and Secrets
  • Configure liveness, readiness and startup probes, and resource requests and limits
  • Diagnose a failing pod with kubectl get, describe, logs and events

Desired state and the reconciliation loop

You do not tell Kubernetes what to do; you tell it what you want, and it works continuously to make reality match. You submit a manifest, a YAML description of an object, to the API server, which stores it in etcd. Controllers watch for differences between desired and actual state and act to close the gap. If a node dies and takes two of your three replicas with it, the controller notices "want 3, have 1" and starts two more elsewhere. Nobody gets paged.

A cluster has a control plane (API server, scheduler, controller manager, etcd) and worker nodes, each running a kubelet that starts containers and reports back. The scheduler decides which node each new pod runs on. Namespaces divide a cluster into named scopes for teams or environments.

CONTROL PLANEWORKER NODESdesired statestoresunscheduled podscreates and repairswatch, run pods, reportkubectlapply -fAPI serveretcddesired stateSchedulerpicks a nodeControllerswant 3, have 2?kubelet + podsnode Akubelet + podsnode Bkubelet + podsnode C
A Kubernetes cluster: you send desired state to the API server, the scheduler and controllers work from it, and every node's kubelet watches the API server and runs what it is assigned.
bash
kubectl config get-contexts             # which cluster am I talking to?
kubectl get nodes
kubectl get pods -n shop -o wide        # pods in one namespace, with node and IP
kubectl get all -n shop
kubectl apply -f k8s/                   # create or update everything in a directory
kubectl diff -f k8s/                    # what would apply change?
Watch out

Check your context before every destructive command. Applying a manifest to the production cluster because it was the last context you used is one of the most common self-inflicted outages.

Pods and Deployments

A Pod is the smallest unit: one or more containers that share a network address and can share volumes. Most pods hold one container. Pods are disposable and get a new IP each time, so you almost never create them directly. You create a Deployment, which manages a ReplicaSet, which keeps the requested number of identical pods running.

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: orders
  namespace: shop
spec:
  replicas: 3
  selector:
    matchLabels:
      app: orders
  template:
    metadata:
      labels:
        app: orders
    spec:
      containers:
        - name: orders
          image: ghcr.io/acme/orders-api:1.4.2
          ports:
            - containerPort: 8000
          envFrom:
            - configMapRef:
                name: orders-config
            - secretRef:
                name: orders-secrets

Labels are the glue. The Deployment's selector finds its pods by label, and must match the labels in the pod template. When you change the template, typically the image tag, the Deployment performs a rolling update: it starts new pods, waits for them to become ready, and only then removes old ones. If the new version never becomes ready, the rollout stalls with the old pods still serving.

bash
kubectl set image deployment/orders orders=ghcr.io/acme/orders-api:1.4.3 -n shop
kubectl rollout status deployment/orders -n shop
kubectl rollout history deployment/orders -n shop
kubectl rollout undo deployment/orders -n shop      # back to the previous revision
kubectl scale deployment/orders --replicas=5 -n shop

Services and Ingress

Pods come and go, so nothing should talk to a pod IP. A Service provides a stable virtual IP and DNS name, and load-balances across every ready pod matching its selector. Inside the cluster, the Service below is reachable as orders from the same namespace, or as orders.shop.svc.cluster.local from anywhere.

yaml
apiVersion: v1
kind: Service
metadata:
  name: orders
  namespace: shop
spec:
  selector:
    app: orders
  ports:
    - port: 80            # the Service's port
      targetPort: 8000    # the container's port
Service typeReachable fromTypical use
ClusterIP (default)Inside the cluster onlyService-to-service traffic
NodePortA fixed port on every nodeSimple external access, local clusters
LoadBalancerA cloud load balancer with a public IPExposing one service directly

One cloud load balancer per service gets expensive. An Ingress routes HTTP traffic for many services through a single entry point, by hostname and path, and usually terminates TLS. The Ingress object is only a set of rules; an ingress controller such as ingress-nginx or Traefik must be installed to implement them.

yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: shop
  namespace: shop
spec:
  ingressClassName: nginx
  tls:
    - hosts: [shop.example.com]
      secretName: shop-tls
  rules:
    - host: shop.example.com
      http:
        paths:
          - path: /api/orders
            pathType: Prefix
            backend:
              service:
                name: orders
                port:
                  number: 80
HTTPS/api/orderstargetPort 8000no trafficUsershop.example.comIngresscontrollerServiceorders:80PodreadyPodreadyPodnot ready
How a request reaches a pod: the Ingress routes by host and path to a Service, and the Service load-balances only across the pods that are ready.
Tip

If a Service seems dead, run kubectl get endpointslices -n shop or kubectl describe service orders -n shop. No endpoints means the selector matches no ready pods: either the labels do not match or the readiness probe is failing.

ConfigMaps and Secrets

This is factor 3 of the twelve-factor app, implemented by the platform. A ConfigMap holds non-sensitive settings and a Secret holds sensitive ones. Both can be injected as environment variables, as in the Deployment above, or mounted as files.

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: orders-config
  namespace: shop
data:
  LOG_LEVEL: info
  PAYMENTS_URL: http://payments.shop.svc.cluster.local
---
apiVersion: v1
kind: Secret
metadata:
  name: orders-secrets
  namespace: shop
type: Opaque
stringData:
  DATABASE_URL: postgres://orders:CHANGE_ME@db.shop.svc.cluster.local:5432/orders
Watch out

Secret values are base64-encoded, not encrypted. Anyone who can read the object can decode it, so never commit Secret manifests with real values to Git. Restrict access with RBAC, enable encryption at rest, and source values from a secret manager, which the secrets module covers.

Pods read environment variables once, at start. Changing a ConfigMap does not restart anything, so trigger a new rollout with kubectl rollout restart deployment/orders. Tools such as Helm and Kustomize can automate that by changing the pod template whenever the config changes.

Probes, requests and limits

Kubernetes can only manage your app well if it knows whether the app is healthy and how much it needs. Probes answer the first question.

ProbeQuestionOn failure
readinessProbeCan this pod take traffic right now?Removed from Service endpoints; not restarted
livenessProbeIs this container stuck beyond recovery?The container is restarted
startupProbeHas the app finished starting?Holds off the other probes until it passes
Inside the container spec
yaml
readinessProbe:
  httpGet:
    path: /ready
    port: 8000
  periodSeconds: 5
  failureThreshold: 3
livenessProbe:
  httpGet:
    path: /healthz
    port: 8000
  periodSeconds: 10
  failureThreshold: 3
resources:
  requests:
    cpu: 250m
    memory: 256Mi
  limits:
    memory: 512Mi

Readiness is what makes rolling updates safe, and it may check dependencies the pod needs in order to serve. Liveness should check only the process itself. A liveness probe that fails when the database is down makes Kubernetes restart every pod during a database outage, turning one problem into two.

Requests are what the scheduler reserves: a pod is only placed on a node with that much unallocated capacity. Limits are enforced ceilings. CPU is measured in millicores (250m is a quarter of a core) and is compressible: exceed the limit and the container is throttled. Memory is not: exceed the limit and the container is killed with OOMKilled. Always set requests; without them the scheduler is guessing and one greedy pod can starve its neighbours.

The debugging loop

Almost every problem yields to the same four commands, in this order.

bash
kubectl get pods -n shop                        # 1. what state is it in?
kubectl describe pod orders-7d9f8c6b5-x2k4q -n shop   # 2. read Events at the bottom
kubectl logs orders-7d9f8c6b5-x2k4q -n shop --previous  # 3. logs of the crashed container
kubectl get events -n shop --sort-by=.lastTimestamp     # 4. everything that happened

kubectl exec -it deploy/orders -n shop -- sh    # look around inside
kubectl port-forward svc/orders 8080:80 -n shop # reach a Service from your laptop
kubectl top pods -n shop                        # live CPU and memory (needs metrics-server)
StatusMeaningLook at
PendingNot scheduled onto any nodedescribe events: insufficient CPU or memory, unbound volume, node selectors
ImagePullBackOffThe image cannot be pulledTypo in name or tag, missing registry credentials
CrashLoopBackOffThe container starts, exits, and is restarted with growing delayslogs --previous, exit code in describe, missing config
OOMKilledMemory limit exceededRaise the limit or fix the leak; check kubectl top
Running but 0/1 readyReadiness probe failingProbe path and port, dependency the app is waiting for

Use kubectl explain deployment.spec.strategy to read the documentation for any field without leaving the terminal, and kubectl get deployment orders -o yaml to see the object as the cluster actually holds it, defaults included.

Hands-on practice

Deploy, break and repair on a local cluster

  1. Install kubectl and a local cluster such as kind or minikube. Create a namespace shop and confirm kubectl get nodes shows a Ready node.
  2. Write a Deployment with three replicas of your image from the earlier modules, plus a ClusterIP Service. Apply them and reach the app with kubectl port-forward.
  3. Delete one pod and watch with kubectl get pods -w as the ReplicaSet replaces it. Compare the old and new pod names and IPs.
  4. Add a ConfigMap and inject it with envFrom. Change a value, observe that running pods do not see it, then run kubectl rollout restart.
  5. Add readiness and liveness probes and resource requests. Then set the image to a tag that does not exist, watch the rollout stall with old pods still serving, read the reason in describe, and run kubectl rollout undo.
  6. Set a memory limit far below what the app needs, find OOMKilled in kubectl describe pod, and fix it.
  7. Change the Service selector so that it matches nothing. Find the problem using only kubectl describe service, then repair it.
Cheat sheet

Kubernetes core — at a glance

Main things to focus on

  • Declarative model: you state desired state, controllers reconcile reality to it, forever.
  • Deployment manages ReplicaSet manages Pods. Never rely on an individual pod or its IP.
  • Labels and selectors connect everything. A Service with no endpoints means the selector matches no ready pods.
  • Readiness gates traffic; liveness restarts. Keep dependencies out of liveness probes.
  • Requests drive scheduling; limits are enforced. Over the CPU limit you are throttled, over the memory limit you are killed.
  • Secrets are base64-encoded, not encrypted.
  • Debug in order: get, describe (events), logs --previous, get events.

Look around

kubectl config use-context NAMESwitch cluster; check before destructive commands
kubectl get pods -n NS -o widePods with node and IP
kubectl get pods -APods in every namespace
kubectl get deploy,svc,ing -n NSSeveral resource types at once
kubectl get RESOURCE NAME -o yamlThe full live object
kubectl explain RESOURCE.FIELD.PATHBuilt-in field documentation
kubectl get pods -l app=ordersFilter by label

Change things

kubectl apply -f FILE_OR_DIRCreate or update from manifests
kubectl diff -f FILE_OR_DIRPreview what apply would change
kubectl delete -f FILERemove the objects in a manifest
kubectl scale deploy/NAME --replicas=NChange the replica count
kubectl set image deploy/NAME CONTAINER=IMAGETrigger a rolling update
kubectl rollout restart deploy/NAMERecreate pods, e.g. to pick up new config

Rollouts

kubectl rollout status deploy/NAMEWait for a rollout and report success or failure
kubectl rollout history deploy/NAMEList revisions
kubectl rollout undo deploy/NAMEReturn to the previous revision
kubectl rollout undo deploy/NAME --to-revision=NReturn to a specific revision

Debug

kubectl describe pod NAMEState, exit codes, and Events at the bottom
kubectl logs NAME --previousLogs from the container instance that crashed
kubectl logs -f deploy/NAMEFollow logs of a Deployment's pod
kubectl get events --sort-by=.lastTimestampRecent cluster events in order
kubectl exec -it NAME -- shShell inside a container
kubectl port-forward svc/NAME 8080:80Local port to a Service
kubectl top podsLive usage (needs metrics-server)

Manifest essentials

apiVersion: apps/v1 / kind: DeploymentDeployments, StatefulSets, DaemonSets
apiVersion: v1Pod, Service, ConfigMap, Secret, Namespace
apiVersion: networking.k8s.io/v1Ingress, NetworkPolicy
spec.selector.matchLabels == template.metadata.labelsMust match, or the Deployment is rejected
cpu: 250m / memory: 256MiQuarter of a core / 256 mebibytes

Common pitfalls

  • Running kubectl apply or delete against the wrong context.
  • A liveness probe that checks the database, so a database blip restarts the entire fleet.
  • No resource requests, leaving the scheduler blind and nodes overcommitted.
  • Editing a ConfigMap and expecting running pods to pick up the new environment variables.
  • Committing Secret manifests to Git in the belief that base64 protects them.
  • Deploying the latest tag, which makes rollouts and rollbacks meaningless.
Quiz

Check your understanding

5 questions · 4 to pass · answers are explained as you go. Your best score is saved on this device only.

Progress and quiz scores are saved in this browser only. Back up or restore on the hub.

Was this lesson useful? Tell me what to improve →