Pods, Deployments and Services
The core objects, the reconciliation loop, labels and selectors, and how a request finds a pod.
Kubernetes is a control loop: you write down the state you want, in YAML, and controllers work continuously to make the cluster match it. Everything else — pods, deployments, services — is the vocabulary for describing that state. This lesson is the four objects a backend service is made of and what each one is actually for, because most of the confusion in the first month comes from mistaking one for another.
Pods: the unit that runs
A pod is one or more containers that share a network namespace (one IP, one localhost) and can share volumes. It is the smallest thing Kubernetes schedules, and it is disposable: a pod that dies is not restarted, it is replaced, with a new name and a new IP. That is the property to internalise before anything else, because it rules out every design that remembers a pod — a sticky session, a local cache that matters, a file written to the pod's disk.
Most pods have one container. The second container, when there is one, is a sidecar: a log shipper reading the application's files, a proxy handling TLS, a Debezium-style agent — something that must share the pod's network or disk. An init container runs to completion before the main one starts: a migration, a chown on a volume, a wait for a dependency.
Deployments and ReplicaSets: how many, and which version
You almost never create a pod directly. A Deployment says "three replicas of this pod template", and its controller creates a ReplicaSet that keeps three running — a pod dies, the ReplicaSet makes another; a node dies, its pods are rescheduled elsewhere:
apiVersion: apps/v1
kind: Deployment
metadata: { name: orders-api }
spec:
replicas: 3
selector: { matchLabels: { app: orders-api } }
template:
metadata: { labels: { app: orders-api, version: "1.14.2" } }
spec:
containers:
- name: api
image: registry.example.com/orders-api:1.14.2
ports: [{ containerPort: 8080 }]Change the image tag and the Deployment creates a new ReplicaSet for the new template and scales the old one down as the new one comes up — the rolling update the rollouts lesson covers. The old ReplicaSet stays at zero replicas, which is what kubectl rollout undo scales back up. So a Deployment is a version history of ReplicaSets, and a ReplicaSet is a pod count; neither is the thing traffic reaches.
A StatefulSet is the sibling for things that do need a stable identity — postgres-0, kafka-1 — with a persistent volume per replica and ordered start-up. A DaemonSet runs one pod per node (log collectors, node agents). A Job runs to completion; a CronJob runs one on a schedule. A Spring Boot API is a Deployment; the database it talks to is usually outside the cluster or a StatefulSet run by an operator, and the Docker course's warning about databases on disposable disks applies doubly here.
Services: a stable name for a moving set
Pods have IPs that change; a Service is the fixed address in front of them. It selects pods by label and keeps an endpoint list of the ones that are ready:
apiVersion: v1
kind: Service
metadata: { name: orders-api }
spec:
selector: { app: orders-api }
ports: [{ port: 80, targetPort: 8080 }]Inside the cluster, http://orders-api (or orders-api.<namespace>.svc.cluster.local) resolves through cluster DNS to the Service's ClusterIP, and kube-proxy load-balances connections across the endpoints. That is the whole of service discovery for most systems, and the microservices course's discovery lesson said so: in Kubernetes it is DNS. Two things it does not do, both from the gRPC lesson: it balances connections, not requests, so one long-lived HTTP/2 connection goes to one pod; and a headless Service (clusterIP: None) returns the pod IPs instead, for clients that balance themselves.
type: NodePort opens a port on every node; type: LoadBalancer asks the cloud for an external load balancer pointed at those. Neither is how a web service is usually exposed, because each costs a load balancer per Service; that is what Ingress is for.
Ingress: HTTP from outside, routed by host and path
An Ingress is a routing table for HTTP — this host and path go to that Service — implemented by an ingress controller (nginx, Traefik, the cloud's own) that is itself a Deployment behind one LoadBalancer Service:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: code10x
annotations: { nginx.ingress.kubernetes.io/proxy-body-size: "8m" }
spec:
tls: [{ hosts: ["code10x.in"], secretName: code10x-tls }]
rules:
- host: code10x.in
http:
paths:
- path: /api
pathType: Prefix
backend: { service: { name: orders-api, port: { number: 80 } } }
- path: /
pathType: Prefix
backend: { service: { name: site, port: { number: 80 } } }That is this repository's nginx configuration — /api to the Java service, everything else to the static site, TLS at the edge, a body-size limit on uploads — written as an object the cluster manages instead of a file on a host. The annotations are where the controller's specifics live (timeouts, buffering, forwarded headers, the things the nginx lesson covers), which is also the trap: annotations are per controller and do not port. The Gateway API is the newer, portable version of the same idea and is where new clusters are heading.
Labels: the glue
Every relationship above is a label selector: the Deployment finds its pods by app: orders-api, the Service finds its endpoints the same way, and kubectl get pods -l app=orders-api finds them for you. Labels are free-form key-value pairs, and a consistent set — app, version, team, environment — is what makes a cluster navigable and what makes the observability course's dashboards possible, because the metrics and logs carry the same labels. Annotations are the other kind of metadata: for tools, not for selection.
Namespaces partition a cluster — orders, payments, monitoring — with their own names, quotas and access rules; a Service is orders-api in its namespace and orders-api.orders from another.