Kubernetes manages containers at scale — it decides where they run, restarts them when they crash, and distributes traffic across replicas. But Kubernetes introduces a lot of abstractions all at once, which makes it hard to know where to start. In practice, almost everything you deploy touches three objects: Pods, Deployments, and Services. Understanding these three gets you 80% of the way there.

Pods: The Smallest Deployable Unit

A Pod is a wrapper around one or more containers that share the same network namespace and storage. Containers in the same Pod can talk to each other on localhost and share mounted volumes.

You rarely create Pods directly — they’re almost always managed by a Deployment. But it’s useful to see the raw spec:

apiVersion: v1
kind: Pod
metadata:
  name: api-pod
  labels:
    app: api
spec:
  containers:
    - name: api
      image: myapp:latest
      ports:
        - containerPort: 8000
      env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: url

Apply it:

$ kubectl apply -f pod.yaml
pod/api-pod created

$ kubectl get pods
NAME      READY   STATUS    RESTARTS   AGE
api-pod   1/1     Running   0          12s

$ kubectl describe pod api-pod
$ kubectl logs api-pod
$ kubectl exec -it api-pod -- sh

The problem with a bare Pod: if it crashes or the node it’s on goes down, it’s gone. Kubernetes won’t reschedule it. That’s Deployment’s job.

Deployments: Managing Replica Sets

A Deployment declares your desired state — “I want 3 replicas of this container” — and Kubernetes continuously works to make reality match it. If a Pod dies, a new one is created. If you push a new image, it rolls out gradually without downtime.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-deployment
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: myapp:1.4.2
          ports:
            - containerPort: 8000
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "500m"
              memory: "256Mi"
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8000
            initialDelaySeconds: 5
            periodSeconds: 10
$ kubectl apply -f deployment.yaml
deployment.apps/api-deployment created

$ kubectl get deployments
NAME             READY   UP-TO-DATE   AVAILABLE   AGE
api-deployment   3/3     3            3           30s

$ kubectl get pods
NAME                              READY   STATUS    RESTARTS   AGE
api-deployment-7d8f9c6b4-2kxmn   1/1     Running   0          30s
api-deployment-7d8f9c6b4-9pqr8   1/1     Running   0          30s
api-deployment-7d8f9c6b4-vbzt1   1/1     Running   0          30s

Rolling out a new image:

$ kubectl set image deployment/api-deployment api=myapp:1.4.3
deployment.apps/api-deployment image updated

$ kubectl rollout status deployment/api-deployment
Waiting for deployment "api-deployment" rollout to finish: 1 out of 3 new replicas have been updated...
deployment "api-deployment" successfully rolled out

# something went wrong? roll back
$ kubectl rollout undo deployment/api-deployment

The readinessProbe is important — Kubernetes won’t send traffic to a Pod until the probe passes, and it won’t kill old Pods until new ones are ready. This is what makes zero-downtime deploys work.

Services: Stable Network Endpoints

Pods are ephemeral. Their IP addresses change every time they’re rescheduled. A Service provides a stable virtual IP and DNS name that routes to matching Pods via label selectors.

apiVersion: v1
kind: Service
metadata:
  name: api-service
spec:
  selector:
    app: api
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8000
  type: ClusterIP
$ kubectl apply -f service.yaml
service/api-service created

$ kubectl get services
NAME          TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
api-service   ClusterIP   10.96.45.231   <none>        80/TCP    10s

Other Pods in the cluster can now reach your app at http://api-service (or http://api-service.default.svc.cluster.local with the full DNS name). The Service load-balances across all three replicas automatically.

Service Types

Type What it does
ClusterIP Internal only — default, no external access
NodePort Exposes the service on each node’s IP at a static port (30000–32767)
LoadBalancer Provisions a cloud load balancer (AWS ELB, GCP LB, etc.)
ExternalName Maps a service name to an external DNS name

For production apps, you’d typically use ClusterIP with an Ingress controller (like Nginx Ingress or Traefik) to manage external HTTP routing — rather than exposing a LoadBalancer per service.

Putting It Together

The typical flow for any app in Kubernetes:

Deployment → manages → ReplicaSet → manages → Pods
                                                 ↑
Service → selects Pods by label and routes traffic
# apply everything at once
$ kubectl apply -f deployment.yaml -f service.yaml

# check the full picture
$ kubectl get all -l app=api
NAME                                  READY   STATUS    RESTARTS   AGE
pod/api-deployment-7d8f9c6b4-2kxmn   1/1     Running   0          5m
pod/api-deployment-7d8f9c6b4-9pqr8   1/1     Running   0          5m
pod/api-deployment-7d8f9c6b4-vbzt1   1/1     Running   0          5m

NAME                             READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/api-deployment   3/3     3            3           5m

NAME                  TYPE        CLUSTER-IP     PORT(S)   AGE
service/api-service   ClusterIP   10.96.45.231   80/TCP    5m

Conclusion

Pods are where your containers actually run, but you almost never manage them directly. Deployments give you self-healing and rolling updates by maintaining a desired replica count. Services give your Pods a stable address so the rest of the cluster can find them regardless of which nodes the Pods happen to land on. These three primitives are the foundation of everything else in Kubernetes — ConfigMaps, Ingresses, StatefulSets, and Jobs all build on top of them.