Kubernetes for Developers: Pods, Services, and Ingress Without the Ops Overhead

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.