Thumbnail

Kubernetes Basics: Pods, Nodes, Containers and Deployments

Understanding the core Kubernetes building blocks is not just useful for getting started. It is the foundation for diagnosing problems when things go wrong. When a pod is stuck in Pending, when a deployment rollout stalls, when a PVC refuses to bind, the path to the fix always starts with understanding what each object is supposed to do and why it is not doing it.

In this post, I'll walk through the fundamental concepts from the perspective of someone who operates these things: nodes, pods, containers, deployments, persistent volumes, and the two main ways to expose services to the outside world.

Prerequisites

  • Basic familiarity with Linux and the command line
  • Some context on what a container is (knowing what a Docker image does is enough)
  • kubectl installed and configured against a cluster (minikube works for local testing)

Goals

  • Understand nodes, pods, and the relationship between them
  • Know when and why to use Persistent Volumes instead of relying on local disk
  • Understand Deployments and how Kubernetes enforces desired state
  • Know the difference between a LoadBalancer service and an Ingress Controller, and when each one is appropriate

Nodes: The Machines Your Workloads Run On

A node is a machine (physical or virtual) in your Kubernetes cluster where pods are scheduled. In cloud environments, nodes are usually EC2 instances, Azure VMs, or GCP compute instances. In a homelab they can be Raspberry Pi boards. Kubernetes abstracts the individual machine away: you declare what you want to run and the scheduler decides where it lands.

kubectl get nodes
NAME           STATUS   ROLES           AGE   VERSION
node-1         Ready    control-plane   10d   v1.28.2
node-2         Ready    <none>          10d   v1.28.2
node-3         Ready    <none>          10d   v1.28.2

A node in NotReady status is one of the first things to check during an incident. When a node goes unhealthy, Kubernetes adds a node.kubernetes.io/not-ready taint and begins evicting pods after a configurable toleration window (300 seconds by default). Understanding this prevents confusion when pods suddenly appear on other nodes without anyone touching a deployment.

Node Pools

Multiple nodes with similar hardware profiles are grouped into node pools. This is how you run different workload types on appropriate hardware: a high-CPU pool for compute-intensive services, a high-memory pool for caches, spot instances for batch jobs. On EKS, GKE, and AKS this is managed at the node group level. The scheduler uses resource requests and node labels to decide which pool a pod lands on.

The practical consequence: if you deploy a pod without resource requests defined, the scheduler has no signal for placement and may put it on any node, including ones that are already under memory pressure.

Persistent Volumes: Where State Lives

Pods are ephemeral by design. When a pod is rescheduled to another node (because the original node had a failure, or the scheduler rebalanced the cluster), anything written to the container's local filesystem is gone. This is the correct behaviour for stateless workloads, but it is a problem for anything that needs to persist data.

Persistent Volumes (PVs) are storage resources that exist independently of any particular pod or node. In cloud environments they are usually backed by block storage (EBS on AWS, Azure Disk, GCP Persistent Disk). Your application requests storage through a PersistentVolumeClaim (PVC):

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: my-app-storage
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi

The accessModes field is one of the most common sources of confusion when getting started:

  • ReadWriteOnce (RWO): the volume can be mounted by one node at a time. This is what EBS gives you. If you have a Deployment with two replicas both trying to mount the same RWO volume, one of them will get stuck in ContainerCreating.
  • ReadWriteMany (RWX): the volume can be mounted by multiple nodes simultaneously. This requires a network filesystem like EFS on AWS or NFS. It is more expensive and slower than block storage.

In managed Kubernetes services like EKS, you need the EBS CSI Driver or EFS CSI Driver installed before PVCs will bind. Without the driver, pods sit in Pending with a driver not found event. This is easy to miss when first setting up a cluster.

Containers and Pods

Containers package an application and its dependencies into a portable image. Kubernetes does not schedule containers directly. It wraps one or more containers into a pod, which is the smallest schedulable unit in Kubernetes.

Containers in the same pod:

  • Share the same network namespace (they communicate over localhost)
  • Share the same storage volumes
  • Are always co-located on the same node
  • Scale together as a unit

The "one process per container" principle exists for a reason. If you pack unrelated processes into a single container, you lose the ability to scale or update them independently. The exception is the sidecar pattern: a main application container paired with a helper container that is tightly coupled to it, for example, a service mesh proxy (Envoy, Linkerd), a log shipper, or a secrets injector.

