DevOpsKubernetes

Kubernetes Pods, Deployments & Services: Deploy and Expose Your App

Learn how Kubernetes Pods, Deployments, and Services work. Write YAML manifests, deploy a containerised application, expose it with a Service, and perform rolling updates.

TT
Daniel Brooks
6 min read
Kubernetes Pods, Deployments & Services: Deploy and Expose Your App

Understanding Pods, Deployments, and Services is the practical core of Kubernetes. Pods run your containers. Deployments manage Pods at scale. Services expose Pods to network traffic — from other Pods inside the cluster or from the outside world.

By the end of this lesson you'll have a containerised application running in Kubernetes, exposed via a Service, and updated with zero downtime.


Pods in Practice

A Pod is the smallest deployable unit in Kubernetes. You rarely create Pods directly — Deployments manage them for you — but understanding Pod manifests is essential because Deployment templates embed them.

A Minimal Pod Manifest

yaml
apiVersion: v1
kind: Pod
metadata:
  name: my-api-pod
  labels:
    app: my-api
spec:
  containers:
    - name: api
      image: myregistry/myapp:1.0
      ports:
        - containerPort: 3000
      resources:
        requests:
          memory: "64Mi"
          cpu: "100m"
        limits:
          memory: "128Mi"
          cpu: "500m"

Apply it:

bash
kubectl apply -f pod.yaml
kubectl get pods
kubectl describe pod my-api-pod

Resource Requests and Limits

Always set resource requests and limits on production containers:

  • Requests: The minimum resources the container needs. The scheduler uses this to decide which node can run the Pod.
  • Limits: The maximum the container can use. Kubernetes kills containers that exceed their memory limit.
yaml
resources:
  requests:
    memory: "128Mi"    # 128 mebibytes
    cpu: "250m"        # 250 millicores = 0.25 CPU cores
  limits:
    memory: "256Mi"
    cpu: "1000m"       # 1 full CPU core

Liveness and Readiness Probes

yaml
livenessProbe:
  httpGet:
    path: /health
    port: 3000
  initialDelaySeconds: 10
  periodSeconds: 30
  failureThreshold: 3

readinessProbe:
  httpGet:
    path: /ready
    port: 3000
  initialDelaySeconds: 5
  periodSeconds: 10
  • Liveness probe: If this fails repeatedly, Kubernetes restarts the container. Use for detecting deadlocks or crashed processes.
  • Readiness probe: If this fails, Kubernetes removes the Pod from the Service's endpoint list — no traffic is sent to it. Use for detecting when the app is still initialising.

Deployments

A Deployment manages a set of identical Pod replicas and handles rolling updates. It's what you'll use for stateless application workloads.

A Production-Ready Deployment Manifest

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-api
  namespace: default
  labels:
    app: my-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-api
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1
  template:
    metadata:
      labels:
        app: my-api
    spec:
      containers:
        - name: api
          image: myregistry/myapp:1.0
          ports:
            - containerPort: 3000
          env:
            - name: NODE_ENV
              value: "production"
            - name: PORT
              value: "3000"
          resources:
            requests:
              memory: "128Mi"
              cpu: "250m"
            limits:
              memory: "256Mi"
              cpu: "1000m"
          livenessProbe:
            httpGet:
              path: /health
              port: 3000
            initialDelaySeconds: 10
            periodSeconds: 30
          readinessProbe:
            httpGet:
              path: /ready
              port: 3000
            initialDelaySeconds: 5
            periodSeconds: 10

Apply and verify:

bash
kubectl apply -f deployment.yaml
kubectl get deployments
kubectl get pods
kubectl rollout status deployment/my-api

The selector and template.labels Must Match

The selector.matchLabels tells the Deployment which Pods it owns. The template.metadata.labels are applied to the Pods it creates. They must match — otherwise the Deployment can't track its Pods.

Rolling Updates

With strategy: RollingUpdate, maxUnavailable: 1 means at most 1 Pod is unavailable during the update, and maxSurge: 1 means at most 1 extra Pod is created above the desired count. Kubernetes terminates old Pods and creates new ones gradually, ensuring the application stays available throughout.

To update to a new image version:

bash
kubectl set image deployment/my-api api=myregistry/myapp:2.0
kubectl rollout status deployment/my-api

Or update the YAML and kubectl apply -f deployment.yaml — the preferred GitOps approach.

Rollback

bash
kubectl rollout undo deployment/my-api
# or rollback to a specific revision:
kubectl rollout undo deployment/my-api --to-revision=2
kubectl rollout history deployment/my-api

Services

Pods have IP addresses, but those IPs change every time a Pod is recreated. A Service provides a stable DNS name and IP that always routes to healthy Pods.

Service Types

TypeDescription
ClusterIPDefault. Internal IP only — accessible within the cluster
NodePortExposes the service on each node's IP at a static port (30000–32767)
LoadBalancerProvisions a cloud load balancer (AWS ELB, GCP LB, etc.)
ExternalNameMaps to an external DNS name (no proxying)

ClusterIP Service (Internal)

yaml
apiVersion: v1
kind: Service
metadata:
  name: my-api-service
spec:
  selector:
    app: my-api
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000
  type: ClusterIP

Other Pods in the cluster can now reach your API at http://my-api-service:80.

NodePort Service (External Access Without Cloud LB)

yaml
apiVersion: v1
kind: Service
metadata:
  name: my-api-nodeport
spec:
  selector:
    app: my-api
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000
      nodePort: 30080
  type: NodePort

Access the API at http://<NODE-IP>:30080. Useful for local development with minikube:

bash
minikube service my-api-nodeport --url

LoadBalancer Service (Cloud)

yaml
apiVersion: v1
kind: Service
metadata:
  name: my-api-lb
spec:
  selector:
    app: my-api
  ports:
    - protocol: TCP
      port: 80
      targetPort: 3000
  type: LoadBalancer

On AWS/GCP/Azure, this automatically provisions a cloud load balancer and assigns an external IP:

bash
kubectl get service my-api-lb
# Watch EXTERNAL-IP column until it's assigned

How Services Find Pods: Labels and Selectors

The magic that connects Services to Pods is labels and selectors. Pods have labels (key-value metadata) and Services have selector (which Pods to route to). When the Service receives traffic, it routes to any Pod matching all selector labels that is Ready (passing its readiness probe).

yaml
# Pod labels
metadata:
  labels:
    app: my-api

# Service selector — must match
spec:
  selector:
    app: my-api

Full Deploy + Expose Workflow

bash
# 1. Apply Deployment
kubectl apply -f deployment.yaml
kubectl rollout status deployment/my-api

# 2. Apply Service
kubectl apply -f service.yaml
kubectl get services

# 3. Test locally with port-forward
kubectl port-forward service/my-api-service 3000:80
curl http://localhost:3000/health

# 4. Scale up
kubectl scale deployment/my-api --replicas=5
kubectl get pods

# 5. Update image
kubectl set image deployment/my-api api=myregistry/myapp:2.0
kubectl rollout status deployment/my-api

# 6. Roll back if needed
kubectl rollout undo deployment/my-api

Previous: Lesson 7 — Kubernetes Architecture | Next: Lesson 9 — ConfigMaps & Secrets


Part of the Docker & Kubernetes Mastery course.

External references: