Skip to content
Codeloom
Kubernetes

Kubernetes Pods, Deployments, and Services

A hands-on tour of the three Kubernetes objects you will use every day — pods, deployments, and services — with real YAML, the kubectl commands to apply them, and how they fit together.

·9 min read · By Codeloom
Intermediate 13 min read

What you'll learn

  • How to write and apply a Pod YAML — and why you rarely do
  • How a Deployment manages replicas and performs rolling updates
  • The three Service types: ClusterIP, NodePort, and LoadBalancer
  • The everyday kubectl commands: apply, get, describe, logs, exec
  • How namespaces partition a cluster

Prerequisites

  • A mental model of Kubernetes — see What Is Kubernetes?
  • A local cluster (Minikube, kind, k3d, or Docker Desktop with K8s enabled)

The previous post introduced the vocabulary. This one gets your hands dirty: real YAML, real kubectl commands, and a small app you can deploy, scale, update, and tear down. By the end you will have the muscle memory to read most production Kubernetes manifests.

Set the scene

You need a local cluster and kubectl configured to talk to it. Any of these will do:

# Minikube
minikube start

# kind
kind create cluster

# k3d
k3d cluster create demo

# Docker Desktop — enable Kubernetes in Settings

Then verify:

kubectl get nodes
# NAME       STATUS   ROLES           AGE   VERSION
# minikube   Ready    control-plane   1m    v1.30.0

One node is enough for everything below.

A pod, the smallest unit

A pod wraps one or more containers that share a network and storage. In 95% of cases there is exactly one container per pod, so it is fine to think of a pod as “a running container with extra Kubernetes metadata.”

Create pod.yaml:

apiVersion: v1
kind: Pod
metadata:
  name: hello-pod
  labels:
    app: hello
spec:
  containers:
    - name: web
      image: nginx:1.27-alpine
      ports:
        - containerPort: 80

Apply it:

kubectl apply -f pod.yaml
# pod/hello-pod created

kubectl get pods
# NAME        READY   STATUS    RESTARTS   AGE
# hello-pod   1/1     Running   0          10s

The pod is alive. Look at it more closely:

kubectl describe pod hello-pod
kubectl logs hello-pod

describe dumps everything Kubernetes knows about the pod — events, IP address, container state. logs shows stdout/stderr from the container, same as docker logs.

Now delete it:

kubectl delete pod hello-pod
# pod "hello-pod" deleted

And here is the catch: the pod is gone forever. Nothing brings it back. Bare pods are not self-healing. That is why you almost never deploy a bare pod — you deploy a Deployment that manages pods on your behalf.

A Deployment, the everyday object

A Deployment says “I want N copies of this pod, and I want updates to roll out gradually.” It owns a ReplicaSet which owns the pods.

Create deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello
spec:
  replicas: 3
  selector:
    matchLabels:
      app: hello
  template:
    metadata:
      labels:
        app: hello
    spec:
      containers:
        - name: web
          image: nginx:1.27-alpine
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: "50m"
              memory: "64Mi"
            limits:
              cpu: "200m"
              memory: "128Mi"

Apply and inspect:

kubectl apply -f deployment.yaml
# deployment.apps/hello created

kubectl get deployments
# NAME    READY   UP-TO-DATE   AVAILABLE   AGE
# hello   3/3     3            3           20s

kubectl get pods
# NAME                     READY   STATUS    RESTARTS   AGE
# hello-6d9c7f8b8d-abcde   1/1     Running   0          20s
# hello-6d9c7f8b8d-fghij   1/1     Running   0          20s
# hello-6d9c7f8b8d-klmno   1/1     Running   0          20s

Three pods, with random suffixes appended to the deployment name. Now delete one by hand:

kubectl delete pod hello-6d9c7f8b8d-abcde
kubectl get pods

Within seconds, a new pod with a new suffix appears. The Deployment notices the missing replica and recreates it. That is self-healing.

Scaling

You can change the replica count without editing YAML:

kubectl scale deployment hello --replicas=5
kubectl get pods
# 5 pods running

Or edit the YAML, change replicas: 3 to replicas: 5, and re-apply. Either way works — the second is preferred because it keeps Git as the source of truth.

Rolling updates

Change the image tag in deployment.yaml:

        image: nginx:1.27-alpine   # change to nginx:1.28-alpine

Apply again:

kubectl apply -f deployment.yaml
kubectl rollout status deployment/hello
# deployment "hello" successfully rolled out

Kubernetes brings up new pods running 1.28, waits for them to be ready, then terminates old 1.27 pods — one or two at a time. There is no downtime. If something goes wrong:

kubectl rollout undo deployment/hello

instantly reverts to the previous version.

Try it yourself. With the deployment running, run watch kubectl get pods in one terminal. In another, change the image tag and re-apply. Watch new pods appear, become ready, and old pods disappear. Then run kubectl rollout undo and watch the reverse happen.

A Service, the stable address

Pods have IP addresses but those IPs change every time a pod is recreated. A Service is a stable virtual address that automatically load-balances across whichever pods match its selector.

