← All TILs · kubernetes

Pods, Deployments, Services — the minimum object model

kubernetes - 2026-09-18

Fifth entry in the RKE2/Kubernetes series. kubectl has been reachable from off the node since the previous entry; this is the first one to actually point it at something — the three objects that account for almost everything you'll run day to day, and one RKE2-specific gap between "the API server answers" and "traffic actually reaches your Pod."

Pod: the unit that isn't self-healing on its own

A Pod is one or more containers sharing a network namespace — same IP, localhost works between them. The detail worth sitting with: a bare Pod is not self-healing. Its restartPolicy (Always, OnFailure, Never) only governs what happens when a container inside it exits — nothing about it recreates the Pod itself if it's deleted or its node dies. Per Kubernetes' own Pod docs: "usually you don't need to create Pods directly, even singleton Pods... instead, create them using workload resources."

kubectl run nginx-standalone --image=nginx --restart=Never
kubectl delete pod nginx-standalone
kubectl get pods
# No resources found -- nothing brought it back

That's the entire reason Deployments exist.

Deployment: you talk to it, it talks to a ReplicaSet, that owns the Pods

A Deployment doesn't own Pods directly. Per Kubernetes' own Deployment docs, the actual chain is Deployment → ReplicaSet → Pods: the Deployment creates and manages a ReplicaSet, and the ReplicaSet is what actually watches the Pod count and creates or removes Pods to match it. Updating the Deployment's Pod template doesn't edit existing Pods in place — it creates a new ReplicaSet and scales it up while scaling the old one down, which is what makes a rolling update a rolling update rather than a hard cutover.

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
kubectl apply -f nginx-deployment.yaml
kubectl get rs                    # the ReplicaSet the Deployment created
kubectl get pods -l app=nginx     # the 3 Pods that ReplicaSet is keeping alive

Delete one of those Pods and, unlike the standalone one above, it comes back within seconds — the ReplicaSet noticed the count drop below 3 and created a replacement. That's the same reconcile loop from the fundamentals entry, just watching a different object.

Nothing above says which node any of those three Pods runs on, and nothing needs to: a Deployment names no node, and kube-scheduler decides. On the single-node lab there's only one answer, which is exactly why it's easy to never think about — the scheduling entry later in this series covers how that choice is actually made, and why on RKE2 the servers themselves are among the candidates.

Service: a stable address for Pods whose IPs keep changing

Every Pod created above got its own IP, and that IP is gone the moment the Pod is replaced. A Service is a stable ClusterIP address in front of a set of Pods, chosen entirely by a label selector — no direct reference to any Pod's name or IP.

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

Per Kubernetes' own Service docs, the Service's controller continuously scans for Pods matching selector: app: nginx and keeps an EndpointSlice object updated with whichever Pod IPs currently qualify — kube-proxy, on every node, is what turns that EndpointSlice into actual routing rules. ClusterIP (this example) is the default type when none is set, and it's deliberately internal-only: reachable from any Pod or node in the cluster, not from outside it.

flowchart TB
    subgraph Deploy["Deployment: nginx, replicas 3"]
        RS[ReplicaSet]
    end
    RS --> POD1["Pod: app=nginx"]
    RS --> POD2["Pod: app=nginx"]
    RS --> POD3["Pod: app=nginx"]
    SVC["Service: selector app=nginx"] -.->|label match, via EndpointSlice| POD1
    SVC -.-> POD2
    SVC -.-> POD3
    CLIENT[Client inside the cluster] --> SVC

The RKE2-specific gap: ClusterIP reachable ≠ reachable from your workstation

This is a different problem from the 127.0.0.1 kubeconfig issue two entries back — that one was about reaching the API server; this one is about reaching a Service, and fixing the first doesn't touch the second at all. Two other Service types exist for exactly this:

For the single-node lab from this series, NodePort needs nothing extra; LoadBalancer needs that flag set before it'll do anything at all.

Where this series goes next

Everything above assumed pod-to-pod networking already works — the next entry looks at what actually provides that: RKE2's default CNI, Canal, and what changes if you swap it for something else.

Created 2026-09-18T02:19:17+02:00, updated 2026-09-20T20:58:29+02:00 · History · Edit