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.
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.
You: "I want 3 instances of my API running at all times"
Kubernetes: "Got it" → starts 3 → one crashes → restarts it automaticallyThe Core Objects
Pod
The smallest unit in Kubernetes. A pod runs one or more containers. Containers in the same pod share network and storage.
# 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.
# 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: 8000kubectl 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.3The 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.
# 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.
# From another service in the cluster
import requests
response = requests.get("http://my-api/health") # Kubernetes DNS resolves thisService types:
ClusterIP: Internal only (default)NodePort: Exposes on a port on every nodeLoadBalancer: 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.
# 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: 80You need an Ingress Controller (like nginx-ingress or traefik) installed in the cluster.
ConfigMaps and Secrets
# 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://...")# 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: urlNever hardcode secrets in your image. Always use Kubernetes Secrets (or an external vault like HashiCorp Vault, AWS Secrets Manager).
Health Checks
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: 5livenessProbe: 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
# 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: 8000Essential kubectl Commands
# 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-apiKey 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/describeare 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.
Related Posts
Continue reading with these related posts
Vertical vs Horizontal Scaling: When to Use Each
Learn the difference between vertical and horizontal scaling. Understand trade-offs, real costs, and when each strategy makes sense for your system.
Reverse Proxy Explained: Nginx, Load Balancing, and More
Learn what a reverse proxy is, how it differs from a forward proxy, and why every production backend uses one. Covers Nginx, SSL termination, and load balancing.
Service Discovery: How Microservices Find Each Other
Learn how service discovery works in microservices. Covers client-side vs server-side discovery, Consul, etcd, and Kubernetes DNS with practical examples.