system design

Kubernetes Explained for Developers: Pods, Services, and Deployments

Learn Kubernetes fundamentals as a developer. Covers pods, deployments, services, ingress, and how to deploy your first app with practical kubectl examples.

By Akash Sharma·6 min read
#kubernetes
#k8s
#devops
#containers
#system design
#backend
#infrastructure

Kubernetes feels overwhelming at first. Pods, services, deployments, ingress, namespaces — too many concepts.

Here's the mental model that makes it click.

Why Kubernetes Exists

You have a Docker container running your app. If it crashes, it's gone. If you need 10 copies, you run 10 manually. If a server dies, you manually restart things.

Kubernetes automates this. You declare the desired state ("run 3 copies of this container"), and Kubernetes makes it happen — and keeps it that way.

plaintext
You: "I want 3 instances of my API running at all times"
Kubernetes: "Got it" → starts 3 → one crashes → restarts it automatically

The Core Objects

Pod

The smallest unit in Kubernetes. A pod runs one or more containers. Containers in the same pod share network and storage.

yaml
# pod.yaml — rarely created directly, use Deployments instead
apiVersion: v1
kind: Pod
metadata:
  name: my-api
spec:
  containers:
  - name: api
    image: myapp:v1.2
    ports:
    - containerPort: 8000
    env:
    - name: DATABASE_URL
      valueFrom:
        secretKeyRef:
          name: db-secret
          key: url
    resources:
      requests:
        memory: "128Mi"
        cpu: "250m"
      limits:
        memory: "256Mi"
        cpu: "500m"

Pods are ephemeral — they die and get replaced. They don't have stable IPs.

Deployment

A Deployment manages pods. It ensures the desired number of pods are running. If a pod crashes, the Deployment creates a new one.

yaml
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-api
spec:
  replicas: 3  # Run 3 pods
  selector:
    matchLabels:
      app: my-api
  template:         # Pod template
    metadata:
      labels:
        app: my-api
    spec:
      containers:
      - name: api
        image: myapp:v1.2
        ports:
        - containerPort: 8000
bash
kubectl apply -f deployment.yaml
 
# See pods running
kubectl get pods
# NAME                      READY   STATUS    RESTARTS
# my-api-5d4b8b-abc12       1/1     Running   0
# my-api-5d4b8b-def34       1/1     Running   0
# my-api-5d4b8b-ghi56       1/1     Running   0
 
# Scale up
kubectl scale deployment my-api --replicas=5
 
# Rolling update (zero downtime)
kubectl set image deployment/my-api api=myapp:v1.3

The Deployment handles rolling updates — gradually replaces old pods with new ones without downtime.

Service

Pods have random IPs that change when pods restart. A Service gives pods a stable DNS name and IP.

yaml
# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: my-api
spec:
  selector:
    app: my-api  # Routes to pods with this label
  ports:
  - port: 80         # Service port
    targetPort: 8000  # Pod port
  type: ClusterIP    # Internal only (default)

Now any other pod in the cluster calls http://my-api — Kubernetes load balances across all 3 pods.

python
# From another service in the cluster
import requests
response = requests.get("http://my-api/health")  # Kubernetes DNS resolves this

Service types:

  • ClusterIP: Internal only (default)
  • NodePort: Exposes on a port on every node
  • LoadBalancer: Creates a cloud load balancer (AWS ALB, GCP GLB) — for external traffic

Ingress

Ingress routes external HTTP traffic to services based on host and path.

yaml
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /api
        pathType: Prefix
        backend:
          service:
            name: my-api
            port:
              number: 80
      - path: /docs
        pathType: Prefix
        backend:
          service:
            name: docs-service
            port:
              number: 80

You need an Ingress Controller (like nginx-ingress or traefik) installed in the cluster.

ConfigMaps and Secrets

yaml
# ConfigMap: non-sensitive config
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  LOG_LEVEL: "info"
  CACHE_TTL: "300"
  FEATURE_FLAGS: "new_ui=true"
 
---
# Secret: sensitive data (base64 encoded, encrypted at rest)
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
data:
  url: cG9zdGdyZXNxbDovLy4uLg==  # base64("postgresql://...")
yaml
# Use in a deployment
env:
- name: LOG_LEVEL
  valueFrom:
    configMapKeyRef:
      name: app-config
      key: LOG_LEVEL
- name: DATABASE_URL
  valueFrom:
    secretKeyRef:
      name: db-secret
      key: url

Never hardcode secrets in your image. Always use Kubernetes Secrets (or an external vault like HashiCorp Vault, AWS Secrets Manager).

Health Checks

yaml
containers:
- name: api
  image: myapp:v1.2
  livenessProbe:
    # If this fails: restart the pod
    httpGet:
      path: /health
      port: 8000
    initialDelaySeconds: 10
    periodSeconds: 10
    failureThreshold: 3
  
  readinessProbe:
    # If this fails: remove pod from Service (stop sending traffic)
    httpGet:
      path: /ready
      port: 8000
    initialDelaySeconds: 5
    periodSeconds: 5

livenessProbe: Is the pod alive? Failed → restart. readinessProbe: Is the pod ready to serve traffic? Failed → remove from load balancer rotation.

During a deployment, new pods only receive traffic after passing readiness checks. No requests to pods still starting up.

A Complete Deployment

yaml
# full-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-api
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1       # Allow 1 extra pod during update
      maxUnavailable: 0  # Never have fewer than desired pods
  selector:
    matchLabels:
      app: my-api
  template:
    metadata:
      labels:
        app: my-api
    spec:
      containers:
      - name: api
        image: myapp:v1.2
        ports:
        - containerPort: 8000
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: url
        resources:
          requests:
            memory: "128Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "1"
        livenessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 10
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 8000
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: my-api
spec:
  selector:
    app: my-api
  ports:
  - port: 80
    targetPort: 8000

Essential kubectl Commands

bash
# Apply config
kubectl apply -f deployment.yaml
 
# See everything
kubectl get pods,deployments,services
 
# See logs
kubectl logs my-api-5d4b8b-abc12
kubectl logs -l app=my-api --tail=100  # All pods with this label
 
# Exec into a pod
kubectl exec -it my-api-5d4b8b-abc12 -- /bin/bash
 
# See events (good for debugging)
kubectl describe pod my-api-5d4b8b-abc12
 
# Rolling update
kubectl set image deployment/my-api api=myapp:v1.3
 
# Rollback
kubectl rollout undo deployment/my-api
 
# Check rollout status
kubectl rollout status deployment/my-api
 
# Delete
kubectl delete deployment my-api

Key Takeaways

  • Pods run your containers — ephemeral, no stable IP
  • Deployments manage pods — desired replicas, rolling updates, self-healing
  • Services give pods a stable DNS name and load balance across them
  • Ingress routes external HTTP traffic to services based on host/path
  • ConfigMaps for config, Secrets for sensitive values — never bake them into images
  • Liveness probe: restart if unhealthy; Readiness probe: stop traffic if not ready
  • kubectl apply + kubectl get/logs/describe are your main tools

Kubernetes is complex infrastructure to run yourself. On AWS use EKS, on GCP use GKE, on Azure use AKS — managed control planes remove the hardest part.

Related reading: Service Discovery Explained · Microservices vs Monolith · Reverse Proxy Explained

Enjoyed this article?

Get weekly insights on backend architecture, system design, and Go programming.