Ticker

6/recent/ticker-posts

Kubernetes for Developers: Container Orchestration Explained (Full Guide)



You've containerized your application with Docker. It runs perfectly on your laptop. But when you try to deploy it to production, you realize: running containers at scale is an entirely different challenge.

This guide walks you through Kubernetes—the industry-standard container orchestration platform—from core concepts to production deployment. Whether you're migrating from Docker Compose or building cloud-native applications from scratch, you'll learn what Kubernetes actually does, when you need it, and how to avoid the mistakes that lead to downtime and cost overruns.

1. The Container Orchestration Problem

Why docker run Doesn't Scale

Running a single container locally is straightforward:

bash
docker run -p 3000:3000 my-app

But production introduces complexity that Docker alone can't solve:

  • What happens when your container crashes? Who restarts it?
  • How do you run 50 copies across multiple servers for high availability?
  • How do you route traffic to healthy containers during deployments?
  • How do you handle server failures without downtime?
  • How do you manage configuration across environments (dev, staging, prod)?
  • How do you scale automatically when traffic spikes?

What this means for your infrastructure: You need orchestration—a system that manages container lifecycle, networking, scaling, and self-healing automatically.

The Scaling Challenge: From 1 Container to 100+

Let's trace what happens as your application grows:

Stage 1: Single Container

  • One server, one container
  • Manual restarts when it crashes
  • No redundancy

