Signal9Signal9 — home
online
all lessons

Kubernetes Introduction: Pods, Deployments, and Services

Advanced40 min5 steps
What you'll learn
  • Explain Kubernetes as a declarative control loop that drives actual state toward desired state
  • Distinguish the core objects: Pod, Deployment/ReplicaSet, and Service
  • Read and apply a manifest with kubectl, and inspect what the cluster did
  • Know why you deploy Deployments, not bare Pods, and why Services exist
  1. The big idea: declare desired state, let the loop reconcile

    Understand Kubernetes as a reconciliation loop, not a script runner.

    Kubernetes (k8s) is a container orchestrator: it runs and keeps containers running across a fleet of machines (nodes). Its defining idea is declarative desired state. You don't tell it how to do things step by step; you declare what you want — 'three replicas of this image, reachable on port 80' — and the cluster's control loop continuously compares actual state to desired state and takes action to close the gap.

    A node dies and takes your containers with it? The loop notices the replica count dropped and schedules replacements elsewhere. You ask for 5 replicas instead of 3? It starts 2 more. This self-healing, level-triggered reconciliation — not one-shot commands — is what makes k8s different from docker run, and it shapes everything else in this lesson.

    It helps to know who runs that loop. A cluster has a control plane and a set of worker nodes. The control plane holds the API server (the single front door — everything, including kubectl, talks to it), etcd (the consistent key-value store that records desired state), the scheduler (decides which node a new Pod lands on), and the controllers (the loops that reconcile). Each worker node runs a kubelet (starts and health-checks the containers the API server assigns it) and a container runtime. When you kubectl apply a manifest, you're writing your desired state into etcd via the API server; the controllers and scheduler then make the worker nodes match it. Nothing you run imperatively 'on a node' — the whole system is mediated through that declarative API, which is what makes it auditable and self-healing.

  2. Pods: the smallest deployable unit

    Understand what a Pod is and why it's rarely created directly.

    The smallest thing k8s schedules is not a container but a Pod: one or more containers that share a network namespace (same IP and port space) and can share storage volumes. Most Pods hold a single application container, sometimes with a helper 'sidecar'. Pods are ephemeral and disposable — they get a cluster-internal IP, but if a Pod dies it is replaced by a new Pod with a new IP, never resurrected.

    Because of that churn you almost never create bare Pods yourself. You declare a higher-level object (a Deployment) that manages Pods for you — creating, replacing, and scaling them to match desired state.

    Example — not runnable on your lab box

    kubectl get pods
    NAME                    READY   STATUS    RESTARTS   AGE
    web-7d9f8c6b5f-2xk4p    1/1     Running   0          3m
    web-7d9f8c6b5f-l7m2q    1/1     Running   0          3m
    web-7d9f8c6b5f-qp8vn    1/1     Running   0          3m
    Three Pods, each with a generated name (the Deployment's name + a ReplicaSet hash + a random suffix). READY 1/1 means the single container in each Pod is up; STATUS Running and RESTARTS 0 show health. These names change whenever Pods are replaced — which is exactly why clients must not target Pod IPs directly.
  3. Deployments: declarative replicas and rollouts

    Use a Deployment to keep N replicas and roll out updates safely.

    A Deployment is the object you actually manage. It records your desired replica count and Pod template, and it owns a ReplicaSet that enforces 'keep exactly N Pods matching this template alive.' Change the image in the Deployment and it performs a rolling update: it brings up new Pods and retires old ones gradually, so the app stays available. If a rollout goes bad, kubectl rollout undo steps back to the previous ReplicaSet.

    Scaling is just editing desired state: set replicas to 5 and the ReplicaSet creates two more Pods to match.

    Example — not runnable on your lab box

    kubectl scale deployment web --replicas=5
    kubectl get deployment web
    deployment.apps/web scaled
    NAME   READY   UP-TO-DATE   AVAILABLE   AGE
    web    5/5     5            5           6m
    kubectl scale edits the Deployment's desired replica count; the control loop reconciles by creating Pods until READY 5/5. UP-TO-DATE is how many Pods match the current template (relevant during a rollout) and AVAILABLE how many are passing readiness — all 5 here.

    Check yourself

    You need three copies of your app to stay running and to survive a node failure. Which Kubernetes object do you create — a bare Pod, or a Deployment?

  4. Services: a stable address for moving targets

    Expose ephemeral Pods behind one stable name and IP.

    Pods come and go with ever-changing IPs, so you never connect to a Pod directly. A Service gives a set of Pods (selected by labels) a single stable virtual IP and DNS name, and load-balances connections across the healthy Pods behind it. Your frontend talks to http://web (the Service); k8s routes each request to one of the current web Pods, whatever their IPs happen to be right now.

    Common Service types: ClusterIP (default — reachable only inside the cluster), NodePort (opens a port on every node), and LoadBalancer (asks the cloud for an external load balancer).

    Example — not runnable on your lab box

    kubectl get service web
    NAME   TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
    web    ClusterIP   10.96.140.22    <none>        80/TCP    6m
    The Service web has a stable ClusterIP (10.96.140.22) and DNS name (web) that never changes even as the Pods behind it are replaced. EXTERNAL-IP <none> because ClusterIP is internal-only; a LoadBalancer type would show an external address here. Behind the scenes the Service keeps an up-to-date list of healthy Pod IPs (its endpoints) as Pods come and go, and the node's networking layer rewrites traffic aimed at the ClusterIP to one of those endpoints. So the indirection you get is real load balancing with automatic membership — add replicas and they start receiving traffic with no client change; drain a Pod and it stops — all keyed off the label selector, never off any fixed address.
  5. Manifests and kubectl: declare it in YAML, apply it

    Read a manifest and apply/inspect it with kubectl.

    You declare objects in YAML manifests and hand them to the cluster with kubectl apply -f. apply is declarative and idempotent — it makes the live object match the file, creating it the first time and updating it thereafter, so the manifest in git is the source of truth. kubectl get lists objects, and kubectl describe / kubectl logs are your first debugging stops.

    Two concepts tie the objects together. Labels are arbitrary key/value tags on objects (app: web), and selectors match on them — that's the loose coupling by which a Deployment claims its Pods and a Service finds its backends; there are no hard-wired references, just labels that agree. Namespaces partition a cluster into virtual sub-clusters (default, kube-system, and your own like staging/prod) so names only need to be unique within a namespace and you can scope access and quotas per team. When a Pod won't start, the workflow is almost always the same: kubectl get pods to see the STATUS (Pending, CrashLoopBackOff, ImagePullBackOff each point at a different cause), then kubectl describe pod <name> to read the Events at the bottom, then kubectl logs <name> for what the container itself said. That trio resolves the large majority of 'why is my app not up' questions.

    Example — not runnable on your lab box

    cat web-deploy.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: web
    spec:
      replicas: 3
      selector:
        matchLabels: { app: web }
      template:
        metadata:
          labels: { app: web }
        spec:
          containers:
            - name: web
              image: ghcr.io/acme/web:1.4.2
              ports:
                - containerPort: 80
    A Deployment manifest: kind names the object type, spec.replicas the desired count, selector.matchLabels which Pods it owns, and template the Pod blueprint (labels must match the selector, and the image is pinned to an immutable tag). kubectl apply -f web-deploy.yaml reconciles the cluster to this file.
Recap
  • Kubernetes is a declarative control loop: you state desired state and it continuously reconciles actual state toward it (self-healing, rolling updates).
  • A Pod is the smallest, ephemeral unit; you rarely create bare Pods — a Deployment (via a ReplicaSet) keeps N replicas alive and rolls out updates.
  • A Service gives label-selected Pods one stable IP/DNS name and load-balances to them, so clients never target ever-changing Pod IPs.
  • Declare objects in YAML manifests and reconcile with kubectl apply -f; inspect with kubectl get/describe/logs.