Kubernetes Without the Ops PhD
Kubernetes has a reputation for complexity, and honestly, it's earned. But most of that complexity lives in the cluster operations layer — networking plugins, storage drivers, control plane upgrades. As a developer deploying applications, you need to understand maybe 20% of Kubernetes to be productive.
This guide covers that 20%: the concepts and configurations you'll interact with daily.
Pods: The Smallest Deployable Unit
A Pod is one or more containers that share a network namespace and storage volumes. In practice, most pods run a single container. Multi-container pods are for sidecar patterns — a log collector, a service mesh proxy, or an init container that runs setup before the main app starts.
You rarely create pods directly. Instead, you create a Deployment (or StatefulSet, or DaemonSet) that manages pods for you. If a pod crashes, the Deployment controller replaces it automatically.
# deployment.yaml — the most common way to run a workload
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
labels:
app: api-server
spec:
replicas: 3
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
spec:
containers:
- name: api
image: myregistry/api-server:v1.4.2
ports:
- containerPort: 8080
resources:
requests:
cpu: "250m" # 0.25 CPU cores
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 15
periodSeconds: 20
Resource Requests and Limits
This trips up a lot of developers. Requests are what Kubernetes uses for scheduling — "this pod needs at least 250m CPU and 256Mi memory to function." The scheduler places the pod on a node with enough available resources. Limits are hard caps — if the pod exceeds its memory limit, it gets OOM-killed.
Set requests based on your application's actual steady-state usage. Set limits at 1.5-2x the request to handle traffic spikes. If you're not sure, deploy without limits, monitor actual usage with kubectl top pods for a few days, then set requests to the p50 and limits to the p99.
A pod without resource requests can be scheduled on any node, including one that's already overcommitted. It'll work until the node runs out of resources, at which point Kubernetes starts evicting pods — yours first, because it has no guarantees to respect.
Probes
Readiness probes tell Kubernetes when a pod is ready to receive traffic. Until the readiness probe passes, the pod is removed from Service endpoints. Use this for applications that need time to warm up (loading config, establishing database connections).
Liveness probes tell Kubernetes when a pod is stuck and needs to be restarted. If the liveness probe fails, Kubernetes kills the pod and creates a new one. Be careful with liveness probes — an overly aggressive probe on a pod that's just slow (not stuck) creates a restart loop.
I'd argue you should always set readiness probes and only add liveness probes for applications with known deadlock or hang scenarios. A pod that's slow but functional is better than one that's constantly restarting.
Services: How Pods Find Each Other
Pods are ephemeral. They get IP addresses, but those IPs change every time a pod restarts. Services provide a stable network identity in front of a set of pods.
apiVersion: v1
kind: Service
metadata:
name: api-server
spec:
selector:
app: api-server # Routes to pods with this label
ports:
- port: 80 # Service port
targetPort: 8080 # Container port
type: ClusterIP # Internal only
With this Service, any pod in the cluster can reach your API at http://api-server (or http://api-server.default.svc.cluster.local for the full DNS name). Kubernetes DNS resolves the service name to a virtual IP, and kube-proxy routes traffic to healthy pods behind the service.
Service Types
ClusterIP (default): Internal-only. Other pods can reach it; the outside world cannot. Use this for service-to-service communication.
NodePort: Exposes the service on a static port on every node. Traffic to any-node-ip:30080 gets routed to the service. Useful for development but not for production — you'd need to handle node IP changes and load balancing yourself.
LoadBalancer: Provisions a cloud provider load balancer (AWS ELB, GCP LB) that routes external traffic to the service. The simplest way to expose a service to the internet, but each LoadBalancer service creates a separate cloud LB, which gets expensive.
Ingress: HTTP Routing
Instead of creating a LoadBalancer service per application, use an Ingress controller. It's a single load balancer that routes HTTP traffic to different services based on hostname or URL path.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: app-ingress
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- api.example.com
secretName: api-tls-cert
rules:
- host: api.example.com
http:
paths:
- path: /v1
pathType: Prefix
backend:
service:
name: api-server-v1
port:
number: 80
- path: /v2
pathType: Prefix
backend:
service:
name: api-server-v2
port:
number: 80
The most popular Ingress controllers are nginx-ingress (open-source, battle-tested) and Traefik (automatic HTTPS, nice dashboard). For AWS, the AWS Load Balancer Controller creates ALBs from Ingress resources.
ConfigMaps and Secrets
Don't bake configuration into your container images. Use ConfigMaps for non-sensitive config and Secrets for credentials.
apiVersion: v1
kind: ConfigMap
metadata:
name: api-config
data:
DATABASE_HOST: "postgres.default.svc"
LOG_LEVEL: "info"
CACHE_TTL: "300"
---
apiVersion: v1
kind: Secret
metadata:
name: api-secrets
type: Opaque
stringData:
DATABASE_PASSWORD: "s3cur3-passw0rd"
API_KEY: "sk_live_abc123"
Mount them as environment variables in your deployment:
spec:
containers:
- name: api
envFrom:
- configMapRef:
name: api-config
- secretRef:
name: api-secrets
One important note: Kubernetes Secrets are base64-encoded, not encrypted. Anyone with access to the namespace can read them. For production, use an external secrets manager (AWS Secrets Manager, HashiCorp Vault) with the External Secrets Operator to sync secrets into Kubernetes.
Local Development
You don't need a full cluster to develop for Kubernetes. Here's what works:
minikube: Runs a single-node cluster locally. Good enough for testing deployments and services. minikube start and you've got a cluster in 2 minutes.
kind (Kubernetes in Docker): Runs Kubernetes nodes as Docker containers. Faster to create and destroy than minikube. I use this for CI testing — spin up a cluster, run integration tests, tear it down.
Skaffold: Watches your source code, rebuilds the container image, and redeploys to your local cluster on every change. Cuts the edit-build-deploy-test cycle from minutes to seconds. Pair it with minikube or kind for a smooth local dev workflow.
Tilt: Similar to Skaffold but with a web dashboard showing build/deploy status. Slightly more setup but better DX for complex multi-service projects.
My recommendation: start with minikube + Skaffold for local development, use kind for CI. Don't develop against a shared remote cluster — you'll step on your teammates' deployments.