Stage 2: Docker Compose (5-10 containers)

  • Multi-container applications
  • Basic networking between services
  • Still single-host (can't span multiple servers)
  • Manual scaling and updates

Stage 3: Kubernetes (10-1000+ containers)

  • Multi-host orchestration
  • Automatic scaling and self-healing
  • Zero-downtime deployments
  • Service discovery and load balancing
  • Declarative configuration

Real mistake we've seen—and how to avoid it: Developers jump straight to Kubernetes for simple applications. If you're running fewer than 5 services with predictable traffic, Docker Compose or managed platforms (Render, Railway, Cloud Run) are often simpler and cheaper.

Docker Compose vs Kubernetes vs Managed Solutions

SolutionBest ForComplexityScaling Limit
Docker ComposeLocal dev, simple appsLowSingle host
KubernetesProduction at scaleHighMulti-host, unlimited
Managed K8s (EKS, GKE, AKS)Production without ops overheadMediumMulti-host, unlimited
Serverless (Cloud Run, Fargate)Stateless apps, variable trafficLowAuto-scales infinitely

What this means for your infrastructure: Start with the simplest solution that meets your requirements. Kubernetes adds operational complexity—make sure you need it.

2. Kubernetes Core Concepts

Cluster Architecture: Control Plane vs Worker Nodes

A Kubernetes cluster consists of two types of machines:

Control Plane (Master Nodes):

  • API Server: Entry point for all cluster operations
  • Scheduler: Decides which worker node runs each pod
  • Controller Manager: Maintains desired state (restarts crashed pods, scales deployments)
  • etcd: Distributed database storing cluster configuration

Worker Nodes:

  • Kubelet: Agent that runs containers on the node
  • Container Runtime: Docker, containerd, or CRI-O
  • Kube-proxy: Handles networking rules

What happens behind the scenes: When you deploy an application, the API server stores the desired state in etcd. The scheduler assigns pods to nodes. The kubelet on each node pulls container images and starts them. Controllers continuously monitor the cluster and take corrective action (e.g., restarting failed pods).

Pods: The Smallest Deployable Unit (Not Containers)

Critical concept: In Kubernetes, you don't deploy containers directly. You deploy Pods—which contain one or more containers that share:

  • Network namespace (same IP address)
  • Storage volumes
  • Lifecycle (start/stop together)

Most common pattern: One container per pod.

When to use multi-container pods:

  • Sidecar pattern: Logging agent alongside your app
  • Ambassador pattern: Proxy for external services
  • Adapter pattern: Standardizing output format
yaml
apiVersion: v1
kind: Pod
metadata:
  name: my-app
spec:
  containers:
  - name: app
    image: my-app:1.0
    ports:
    - containerPort: 3000
  - name: log-shipper  # Sidecar container
    image: fluent-bit:latest

Real mistake we've seen—and how to avoid it: Developers create pods directly. Don't. Use Deployments instead (explained below)—they provide self-healing, scaling, and updates.

Services: How Containers Communicate

Pods are ephemeral—they get new IP addresses when restarted. Services provide stable networking:

ClusterIP (default):

  • Internal load balancer
  • Accessible only within the cluster
  • Use for backend services, databases

NodePort:

  • Exposes service on each node's IP at a static port
  • Use for development or when you control the infrastructure

LoadBalancer:

  • Creates an external load balancer (AWS ELB, GCP Load Balancer)
  • Use for production external services
  • Warning: Can be expensive—each LoadBalancer costs ~$20/month

ExternalName:

  • Maps service to external DNS name
  • Use for accessing external databases
yaml
apiVersion: v1
kind: Service
metadata:
  name: my-app-service
spec:
  type: ClusterIP
  selector:
    app: my-app
  ports:
  - port: 80
    targetPort: 3000

What happens behind the scenes: Kubernetes creates a virtual IP for the service. Kube-proxy configures iptables rules on every node to route traffic to healthy pods matching the selector.

Deployments: Declarative Application Management

Deployments manage ReplicaSets, which manage Pods. This three-layer abstraction enables:

  • Self-healing: Restarts crashed pods
  • Scaling: Runs multiple replicas
  • Rolling updates: Zero-downtime deployments
  • Rollbacks: Revert to previous versions
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: app
        image: my-app:1.0
        ports:
        - containerPort: 3000
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "500m"

What this means for your infrastructure: You declare the desired state (3 replicas). Kubernetes continuously works to maintain it—if a pod crashes, a new one starts automatically.

ConfigMaps and Secrets: Configuration Management

ConfigMaps: Non-sensitive configuration (feature flags, API endpoints)

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  API_URL: "https://api.example.com"
  LOG_LEVEL: "info"

Secrets: Sensitive data (passwords, API keys, certificates)

bash
kubectl create secret generic db-credentials \
  --from-literal=username=admin \
  --from-literal=password=secret123

Using them in pods:

yaml
containers:
- name: app
  image: my-app:1.0
  env:
  - name: API_URL
    valueFrom:
      configMapKeyRef:
        name: app-config
        key: API_URL
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: db-credentials
        key: password

Security warning: Secrets are base64-encoded, not encrypted by default. For production:

  • Enable encryption at rest in etcd (AWS KMS integration)
  • Use external secret managers (AWS Secrets Manager, HashiCorp Vault)
  • Implement RBAC to restrict secret access

Namespaces: Logical Resource Isolation

Namespaces divide a cluster into virtual sub-clusters:

bash
kubectl create namespace production
kubectl create namespace staging

Use cases:

  • Environment separation: dev, staging, prod
  • Team isolation: frontend-team, backend-team
  • Resource quotas: Limit CPU/memory per namespace

Real mistake we've seen—and how to avoid it: Not using namespaces in shared clusters. Without them, name collisions occur and there's no resource isolation. Production workloads can starve staging resources.

The Declarative Model: Desired State vs Imperative Commands

Imperative (avoid in production):

bash
kubectl run my-app --image=my-app:1.0
kubectl scale deployment my-app --replicas=5

Declarative (recommended):

bash
kubectl apply -f deployment.yaml

Why declarative wins:

  • Configuration is version-controlled
  • Changes are auditable
  • Easy to replicate environments
  • GitOps workflows possible

3. Your First Kubernetes Deployment

Local Development Setup

Option 1: Minikube (Most popular)

bash
# Install Minikube
brew install minikube  # macOS
# or: https://minikube.sigs.k8s.io/docs/start/

# Start cluster
minikube start --cpus=4 --memory=8192

# Verify
kubectl get nodes

Option 2: Kind (Kubernetes in Docker)

bash
# Install Kind
brew install kind

# Create cluster
kind create cluster --name dev

Option 3: Docker Desktop

  • Enable Kubernetes in settings
  • Simplest for Docker users

If you're using Windows: Use WSL2 for better performance. Avoid running Kubernetes in VirtualBox.

Writing Your First Deployment YAML

Create deployment.yaml:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-app
  labels:
    app: hello
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hello
  template:
    metadata:
      labels:
        app: hello
    spec:
      containers:
      - name: hello
        image: gcr.io/google-samples/hello-app:1.0
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: "100m"
            memory: "128Mi"
          limits:
            cpu: "500m"
            memory: "256Mi"
        livenessProbe:
          httpGet:
            path: /
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: hello-service
spec:
  type: LoadBalancer
  selector:
    app: hello
  ports:
  - port: 80
    targetPort: 8080

Understanding kubectl Commands

bash
# Apply configuration
kubectl apply -f deployment.yaml

# Check deployment status
kubectl get deployments
kubectl get pods
kubectl get services

# Detailed pod information
kubectl describe pod <pod-name>

# Watch real-time updates
kubectl get pods --watch

# Delete resources
kubectl delete -f deployment.yaml

Essential commands reference: Official kubectl Cheat Sheet

Exposing Your Application

ClusterIP (internal only):

yaml
spec:
  type: ClusterIP

NodePort (development):

yaml
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: 8080
    nodePort: 30080  # Accessible at <node-ip>:30080

LoadBalancer (production):

yaml
spec:
  type: LoadBalancer

For Minikube:

bash
# LoadBalancer won't get external IP in Minikube
# Use tunnel instead:
minikube service hello-service

Viewing Logs and Debugging Pods

bash
# View logs
kubectl logs <pod-name>
kubectl logs -f <pod-name>  # Follow mode

# Logs from specific container in multi-container pod
kubectl logs <pod-name> -c <container-name>

# Execute commands in running pod
kubectl exec -it <pod-name> -- /bin/sh

# Copy files from pod
kubectl cp <pod-name>:/path/to/file ./local-file

# Port forward for local testing
kubectl port-forward <pod-name> 8080:8080

Real mistake we've seen—and how to avoid it: Forgetting to check logs when pods fail. Always start with kubectl describe pod and kubectl logs—they reveal 90% of issues.

4. Real-World Patterns

Rolling Updates and Rollbacks

Rolling update strategy:

yaml
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1        # Max extra pods during update
      maxUnavailable: 0  # Ensure zero downtime

Update image:

bash
kubectl set image deployment/hello-app hello=gcr.io/google-samples/hello-app:2.0

# Check rollout status
kubectl rollout status deployment/hello-app

# View rollout history
kubectl rollout history deployment/hello-app

Rollback to previous version:

bash
kubectl rollout undo deployment/hello-app

# Rollback to specific revision
kubectl rollout undo deployment/hello-app --to-revision=2

What happens behind the scenes: Kubernetes creates a new ReplicaSet with the new image version. It gradually scales up the new ReplicaSet while scaling down the old one, ensuring the total number of replicas remains constant.

Health Checks: Liveness vs Readiness Probes

Liveness Probe: "Is the container alive?"

  • If it fails, Kubernetes restarts the container
  • Use for detecting deadlocks or hung processes
yaml
livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10
  timeoutSeconds: 5
  failureThreshold: 3

Readiness Probe: "Is the container ready to receive traffic?"

  • If it fails, Kubernetes removes the pod from service endpoints
  • Use during startup, database migrations, or temporary failures
yaml
readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5

Real mistake we've seen—and how to avoid it: No health checks, or using the same endpoint for both probes. During deployment, new pods receive traffic before they're ready, causing errors. Use different endpoints: liveness checks core functionality, readiness checks dependencies (database, cache).

Optional—but strongly recommended by SimplifiedTechhub DevOps experts: Implement /healthz and /ready endpoints in your application. Return 200 for healthy, 503 for unhealthy. Include dependency checks in readiness (database connection, Redis availability).

Resource Limits and Requests (Preventing Node Crashes)

Requests: Guaranteed resources (used for scheduling) Limits: Maximum resources (enforced by kernel)

yaml
resources:
  requests:
    cpu: "250m"      # 0.25 CPU cores
    memory: "512Mi"  # 512 megabytes
  limits:
    cpu: "1000m"     # 1 CPU core
    memory: "1Gi"    # 1 gigabyte

What happens behind the scenes:

  • Kubernetes schedules pods only on nodes with available requested resources
  • If a pod exceeds memory limit, it's killed (OOMKilled)
  • If a pod exceeds CPU limit, it's throttled (slower, not killed)

Real mistake we've seen—and how to avoid it: Not setting resource limits. A single pod with a memory leak can crash an entire node, taking down all pods on it. Always set limits.

If you're using AWS EKS or GKE, here's what to watch for: Use Vertical Pod Autoscaler to analyze actual resource usage and recommend right-sized requests/limits.

Horizontal Pod Autoscaling

Automatically scale based on CPU, memory, or custom metrics:

yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: hello-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: hello-app
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

Apply metrics server (required for HPA):

bash
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

Advanced scaling: Use KEDA for event-driven autoscaling (queue length, database connections, custom metrics).

StatefulSets for Databases (When You Need Persistent Storage)

Use StatefulSets for applications requiring:

  • Stable network identities (predictable pod names)
  • Persistent storage (data survives pod restarts)
  • Ordered deployment and scaling
yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres
  replicas: 3
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:15
        ports:
        - containerPort: 5432
        volumeMounts:
        - name: postgres-storage
          mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
  - metadata:
      name: postgres-storage
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 10Gi

What this means for your infrastructure: Pods are named predictably (postgres-0, postgres-1, postgres-2). Each pod gets its own persistent volume that survives restarts.

Real mistake we've seen—and how to avoid it: Running production databases in Kubernetes without understanding storage classes and backup strategies. For small teams, managed databases (RDS, Cloud SQL) are often simpler and more reliable.

Jobs and CronJobs for Batch Processing

Job (one-time task):

yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: data-migration
spec:
  template:
    spec:
      containers:
      - name: migrate
        image: my-app:1.0
        command: ["node", "migrate.js"]
      restartPolicy: OnFailure

CronJob (scheduled task):

yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: backup
spec:
  schedule: "0 2 * * *"  # Daily at 2 AM
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: backup
            image: my-backup:1.0
            command: ["./backup.sh"]
          restartPolicy: OnFailure
```

---

## 5. Kubernetes Networking Demystified

### How Pod-to-Pod Communication Works

**Every pod gets its own IP address.** Pods can communicate directly without NAT.

**Network model:**
1. Pods on the same node communicate via virtual Ethernet bridge
2. Pods on different nodes communicate via overlay network (Calico, Flannel, Weave)

**What happens behind the scenes:** The Container Network Interface (CNI) plugin configures network routes. Traffic between pods flows through the CNI without leaving the cluster network.

### Service Discovery and DNS

Kubernetes runs an internal DNS server (CoreDNS). Services are automatically registered.

**DNS format:**
```
<service-name>.<namespace>.svc.cluster.local

Examples:

bash
# Within the same namespace
curl http://backend-service

# Across namespaces
curl http://database.production.svc.cluster.local
```

**Environment variables (alternative to DNS):**
Kubernetes injects service endpoints as environment variables:
```
BACKEND_SERVICE_HOST=10.96.0.5
BACKEND_SERVICE_PORT=8080

Ingress Controllers for External Traffic

Ingress routes external HTTP/HTTPS traffic to services:

yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
  - host: app.example.com
    http:
      paths:
      - path: /api
        pathType: Prefix
        backend:
          service:
            name: backend-service
            port:
              number: 8080
      - path: /
        pathType: Prefix
        backend:
          service:
            name: frontend-service
            port:
              number: 80
  tls:
  - hosts:
    - app.example.com
    secretName: app-tls-cert

Popular ingress controllers:

  • NGINX Ingress: Most widely used
  • Traefik: Built-in Let's Encrypt support
  • AWS ALB Ingress: Native AWS integration
  • Istio Gateway: Service mesh features

Install NGINX Ingress:

bash
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yaml

If you're using AWS, here's what to watch for: Use the AWS Load Balancer Controller for native ALB/NLB integration. It's more cost-effective than creating a LoadBalancer service per application.

Network Policies for Security

By default, all pods can communicate. NetworkPolicies implement zero-trust networking:

yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-policy
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
  - Ingress
  - Egress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 8080
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: database
    ports:
    - protocol: TCP
      port: 5432

What this means for your infrastructure: Backend pods can only receive traffic from frontend pods and can only send traffic to database pods.

Real mistake we've seen—and how to avoid it: Enabling network policies without testing. If misconfigured, you can lock yourself out or break internal communication. Test in staging first.

6. Storage and Persistence

Volumes vs Persistent Volumes vs Persistent Volume Claims

Volume: Temporary storage tied to pod lifecycle

yaml
volumes:
- name: cache
  emptyDir: {}

Persistent Volume (PV): Cluster-level storage resource Persistent Volume Claim (PVC): Request for storage by a user

Workflow:

  1. Admin provisions PV (or uses dynamic provisioning)
  2. User creates PVC requesting storage
  3. Kubernetes binds PVC to matching PV
  4. Pod mounts PVC as volume
yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: app-storage
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 5Gi
  storageClassName: standard
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app
spec:
  template:
    spec:
      containers:
      - name: app
        image: my-app:1.0
        volumeMounts:
        - name: data
          mountPath: /data
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: app-storage

When to Use StatefulSets vs Deployments

Use Deployments when:

  • Application is stateless
  • Any pod can handle any request
  • Pods are interchangeable

Use StatefulSets when:

  • Application requires stable network identity
  • Pods need persistent storage
  • Pods must start/stop in order (e.g., database replicas)

Storage Classes and Dynamic Provisioning

Storage Classes define types of storage:

yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: kubernetes.io/aws-ebs
parameters:
  type: gp3
  iopsPerGB: "10"
allowVolumeExpansion: true

Cloud provider defaults:

  • AWS: gp2 (EBS volumes)
  • GCP: pd-standard (Persistent Disks)
  • Azure: default (Azure Disk)

Real mistake we've seen—and how to avoid it: Using default storage classes for production databases. They're often slow. Use SSD-backed classes (gp3 on AWS, pd-ssd on GCP).

7. Development Workflow

Local Development with Kubernetes

Option 1: Skaffold (auto-rebuild on code changes)

yaml
# skaffold.yaml
apiVersion: skaffold/v2beta29
kind: Config
build:
  artifacts:
  - image: my-app
    docker:
      dockerfile: Dockerfile
deploy:
  kubectl:
    manifests:
    - k8s/*.yaml
bash
skaffold dev  # Watch mode: rebuilds on file changes

Option 2: Tilt (visual dashboard + hot reload)

python
# Tiltfile
docker_build('my-app', '.')
k8s_yaml('k8s/deployment.yaml')
k8s_resource('my-app', port_forwards=8080)
bash
tilt up

Option 3: DevSpace (development containers in cluster)

bash
devspace dev

If you're using VS Code: Install the Kubernetes extension for YAML validation and IntelliSense.

CI/CD Integration Patterns

GitHub Actions example:

yaml
name: Deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    
    - name: Build image
      run: docker build -t my-app:${{ github.sha }} .
    
    - name: Push to registry
      run: |
        docker tag my-app:${{ github.sha }} gcr.io/my-project/my-app:${{ github.sha }}
        docker push gcr.io/my-project/my-app:${{ github.sha }}
    
    - name: Deploy to Kubernetes
      run: |
        kubectl set image deployment/my-app my-app=gcr.io/my-project/my-app:${{ github.sha }}

Optional—but strongly recommended by SimplifiedTechhub DevOps experts: Use image digests instead of tags for deployments. Tags are mutable; digests are immutable and guarantee reproducible deployments.

GitOps Approach (ArgoCD, FluxCD)

GitOps principle: Git repository is the single source of truth for cluster state.

ArgoCD workflow:

  1. Push Kubernetes manifests to Git
  2. ArgoCD monitors repository
  3. ArgoCD automatically syncs changes to cluster
  4. Drift detection: alerts if cluster state diverges from Git

Install ArgoCD:

bash
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

Create application:

yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/my-app
    targetRevision: HEAD
    path: k8s
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

What this means for your infrastructure: Changes to production happen through pull requests. No manual kubectl commands. Full audit trail in Git history.

Helm Charts: Packaging Kubernetes Applications

Helm is a package manager for Kubernetes. Charts are reusable application templates.

Install Helm:

bash
brew install helm

Create chart:

bash
helm create my-app
```

**Chart structure:**
```
my-app/
├── Chart.yaml         # Chart metadata
├── values.yaml        # Default configuration
└── templates/
    ├── deployment.yaml
    ├── service.yaml
    └── ingress.yaml

values.yaml:

yaml
image:
  repository: my-app
  tag: "1.0"
replicaCount: 2
service:
  type: ClusterIP
  port: 80

Install chart:

bash
helm install my-app ./my-app --set replicaCount=3

Use Helm for:

  • Deploying complex applications (PostgreSQL, Redis, Kafka)
  • Managing multiple environments (values-dev.yaml, values-prod.yaml)
  • Versioning application releases

Find charts: Artifact Hub

8. Common Mistakes and Gotchas

Not Setting Resource Limits (Crashing Entire Nodes)

Symptom: Node runs out of memory, kubelet starts evicting pods randomly.

Root cause: Pods without memory limits can consume all node resources.

Fix: Always set requests and limits:

yaml
resources:
  requests:
    memory: "256Mi"
    cpu: "250m"
  limits:
    memory: "512Mi"
    cpu: "500m"

If you're using managed Kubernetes: Enable Limit Ranges to enforce default limits on all pods in a namespace.

Missing Health Checks (Traffic to Unhealthy Pods)

Symptom: Intermittent 502 errors during deployments.

Root cause: New pods receive traffic before they're ready.

Fix: Implement readiness probes:

yaml
readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  initialDelaySeconds: 10
  periodSeconds: 5

Using latest Tag in Production

Symptom: Deployments are not reproducible. Rollbacks don't work as expected.

Root cause: latest tag is mutable. Different nodes might pull different versions.

Fix: Use immutable tags (commit SHAs or semantic versions):

yaml
image: my-app:v1.2.3
# or
image: my-app:abc123def  # Git commit SHA

Not Understanding Pod Restart Policies

Policies:

  • Always (default for Deployments): Restart on any exit
  • OnFailure (Jobs): Restart only on non-zero exit
  • Never: Never restart

Real mistake we've seen—and how to avoid it: Using Always for Jobs or init containers. This causes infinite restart loops if the task fails.

Misunderstanding Service Types

Common confusion: "I set type: LoadBalancer but can't access the service."

Troubleshooting:

  1. Check external IP: kubectl get svc
  2. If <pending>, your cluster doesn't support LoadBalancer (use NodePort or Ingress)
  3. Verify security groups / firewall rules

Security: Running as Root, Exposing Secrets

Symptom: Container compromise leads to node compromise.

Root cause: Containers running as root have elevated privileges.

Fix: Run as non-root user:

yaml
spec:
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    fsGroup: 1000
  containers:
  - name: app
    image: my-app:1.0
    securityContext:
      allowPrivilegeEscalation: false
      readOnlyRootFilesystem: true
      capabilities:
        drop:
        - ALL

Dockerfile best practice:

dockerfile
FROM node:18-alpine

# Create non-root user
RUN addgroup -g 1000 appuser && \
    adduser -D -u 1000 -G appuser appuser

# Install dependencies as root
COPY package*.json ./
RUN npm ci --only=production

# Switch to non-root user
USER appuser

COPY --chown=appuser:appuser . .

CMD ["node", "server.js"]

Real mistake we've seen—and how to avoid it: Hardcoding secrets in environment variables or ConfigMaps. Use Kubernetes Secrets, and for production, integrate with external secret managers:

Optional—but strongly recommended by SimplifiedTechhub DevOps experts: Scan container images for vulnerabilities using Trivy or Grype. Integrate into CI/CD to block vulnerable images from deployment.

9. When Kubernetes Is Overkill

Do You Actually Need Kubernetes?

Kubernetes adds complexity. Before adopting, ask:

  1. Are you running multiple services? (Single apps rarely need orchestration)
  2. Do you need to scale dynamically? (Predictable traffic doesn't require autoscaling)
  3. Do you have operational expertise? (Kubernetes requires monitoring, security, upgrades)
  4. Is your team large enough? (Small teams benefit more from managed platforms)

What this means for your infrastructure: If you're a startup with 2-3 developers running 5 microservices, Kubernetes is probably overkill.

Simpler Alternatives

For small applications (1-3 services):

  • Render, Railway, Fly.io: Deploy from Git, auto-scaling, zero config
  • AWS App Runner, Google Cloud Run: Serverless containers
  • Heroku, DigitalOcean App Platform: Managed PaaS

For Docker Compose users:

  • AWS ECS Fargate: Run containers without managing servers
  • Docker Swarm: Simpler orchestration (though less popular)

For predictable workloads:

  • Traditional VPS (DigitalOcean, Linode): Simple, cost-effective
  • Managed Kubernetes (EKS Autopilot, GKE Autopilot): Kubernetes without infrastructure management

When Kubernetes Makes Sense

You should use Kubernetes if:

  • Running 10+ microservices
  • Need multi-cloud or hybrid deployments
  • Require complex networking (service mesh, advanced routing)
  • Traffic patterns require rapid scaling (10x spikes)
  • Team has dedicated DevOps/platform engineers
  • Building internal developer platform

Real-world examples:

  • Spotify: Runs 1,000+ services on Kubernetes across multiple clouds
  • Airbnb: Uses Kubernetes for dynamic scaling during travel surges
  • Reddit: Migrated from EC2 to Kubernetes for better resource utilization

What this means for your infrastructure: Kubernetes is an infrastructure investment. Start with managed services to reduce operational burden.

10. Production-Ready Deployment Checklist

Architecture Decisions

Managed vs Self-Hosted:

  • EKS (AWS): Deep AWS integration, ALB/EBS support, IAM for pods (setup guide)
  • GKE (GCP): Best Kubernetes experience, Autopilot mode, built-in monitoring (setup guide)
  • AKS (Azure): Azure native, Active Directory integration (setup guide)
  • Self-hosted: Full control, but requires expertise in etcd, networking, certificate management

If you're using AWS, here's what to watch for: Use EKS Fargate for serverless pods (no node management). Use Spot Instances for dev/staging to save 70% on costs.

Essential Add-ons for Production

Monitoring & Logging:

bash
# Prometheus + Grafana
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install prometheus prometheus-community/kube-prometheus-stack

# ELK Stack for logs
helm repo add elastic https://helm.elastic.co
helm install elasticsearch elastic/elasticsearch
helm install kibana elastic/kibana

Or use managed services:

  • Datadog: Unified monitoring, APM, logs
  • New Relic: Full observability platform
  • AWS CloudWatch Container Insights: Native AWS integration

Ingress & TLS:

bash
# NGINX Ingress + cert-manager (Let's Encrypt)
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm install ingress-nginx ingress-nginx/ingress-nginx

helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager --set installCRDs=true

Backup & Disaster Recovery:

bash
# Velero for cluster backups
helm repo add vmware-tanzu https://vmware-tanzu.github.io/helm-charts
helm install velero vmware-tanzu/velero \
  --set configuration.provider=aws \
  --set configuration.backupStorageLocation.bucket=my-backup-bucket

Security Scanning:

  • Falco: Runtime security monitoring
  • OPA Gatekeeper: Policy enforcement
  • Kyverno: Kubernetes-native policy engine

Cost Optimization Strategies

Node Autoscaling:

yaml
# Cluster Autoscaler
apiVersion: autoscaling/v1
kind: ClusterAutoscaler
spec:
  minNodes: 2
  maxNodes: 10

Spot Instances (AWS):

  • Use for stateless workloads, dev environments
  • Save 70-90% vs on-demand pricing
  • Configure multiple instance types for availability

Right-Sizing:

bash
# Install Goldilocks for recommendations
helm repo add fairwinds-stable https://charts.fairwinds.com/stable
helm install goldilocks fairwinds-stable/goldilocks

Namespace Resource Quotas:

yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: compute-quota
  namespace: development
spec:
  hard:
    requests.cpu: "10"
    requests.memory: 20Gi
    limits.cpu: "20"
    limits.memory: 40Gi
    persistentvolumeclaims: "10"

Real mistake we've seen—and how to avoid it: Running production workloads on oversized nodes. Use multiple smaller nodes for better bin-packing and fault tolerance. T3.medium (2 vCPU, 4GB) is often better than T3.xlarge (4 vCPU, 16GB) for most workloads.

11. Real-World Deployment Scenarios

Scenario 1: Deploying a Node.js API

Architecture:

  • Node.js Express API
  • PostgreSQL database (managed RDS)
  • Redis cache (ElastiCache)
  • Environment-specific configuration

Step 1: Create Dockerfile

dockerfile
FROM node:18-alpine

WORKDIR /app

# Install dependencies
COPY package*.json ./
RUN npm ci --only=production

# Add non-root user
RUN addgroup -g 1000 node && adduser -D -u 1000 -G node node
USER node

# Copy application
COPY --chown=node:node . .

EXPOSE 3000

CMD ["node", "server.js"]

Step 2: Build and push image

bash
docker build -t my-api:v1.0.0 .
docker tag my-api:v1.0.0 123456789.dkr.ecr.us-east-1.amazonaws.com/my-api:v1.0.0
docker push 123456789.dkr.ecr.us-east-1.amazonaws.com/my-api:v1.0.0

Step 3: Create ConfigMap for non-sensitive config

yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: api-config
  namespace: production
data:
  NODE_ENV: "production"
  LOG_LEVEL: "info"
  API_RATE_LIMIT: "100"

Step 4: Create Secret for database credentials

bash
kubectl create secret generic db-credentials \
  --from-literal=DB_HOST=mydb.123456.us-east-1.rds.amazonaws.com \
  --from-literal=DB_USER=api_user \
  --from-literal=DB_PASSWORD='SecureP@ssw0rd!' \
  --from-literal=REDIS_URL=redis://mycache.abc123.0001.use1.cache.amazonaws.com:6379 \
  --namespace production

Step 5: Create Deployment

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
  namespace: production
  labels:
    app: api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
        version: v1.0.0
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        fsGroup: 1000
      containers:
      - name: api
        image: 123456789.dkr.ecr.us-east-1.amazonaws.com/my-api:v1.0.0
        imagePullPolicy: Always
        ports:
        - containerPort: 3000
          name: http
        env:
        - name: PORT
          value: "3000"
        envFrom:
        - configMapRef:
            name: api-config
        - secretRef:
            name: db-credentials
        resources:
          requests:
            cpu: "250m"
            memory: "512Mi"
          limits:
            cpu: "1000m"
            memory: "1Gi"
        livenessProbe:
          httpGet:
            path: /health
            port: 3000
          initialDelaySeconds: 30
          periodSeconds: 10
          timeoutSeconds: 5
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /ready
            port: 3000
          initialDelaySeconds: 10
          periodSeconds: 5
          timeoutSeconds: 3
          failureThreshold: 2
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities:
            drop:
            - ALL
        volumeMounts:
        - name: tmp
          mountPath: /tmp
      volumes:
      - name: tmp
        emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
  name: api-service
  namespace: production
spec:
  type: ClusterIP
  selector:
    app: api
  ports:
  - port: 80
    targetPort: 3000
    protocol: TCP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: api-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: api
  minReplicas: 3
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80

Step 6: Create Ingress

yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-ingress
  namespace: production
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
    nginx.ingress.kubernetes.io/rate-limit: "100"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - api.example.com
    secretName: api-tls
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: api-service
            port:
              number: 80

Step 7: Deploy

bash
kubectl apply -f k8s/configmap.yaml
kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/ingress.yaml

# Verify deployment
kubectl rollout status deployment/api -n production
kubectl get pods -n production -l app=api

What this means for your infrastructure: Your API runs with 3 replicas for high availability, automatically scales to 20 replicas under load, has health checks to prevent traffic to unhealthy pods, and runs with security best practices (non-root user, read-only filesystem).

Scenario 2: Multi-Service Application (Frontend + Backend + Database)

Architecture:

  • React frontend (static files served by NGINX)
  • Node.js backend API
  • PostgreSQL database (StatefulSet with persistent storage)
  • All services communicate internally

Frontend Deployment:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
  namespace: production
spec:
  replicas: 2
  selector:
    matchLabels:
      app: frontend
  template:
    metadata:
      labels:
        app: frontend
    spec:
      containers:
      - name: nginx
        image: my-frontend:v1.0.0
        ports:
        - containerPort: 80
        resources:
          requests:
            cpu: "100m"
            memory: "128Mi"
          limits:
            cpu: "200m"
            memory: "256Mi"
---
apiVersion: v1
kind: Service
metadata:
  name: frontend
  namespace: production
spec:
  selector:
    app: frontend
  ports:
  - port: 80
    targetPort: 80

Backend Deployment:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      containers:
      - name: api
        image: my-backend:v1.0.0
        ports:
        - containerPort: 8080
        env:
        - name: DATABASE_URL
          value: "postgresql://postgres.production.svc.cluster.local:5432/mydb"
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: password
---
apiVersion: v1
kind: Service
metadata:
  name: backend
  namespace: production
spec:
  selector:
    app: backend
  ports:
  - port: 8080
    targetPort: 8080

PostgreSQL StatefulSet:

yaml
apiVersion: v1
kind: Service
metadata:
  name: postgres
  namespace: production
spec:
  clusterIP: None  # Headless service
  selector:
    app: postgres
  ports:
  - port: 5432
    targetPort: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  namespace: production
spec:
  serviceName: postgres
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:15-alpine
        ports:
        - containerPort: 5432
        env:
        - name: POSTGRES_DB
          value: mydb
        - name: POSTGRES_USER
          value: postgres
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: password
        - name: PGDATA
          value: /var/lib/postgresql/data/pgdata
        volumeMounts:
        - name: postgres-storage
          mountPath: /var/lib/postgresql/data
        resources:
          requests:
            cpu: "500m"
            memory: "1Gi"
          limits:
            cpu: "1000m"
            memory: "2Gi"
  volumeClaimTemplates:
  - metadata:
      name: postgres-storage
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: gp3  # AWS EBS gp3
      resources:
        requests:
          storage: 20Gi

Ingress routing:

yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  namespace: production
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  ingressClassName: nginx
  rules:
  - host: myapp.example.com
    http:
      paths:
      - path: /api(/|$)(.*)
        pathType: Prefix
        backend:
          service:
            name: backend
            port:
              number: 8080
      - path: /()(.*)
        pathType: Prefix
        backend:
          service:
            name: frontend
            port:
              number: 80

What happens behind the scenes: Frontend makes API calls to /api/*, which Ingress routes to the backend service. Backend connects to PostgreSQL using internal DNS (postgres.production.svc.cluster.local). No external exposure of the database.

Real mistake we've seen—and how to avoid it: Running production databases in Kubernetes without backup strategies. Implement automated backups:

yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: postgres-backup
  namespace: production
spec:
  schedule: "0 2 * * *"  # Daily at 2 AM
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: backup
            image: postgres:15-alpine
            command:
            - /bin/sh
            - -c
            - |
              pg_dump -h postgres.production.svc.cluster.local -U postgres mydb | \
              gzip > /backup/backup-$(date +%Y%m%d-%H%M%S).sql.gz
              aws s3 cp /backup/*.sql.gz s3://my-backups/postgres/
            env:
            - name: PGPASSWORD
              valueFrom:
                secretKeyRef:
                  name: db-credentials
                  key: password
            volumeMounts:
            - name: backup
              mountPath: /backup
          volumes:
          - name: backup
            emptyDir: {}
          restartPolicy: OnFailure

Scenario 3: Background Job Processing (Queue-Based Workers)

Architecture:

  • RabbitMQ message queue
  • Multiple worker pods processing jobs
  • CronJob for scheduled tasks

RabbitMQ Deployment:

bash
# Install using Helm
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install rabbitmq bitnami/rabbitmq \
  --set auth.username=admin \
  --set auth.password=SecurePassword123 \
  --namespace production

Worker Deployment:

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: worker
  namespace: production
spec:
  replicas: 5
  selector:
    matchLabels:
      app: worker
  template:
    metadata:
      labels:
        app: worker
    spec:
      containers:
      - name: worker
        image: my-worker:v1.0.0
        env:
        - name: RABBITMQ_URL
          value: "amqp://admin:SecurePassword123@rabbitmq.production.svc.cluster.local:5672"
        - name: QUEUE_NAME
          value: "jobs"
        - name: CONCURRENCY
          value: "10"
        resources:
          requests:
            cpu: "500m"
            memory: "512Mi"
          limits:
            cpu: "2000m"
            memory: "2Gi"
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: worker-hpa
  namespace: production
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: worker
  minReplicas: 5
  maxReplicas: 50
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 80

CronJob for scheduled tasks:

yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: daily-report
  namespace: production
spec:
  schedule: "0 8 * * *"  # Daily at 8 AM
  successfulJobsHistoryLimit: 3
  failedJobsHistoryLimit: 3
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: report-generator
            image: my-worker:v1.0.0
            command: ["node", "generate-report.js"]
            env:
            - name: REPORT_TYPE
              value: "daily"
            - name: S3_BUCKET
              value: "my-reports"
          restartPolicy: OnFailure

What this means for your infrastructure: Workers automatically scale based on CPU usage (queue depth can also be used with custom metrics). CronJobs run scheduled tasks reliably without manual intervention.

12. Debugging Strategies When Things Go Wrong

Common Debugging Commands

bash
# Get high-level cluster status
kubectl get nodes
kubectl get pods --all-namespaces
kubectl top nodes
kubectl top pods

# Describe resources for detailed info
kubectl describe pod <pod-name>
kubectl describe node <node-name>
kubectl describe service <service-name>

# View logs
kubectl logs <pod-name>
kubectl logs <pod-name> --previous  # Logs from crashed container
kubectl logs <pod-name> -c <container-name>  # Specific container
kubectl logs -f <pod-name>  # Follow logs

# Execute commands in pod
kubectl exec -it <pod-name> -- /bin/sh
kubectl exec <pod-name> -- env  # View environment variables
kubectl exec <pod-name> -- ps aux  # View processes

# Port forwarding for local access
kubectl port-forward <pod-name> 8080:8080
kubectl port-forward service/<service-name> 8080:80

# Events (often reveals issues)
kubectl get events --sort-by='.lastTimestamp'
kubectl get events --field-selector involvedObject.name=<pod-name>

Debugging "My Pod Keeps Crashing"

Step 1: Check pod status

bash
kubectl get pods

Common statuses:

  • CrashLoopBackOff: Container crashes repeatedly
  • ImagePullBackOff: Can't pull container image
  • Pending: Waiting to be scheduled
  • OOMKilled: Out of memory

Step 2: Describe the pod

bash
kubectl describe pod <pod-name>

Look for:

  • Events section: Error messages
  • Last State: Reason for previous crash
  • Conditions: Why pod isn't ready

Step 3: Check logs

bash
kubectl logs <pod-name>
kubectl logs <pod-name> --previous  # Critical for crash loops
```

**Common causes and fixes:**

**ImagePullBackOff:**
```
Error: Failed to pull image "my-app:1.0.0": rpc error: code = Unknown desc = Error response from daemon: pull access denied
```
**Fix:** Check image name, registry authentication, or image exists

**OOMKilled:**
```
Last State: Terminated
  Reason: OOMKilled
  Exit Code: 137
```
**Fix:** Increase memory limits or optimize application memory usage

**CrashLoopBackOff with immediate exit:**
```
Error: failed to start container "app": Error response from daemon: OCI runtime create failed

Fix: Check Dockerfile CMD/ENTRYPOINT, ensure binary exists and has execute permissions

Debugging "I Can't Reach My Service"

Step 1: Verify service exists and has endpoints

bash
kubectl get service <service-name>
kubectl get endpoints <service-name>

If no endpoints: Service selector doesn't match any pods

bash
# Check pod labels
kubectl get pods --show-labels

# Compare with service selector
kubectl get service <service-name> -o yaml | grep selector -A 2

Step 2: Test internal connectivity

bash
# Create debug pod
kubectl run debug --image=nicolaka/netshoot -it --rm

# Inside debug pod:
curl http://<service-name>.<namespace>.svc.cluster.local
nslookup <service-name>.<namespace>.svc.cluster.local

Step 3: Check network policies

bash
kubectl get networkpolicies
kubectl describe networkpolicy <policy-name>

Step 4: Verify port configuration

bash
# Check service ports
kubectl get service <service-name> -o yaml

# Check pod ports
kubectl get pod <pod-name> -o yaml | grep -A 5 ports

Real mistake we've seen—and how to avoid it: Port mismatch between service and container. Service targets port 8080, but container listens on 3000. Always verify targetPort matches containerPort.

Debugging Tools

k9s: Terminal UI for Kubernetes

bash
brew install k9s
k9s

Lens: Desktop IDE for Kubernetes

Stern: Multi-pod log tailing

bash
brew install stern
stern <pod-name-prefix>  # Tails logs from all matching pods

kubectx/kubens: Context and namespace switching

bash
brew install kubectx
kubectx  # Switch clusters
kubens  # Switch namespaces

13. Security Best Practices

RBAC (Role-Based Access Control) Fundamentals

Principle: Grant minimum necessary permissions.

Create read-only user:

yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: readonly-user
  namespace: production
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: readonly-role
  namespace: production
rules:
- apiGroups: [""]
  resources: ["pods", "services", "configmaps"]
  verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
  resources: ["deployments", "replicasets"]
  verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: readonly-binding
  namespace: production
subjects:
- kind: ServiceAccount
  name: readonly-user
  namespace: production
roleRef:
  kind: Role
  name: readonly-role
  apiGroup: rbac.authorization.k8s.io

For cluster-wide permissions, use ClusterRole and ClusterRoleBinding.

Real mistake we've seen—and how to avoid it: Giving developers cluster-admin access. Create namespace-specific roles instead.

Pod Security Standards

Kubernetes Pod Security Standards (replacement for Pod Security Policies):

Levels:

  • Privileged: Unrestricted (default)
  • Baseline: Prevents known privilege escalations
  • Restricted: Heavily restricted (production recommendation)

Enable at namespace level:

yaml
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted

What this enforces:

  • No root containers
  • No privilege escalation
  • No host networking/ports/IPC
  • Read-only root filesystem
  • Restricted volumes

Official documentation: Pod Security Standards

Network Policies for Zero-Trust Networking

Default deny all traffic:

yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  - Egress

Allow specific traffic:

yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 8080

If you're using AWS EKS: Ensure you're using a CNI that supports network policies (Calico, Cilium). The default AWS VPC CNI doesn't support them.

Scanning Images for Vulnerabilities

Integrate Trivy into CI/CD:

yaml
# GitHub Actions
- name: Scan image
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: 'my-app:${{ github.sha }}'
    severity: 'CRITICAL,HIGH'
    exit-code: '1'  # Fail build on vulnerabilities

Scan running images:

bash
trivy image --severity HIGH,CRITICAL my-app:1.0.0

Optional—but strongly recommended by SimplifiedTechhub DevOps experts: Use admission controllers (OPA Gatekeeper, Kyverno) to block deployment of vulnerable images:

yaml
# Kyverno policy
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: block-vulnerable-images
spec:
  validationFailureAction: enforce
  rules:
  - name: check-vulnerabilities
    match:
      resources:
        kinds:
        - Pod
    validate:
      message: "Image contains HIGH or CRITICAL vulnerabilities"
      deny:
        conditions:
        - key: "{{ images.*.vulnerabilities | length(@) }}"
          operator: GreaterThan
          value: 0

14. Migration Strategy

Moving from Docker Compose to Kubernetes

Docker Compose:

yaml
version: '3'
services:
  web:
    image: my-app:latest
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://db:5432/mydb
    depends_on:
      - db
  db:
    image: postgres:15
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD=secret

volumes:
  postgres_data:

Equivalent Kubernetes manifests:

yaml
# web-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  replicas: 1
  selector:
yaml
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: web
        image: my-app:latest
        ports:
        - containerPort: 3000
        env:
        - name: DATABASE_URL
          value: "postgresql://db:5432/mydb"
---
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  type: LoadBalancer
  selector:
    app: web
  ports:
  - port: 80
    targetPort: 3000
---
# db-statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: db
spec:
  serviceName: db
  replicas: 1
  selector:
    matchLabels:
      app: db
  template:
    metadata:
      labels:
        app: db
    spec:
      containers:
      - name: postgres
        image: postgres:15
        env:
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: password
        - name: PGDATA
          value: /var/lib/postgresql/data/pgdata
        volumeMounts:
        - name: postgres-data
          mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
  - metadata:
      name: postgres-data
    spec:
      accessModes: ["ReadWriteOnce"]
      resources:
        requests:
          storage: 10Gi
---
apiVersion: v1
kind: Service
metadata:
  name: db
spec:
  clusterIP: None  # Headless service
  selector:
    app: db
  ports:
  - port: 5432

Automated conversion tool:

bash
# Kompose converts Docker Compose to Kubernetes manifests
brew install kompose

kompose convert -f docker-compose.yml

What this means for your infrastructure: Kompose generates a starting point, but you'll need to add production concerns: resource limits, health checks, proper secrets management, and storage classes.

Lift-and-Shift vs Cloud-Native Refactoring

Lift-and-Shift Approach:

  • Containerize existing application as-is
  • Deploy to Kubernetes with minimal changes
  • Use StatefulSets for databases
  • Keep external dependencies (RDS, Redis)

Pros: Fast migration, minimal code changes Cons: Doesn't leverage Kubernetes benefits, harder to scale

Cloud-Native Refactoring:

  • Design for statelessness
  • Externalize configuration (ConfigMaps, Secrets)
  • Implement health check endpoints
  • Use horizontal scaling
  • Leverage service mesh (Istio, Linkerd)
  • Adopt 12-factor app principles

Pros: Better scalability, resilience, observability Cons: Requires application changes, longer timeline

SimplifiedTechhub recommendation: Start with lift-and-shift for quick wins, then incrementally refactor. Prioritize:

  1. Health checks (immediate reliability improvement)
  2. Configuration externalization (easier environment management)
  3. Stateless design (enables autoscaling)
  4. Observability (structured logging, metrics, tracing)

Phased Migration Strategy

Phase 1: Non-Critical Services (Week 1-2)

  • Choose low-traffic internal service
  • Deploy to staging Kubernetes cluster
  • Test thoroughly
  • Monitor for 1 week
  • Document lessons learned

Phase 2: Canary Production Deployment (Week 3-4)

  • Deploy to production alongside existing infrastructure
  • Route 10% of traffic to Kubernetes (using feature flags or load balancer)
  • Gradually increase to 100%
  • Keep rollback plan ready

Phase 3: Critical Services (Week 5-8)

  • Apply lessons from Phase 1-2
  • Migrate with blue-green deployment strategy
  • Full monitoring and alerting

Phase 4: Database Migration (Week 9-12)

  • Set up replication between old and new
  • Test failover procedures
  • Perform migration during maintenance window
  • Keep old database as backup for 30 days

Real mistake we've seen—and how to avoid it: Big-bang migrations without fallback plans. Always maintain the ability to route traffic back to the old infrastructure during migration.

15. When to Start with Managed Services

Managed Kubernetes Options

GKE Autopilot (Google Cloud):

  • What it manages: Nodes, upgrades, security patches
  • You manage: Just your applications
  • Pricing: Pay per pod resource usage
  • Best for: Teams wanting zero infrastructure management
bash
# Create Autopilot cluster
gcloud container clusters create-auto my-cluster \
  --region=us-central1

EKS Fargate (AWS):

  • What it manages: EC2 instances, node groups
  • You manage: Pod specifications
  • Pricing: Pay per pod (0.25 vCPU minimum)
  • Best for: Variable workloads, serverless architecture
bash
# Create Fargate profile
eksctl create fargateprofile \
  --cluster my-cluster \
  --name production \
  --namespace production

AKS with Virtual Nodes (Azure):

  • What it manages: Underlying VMs
  • You manage: Kubernetes resources
  • Pricing: Per second billing
  • Best for: Burst workloads

What this means for your infrastructure: Managed services reduce operational burden but cost more than self-managed nodes. Calculate break-even point based on team size and operational complexity.

Cost Comparison Example

Self-Managed EKS (3-node cluster):

  • EC2 instances: 3 × t3.large × $0.0832/hr = $179/month
  • EKS control plane: $73/month
  • Total: ~$252/month + operational overhead

EKS Fargate:

  • Pay per pod: varies by workload
  • Example: 10 pods × 0.5 vCPU × $0.04/hr = $144/month
  • Total: ~$144/month for small workloads, no operational overhead

Real-world insight: For teams under 10 developers, managed services often cost less when you factor in engineer time spent on cluster maintenance, security patches, and troubleshooting.

Alternative: Start with Simpler Solutions

Before jumping to Kubernetes:

For simple apps (1-3 services):

  • Render: Git-push deployments, auto-scaling, free tier
  • Railway: Instant deployments, GitHub integration
  • Fly.io: Global edge deployment, simple pricing

For containerized workloads:

  • AWS ECS Fargate: Simpler than Kubernetes, still scalable
  • Google Cloud Run: Fully managed, pay per request
  • Azure Container Instances: Quick container deployment

For teams with Docker Compose:

  • DigitalOcean App Platform: Docker Compose compatibility
  • Render (Docker): Compose-to-production
  • Railway (Docker): Multi-container support

When to graduate to Kubernetes:

  • Running 10+ interconnected services
  • Need complex networking (service mesh, advanced routing)
  • Require multi-cloud or hybrid deployments
  • Team has dedicated platform/DevOps engineers
  • Custom scaling requirements beyond standard autoscaling

16. Developer Tools That Make Kubernetes Less Painful

k9s: Terminal UI for Kubernetes

Install:

bash
brew install k9s

Key features:

  • Live view of all resources
  • Log streaming
  • Shell into pods
  • Resource editing
  • Port forwarding
  • Context switching

Keyboard shortcuts:

  • :pod - View pods
  • :deploy - View deployments
  • :svc - View services
  • l - View logs
  • d - Describe resource
  • e - Edit resource
  • s - Shell into pod

What this means for your infrastructure: Significantly faster than typing kubectl commands. Essential for daily Kubernetes work.

Lens: Kubernetes IDE

Download: https://k8slens.dev/

Key features:

  • Visual cluster management
  • Built-in terminal
  • Metrics dashboard
  • Helm chart deployment
  • Resource templates
  • Multi-cluster management

Best for: Developers who prefer GUI over CLI.

Skaffold: Continuous Development

Install:

bash
brew install skaffold

skaffold.yaml:

yaml
apiVersion: skaffold/v4beta6
kind: Config
metadata:
  name: my-app
build:
  artifacts:
  - image: my-app
    docker:
      dockerfile: Dockerfile
    sync:
      manual:
      - src: 'src/**/*.js'
        dest: /app/src
deploy:
  kubectl:
    manifests:
    - k8s/*.yaml
portForward:
- resourceType: service
  resourceName: my-app
  port: 8080
  localPort: 3000

Development workflow:

bash
# Auto-rebuild and redeploy on file changes
skaffold dev

# Build and deploy once
skaffold run

# Delete all resources
skaffold delete

What happens behind the scenes: Skaffold watches your source files, rebuilds the container image when they change, pushes to registry, and updates the deployment. File sync mode (for interpreted languages) copies changed files directly into running pods without rebuilding.

Tilt: Visual Development Environment

Install:

bash
brew install tilt

Tiltfile:

python
# Build Docker image
docker_build('my-app', '.')

# Deploy Kubernetes manifests
k8s_yaml('k8s/deployment.yaml')

# Port forwarding
k8s_resource('my-app', port_forwards=3000)

# Live update (no rebuild needed)
docker_build('my-app', '.', 
  live_update=[
    sync('./src', '/app/src'),
    run('npm install', trigger='package.json')
  ]
)

Start development:

bash
tilt up

Access web UI: http://localhost:10350

Key features:

  • Visual dashboard of all services
  • Live logs from all pods
  • Build status and timing
  • Resource health indicators
  • One-click restart

Optional—but strongly recommended by SimplifiedTechhub DevOps experts: Use Tilt for local development. It dramatically improves the feedback loop compared to manual docker build and kubectl apply cycles.

Telepresence: Local Development with Remote Services

Problem: Running entire microservices stack locally is resource-intensive.

Solution: Run one service locally, connect to remote cluster for dependencies.

Install:

bash
brew install telepresence

Connect to cluster:

bash
# Connect local machine to Kubernetes network
telepresence connect

# Run local service as if it's in the cluster
telepresence intercept my-app --port 3000

What happens behind the scenes: Telepresence creates a VPN connection to your cluster. Traffic to the intercepted service is routed to your local machine. You can access remote services (databases, APIs) using cluster DNS names.

Use case: Debugging production issues locally with access to production dependencies (with proper safeguards).

17. Cost Optimization in Production

Right-Sizing Workloads

Problem: Developers often over-provision resources "to be safe."

Solution: Use Vertical Pod Autoscaler (VPA) for recommendations:

bash
# Install VPA
git clone https://github.com/kubernetes/autoscaler.git
cd autoscaler/vertical-pod-autoscaler
./hack/vpa-up.sh

Create VPA recommendation:

yaml
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: my-app-vpa
spec:
  targetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-app
  updateMode: "Off"  # Recommendation only, doesn't auto-apply

View recommendations:

bash
kubectl describe vpa my-app-vpa

What this means for your infrastructure: VPA analyzes actual resource usage and recommends optimal requests/limits. Can reduce costs by 30-50% by eliminating over-provisioning.

Cluster Autoscaling

AWS EKS:

bash
# Install Cluster Autoscaler
kubectl apply -f https://raw.githubusercontent.com/kubernetes/autoscaler/master/cluster-autoscaler/cloudprovider/aws/examples/cluster-autoscaler-autodiscover.yaml

# Annotate deployment with cluster name
kubectl -n kube-system annotate deployment.apps/cluster-autoscaler \
  cluster-autoscaler.kubernetes.io/safe-to-evict="false"

GKE (built-in):

bash
gcloud container clusters update my-cluster \
  --enable-autoscaling \
  --min-nodes=1 \
  --max-nodes=10

What happens behind the scenes: When pods can't be scheduled due to insufficient resources, Cluster Autoscaler adds nodes. When nodes are underutilized for 10 minutes, it drains and removes them.

Spot Instances (AWS) / Preemptible VMs (GCP)

Save 70-90% on compute costs for fault-tolerant workloads.

AWS EKS with Spot Instances:

yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
  name: my-cluster
  region: us-east-1
nodeGroups:
- name: spot-workers
  instancesDistribution:
    instanceTypes: ["t3.medium", "t3.large", "t3a.medium"]
    onDemandBaseCapacity: 0
    onDemandPercentageAboveBaseCapacity: 0  # 100% spot
    spotInstancePools: 3
  minSize: 2
  maxSize: 20
  labels:
    workload-type: spot
  taints:
    - key: spot
      value: "true"
      effect: NoSchedule

Deploy to Spot nodes:

yaml
spec:
  tolerations:
  - key: spot
    operator: Equal
    value: "true"
    effect: NoSchedule
  nodeSelector:
    workload-type: spot

Best practices for Spot:

  • Use for stateless workloads (web servers, workers)
  • Avoid for databases or stateful services
  • Mix Spot and On-Demand nodes
  • Implement graceful shutdown handlers (SIGTERM)
  • Use multiple instance types for availability

Real mistake we've seen—and how to avoid it: Running critical production workloads entirely on Spot instances. Use 70% Spot / 30% On-Demand mix for resilience.

Namespace Resource Quotas

Prevent runaway costs by limiting resources per team/environment:

yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: dev-quota
  namespace: development
spec:
  hard:
    requests.cpu: "20"
    requests.memory: 40Gi
    limits.cpu: "40"
    limits.memory: 80Gi
    persistentvolumeclaims: "10"
    services.loadbalancers: "2"

What this means for your infrastructure: Development namespace can't accidentally spin up 50 load balancers or consume all cluster resources.

Monitoring Costs

Kubecost: Open-source cost monitoring for Kubernetes

bash
helm install kubecost kubecost/cost-analyzer \
  --namespace kubecost --create-namespace \
  --set kubecostToken="your-token"

Access dashboard:

bash
kubectl port-forward -n kubecost svc/kubecost-cost-analyzer 9090:9090

Features:

  • Per-namespace, deployment, pod cost breakdown
  • Idle resource alerts
  • Right-sizing recommendations
  • Multi-cluster cost allocation

Cloud-native alternatives:

  • AWS Cost Explorer: Filter by Kubernetes tags
  • GCP Billing: GKE cost breakdown
  • Azure Cost Management: AKS cost tracking

18. Production Readiness Checklist

Before going live, ensure you've addressed:

High Availability

  • Multiple replicas (minimum 3 for critical services)
  • Pod Disruption Budgets configured
yaml
  apiVersion: policy/v1
  kind: PodDisruptionBudget
  metadata:
    name: my-app-pdb
  spec:
    minAvailable: 2
    selector:
      matchLabels:
        app: my-app
  • Anti-affinity rules (spread pods across zones)
yaml
  affinity:
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - weight: 100
        podAffinityTerm:
          labelSelector:
            matchLabels:
              app: my-app
          topologyKey: topology.kubernetes.io/zone
  • Multi-zone cluster (AWS: 3 AZs, GCP: 3 zones)

Security

  • RBAC configured (no cluster-admin for users)
  • Network policies enabled
  • Secrets encrypted at rest (KMS integration)
  • Pod Security Standards enforced (restricted level)
  • Container images scanned (no HIGH/CRITICAL vulnerabilities)
  • Non-root containers (runAsNonRoot: true)
  • Read-only root filesystem where possible
  • Resource limits set on all pods

Observability

  • Metrics collection (Prometheus or cloud-native)
  • Log aggregation (ELK, Loki, or CloudWatch)
  • Distributed tracing (Jaeger, Zipkin, or cloud APM)
  • Health check endpoints implemented
  • Liveness and readiness probes configured
  • Alerting rules defined (high error rate, pod crashes, resource exhaustion)
  • SLO/SLI defined (target availability, latency)

Scalability

  • Horizontal Pod Autoscaler configured
  • Cluster Autoscaler enabled
  • Resource requests/limits tuned
  • Load testing completed (find breaking points)

Resilience

  • Graceful shutdown implemented (handle SIGTERM)
  • Circuit breakers for external dependencies
  • Retry logic with exponential backoff
  • Timeouts configured on all network calls
  • Database connection pooling configured

Disaster Recovery

  • Backup strategy defined (Velero, native snapshots)
  • Recovery procedures documented
  • Backup testing (restore from backup regularly)
  • Multi-region failover (if required)

Cost Management

  • Resource quotas per namespace
  • Cost monitoring enabled (Kubecost, cloud billing)
  • Spot instances for appropriate workloads
  • Idle resource alerts configured

Documentation

  • Architecture diagrams created
  • Runbooks for common incidents
  • On-call procedures documented
  • Deployment procedures automated (GitOps)

Official Kubernetes production checklist: Production Best Practices

Final Recommendations from SimplifiedTechhub

Start Simple, Scale Complexity as Needed

Phase 1: Learning (Local Development)

  • Use Minikube or Docker Desktop Kubernetes
  • Deploy simple applications (single Deployment + Service)
  • Focus on understanding core concepts

Phase 2: Non-Production (Staging/Dev)

  • Deploy to managed Kubernetes (GKE, EKS, AKS)
  • Add monitoring and logging
  • Implement CI/CD pipelines
  • Practice troubleshooting

Phase 3: Production (Critical Workloads)

  • Implement full security hardening
  • Add high availability and autoscaling
  • Set up comprehensive monitoring and alerting
  • Document everything

When to Get Expert Help

Consider SimplifiedTechhub's Premium Guidance when:

  • Migrating critical production workloads (risk of downtime)
  • Designing multi-cluster architectures (compliance, geo-distribution)
  • Implementing service mesh (Istio, Linkerd complexity)
  • Performance tuning (fixing bottlenecks, optimizing costs)
  • Security hardening (compliance requirements, audit preparation)
  • Disaster recovery planning (multi-region failover, backup strategies)

What you get:

  • Architecture design review
  • Hands-on implementation guidance
  • Production readiness assessments
  • Knowledge transfer to your team
  • 24/7 support during critical migrations

Resources for Continued Learning

Official Documentation:

Interactive Learning:

Books:

  • "Kubernetes Up and Running" by Kelsey Hightower
  • "Kubernetes Patterns" by Bilgin Ibryam
  • "Production Kubernetes" by Josh Rosso

Community:

Conclusion

Kubernetes is powerful but complex. The key to success is incremental adoption:

  1. Start with the basics: Understand Pods, Deployments, Services
  2. Build muscle memory: Deploy real applications to learn the patterns
  3. Add production concerns gradually: Security, monitoring, scaling
  4. Leverage managed services: Reduce operational burden
  5. Don't over-engineer: Use Kubernetes when it solves real problems

Remember: Kubernetes is a tool, not a goal. The objective is reliable, scalable applications—not Kubernetes itself.

Whether you're self-serving with these resources or working with SimplifiedTechhub's DevOps experts, you now have the foundation to deploy production-grade containerized applications with confidence.

Next steps:

  • Deploy your first application following Scenario 1
  • Set up local development with Skaffold or Tilt
  • Implement health checks and monitoring
  • Join the Kubernetes community

Need personalized guidance? SimplifiedTechhub's DevOps engineers are here to help you navigate complex migrations, optimize costs, and build resilient cloud-native infrastructure. 


Post a Comment

0 Comments