The sidecar pattern is worth knowing because it is how many production infrastructure tools integrate. Istio injects an Envoy proxy as a sidecar into every pod in a mesh-enabled namespace. Fluent Bit can run as a sidecar to ship application logs alongside the main process. These sidecars are defined in the same pod spec and share the pod's lifecycle.

What You Actually See in kubectl get pods

kubectl get pods -n production
NAME                          READY   STATUS    RESTARTS   AGE
api-server-7c5b8f4b9d-abc12   2/2     Running   0          2d
api-server-7c5b8f4b9d-def34   2/2     Running   0          2d
worker-6d4f9b8c7-ghi56        1/1     Running   3          5d

The 2/2 in READY means two containers are running in that pod: likely the application container and a sidecar. The 3 restarts on the worker pod is a signal worth investigating. A pod that restarts repeatedly usually has a crash loop caused by a missing environment variable, a failed health check, or an OOM kill.

Deployments: Declarative Pod Management

You rarely create pods directly. A Deployment wraps the pod spec and declares the desired number of replicas. The Deployment controller watches the cluster and ensures the actual state matches the desired state. If a pod crashes, the controller recreates it. If a node goes down, the controller reschedules the pod's replacement on a healthy node.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.27
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 500m
              memory: 256Mi

A few things an experienced operator pays attention to in a Deployment spec:

Always set resource requests and limits. Without requests, the scheduler has no signal for placement, the HPA cannot calculate utilization percentages, and a noisy pod can starve its neighbours on the same node. Without limits, a memory leak in one pod can OOM-kill the entire node.

The selector.matchLabels must match template.metadata.labels. This is the link between the Deployment and the pods it owns. If they do not match, the Deployment cannot track or manage its pods. Kubernetes enforces this at creation time but it is easy to create mismatches during manual edits.

Apply and verify:

kubectl apply -f nginx-deployment.yaml
kubectl get deployments
kubectl get pods -l app=nginx
NAME               READY   UP-TO-DATE   AVAILABLE   AGE
nginx-deployment   3/3     3            3           30s

If you delete a pod manually, the Deployment creates a replacement within seconds. If the cluster is under resource pressure and the new pod cannot be scheduled, it will stay in Pending. That is the Deployment doing its job: it keeps trying to reach the desired state.

Exposing Services: LoadBalancer vs Ingress

By default, pods are only reachable inside the cluster. To expose a service externally, you have two main options.

Load Balancer

A Service of type LoadBalancer provisions an external load balancer from the cloud provider. One load balancer per service, with a dedicated external IP.

apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  type: LoadBalancer
  selector:
    app: nginx
  ports:
    - port: 80
      targetPort: 80

This is the simplest approach and supports any protocol (TCP, UDP, gRPC, WebSockets). The trade-off: cloud load balancers are billed per hour and per GB of data processed. At ten services, the cost adds up. At fifty services, it becomes a significant line item.

Ingress Controller

An Ingress Controller (NGINX, Traefik, Kong) sits in front of multiple services and routes HTTP/HTTPS traffic based on hostname or path. One load balancer, shared across all services.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-ingress
spec:
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 80
    - host: app.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: frontend-service
                port:
                  number: 80

In production I almost always pair NGINX Ingress with cert-manager for automated TLS, and run two separate Ingress controller instances: one internet-facing for public services, and one internal for dashboards (Grafana, ArgoCD, Prometheus) that should never be publicly accessible.

The limitation: Ingress only handles HTTP and HTTPS. If you need to expose a TCP service (a database port, a gRPC endpoint on a non-standard port), you still need a LoadBalancer or a TCP proxy configured through the Ingress controller's custom annotations.

Summary

Concept What It Does What to Watch
Node A machine where pods run NotReady status means pods will be evicted
Node Pool Grouped nodes with similar hardware Always set resource requests to guide scheduling
Persistent Volume Storage independent of any pod or node Access mode must match your workload (RWO vs RWX)
Pod Wrapper around one or more containers READY column shows how many containers are running
Deployment Manages replica count and pod lifecycle Always set resource requests and limits
Load Balancer One LB per service, any protocol Cost scales with number of services
Ingress Controller Shared LB for HTTP/HTTPS with hostname/path routing Only for HTTP/HTTPS; pair with cert-manager for TLS

The patterns here are the foundation for everything else in Kubernetes. StatefulSets, DaemonSets, HPA, and scheduling constraints all build on top of these concepts. Getting the fundamentals solid means every more advanced topic is easier to reason about when something breaks in production.

Comments