There are three Service types you will use.

ClusterIP — inside the cluster only

The default. The service is reachable only from inside the cluster. Use it for internal communication (web → database, web → cache).

Create service-clusterip.yaml:

apiVersion: v1
kind: Service
metadata:
  name: hello
spec:
  type: ClusterIP
  selector:
    app: hello
  ports:
    - port: 80
      targetPort: 80

Apply:

kubectl apply -f service-clusterip.yaml
kubectl get service hello
# NAME    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
# hello   ClusterIP   10.96.123.45    <none>        80/TCP    5s

From any pod in the cluster, http://hello (yes, just the service name — Kubernetes provides DNS) routes to one of the three nginx pods.

NodePort — exposed on every node

Opens a high-numbered port (30000–32767) on every node in the cluster. Useful for local development and quick demos:

apiVersion: v1
kind: Service
metadata:
  name: hello-np
spec:
  type: NodePort
  selector:
    app: hello
  ports:
    - port: 80
      targetPort: 80
      nodePort: 30080

Apply and hit it from your laptop:

kubectl apply -f service-nodeport.yaml

# On Minikube
minikube service hello-np --url

# On Docker Desktop / kind / k3d
curl http://localhost:30080

NodePort is great for learning, less so for production — you typically do not want random high-numbered ports facing the internet.

LoadBalancer — a real public IP

Asks the cloud provider for a managed load balancer with a public IP. On AWS, this provisions an ELB; on GKE, a Google load balancer.

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

On a real cloud cluster this would get a public IP within a minute or two. On a local cluster, EXTERNAL-IP stays <pending> because there is no cloud LB to provision.

In production, most teams use a single LoadBalancer in front of an Ingress controller, then route many services through it with Ingress rules — but that is a topic for another post.

How the pieces fit

Picture three layers stacked on top of each other:

  Service       (stable address: hello)
     |
     v   (selector: app=hello)
   Pods       (3 replicas, ephemeral IPs)
     |
     v
 Deployment   (desired: 3 replicas, image: nginx:1.27)

The Deployment creates and replaces pods. The Service routes traffic to whichever pods happen to be alive and match its label selector. Pods themselves are disposable — you can delete them at will and the Deployment makes more.

This separation is why Kubernetes is so flexible: you can update the Deployment without touching the Service, scale pods up and down without changing addresses, and so on.

Namespaces, briefly

Everything above lives in a namespace — by default, the one called default. Namespaces partition a cluster so different teams or environments do not collide:

kubectl create namespace staging
kubectl apply -f deployment.yaml -n staging
kubectl get pods -n staging

Inside a namespace, names must be unique. Across namespaces, they can repeat. A service called hello in staging is reachable as hello.staging.svc.cluster.local from anywhere in the cluster.

For local learning, the default namespace is fine. For production, you typically separate by environment (prod, staging) or by team.

The everyday kubectl kit

You will use these commands constantly. Worth committing to memory:

# Apply YAML
kubectl apply -f file.yaml
kubectl apply -f .                 # all YAML in current directory

# List things
kubectl get pods
kubectl get pods -o wide           # include node and IP columns
kubectl get all                    # everything in the namespace

# Inspect
kubectl describe pod <name>
kubectl describe deployment <name>

# Logs
kubectl logs <pod>
kubectl logs -f <pod>              # follow, like tail -f
kubectl logs <pod> -c <container>  # specific container in a multi-container pod

# Exec into a pod
kubectl exec -it <pod> -- sh

# Delete
kubectl delete -f file.yaml
kubectl delete pod <name>

# Rollouts
kubectl rollout status deployment/<name>
kubectl rollout undo deployment/<name>
kubectl rollout history deployment/<name>

The -f flag takes a file or a directory; -n selects a namespace; -o yaml or -o json returns full machine-readable output. Once those flags are reflex, navigating Kubernetes feels less alien.

Try it yourself. Apply the deployment and the ClusterIP service. Then kubectl exec into one of the pods and run wget -qO- http://hello. You should see the nginx welcome page — confirmation that the service is correctly load-balancing across pods inside the cluster.

Cleanup

When you are done experimenting:

kubectl delete -f .
# or delete everything labelled app=hello
kubectl delete all -l app=hello

And destroy the local cluster if you no longer need it:

minikube delete       # or kind delete cluster / k3d cluster delete demo

Recap

You now know how to:

  • Write and apply a Pod — and why you rarely do, because bare pods are not self-healing
  • Use a Deployment for replica management, rolling updates, and kubectl rollout undo
  • Choose between ClusterIP, NodePort, and LoadBalancer services
  • Read state with get and describe, debug with logs and exec
  • Partition a cluster with namespaces

That is the working core of Kubernetes for an application developer.

Next steps

So far this series has been on-premises agnostic — everything runs on your laptop or any cluster. Real production usually lives in a cloud, and on the internet, “the cloud” overwhelmingly means AWS. The next post zooms out to introduce AWS as a mental model.

→ Next: What Is AWS? A Practical Introduction

Questions or feedback? Email codeloomdevv@gmail.com.