# Kubernetes Workload Design

> Guides expert-level kubernetes workload design implementation: cloud and architecture decision frameworks, production-ready patterns, and concrete templates for kubernetes workload design workflows. Use when the user asks about kubernetes workload design, kubernetes workload design configuration, or devops best practices for kubernetes projects. Do NOT use when the user needs a different devops cloud capability -- check sibling skills in the devops cloud subcategory.

- Skill: `ferroxlabs/kubernetes-workload-design` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add ferroxlabs/kubernetes-workload-design`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ferroxlabs/kubernetes-workload-design/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- License: Apache-2.0
- Author: FerroxLabs (https://skillmd.com/u/ferroxlabs)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ferroxlabs/kubernetes-workload-design

---

# Kubernetes Workload Design

## When to Use

**Use this skill when:**
- A user is selecting the correct Kubernetes workload type (Deployment, StatefulSet, DaemonSet, Job, CronJob) for a new or existing application component
- A user needs production-ready YAML for a service and is unsure how to configure resource requests/limits, liveness/readiness probes, pod disruption budgets, or affinity rules
- A user is experiencing pod evictions, OOMKills, or scheduling failures and needs to redesign their workload spec to fix root causes
- A user wants to migrate a stateful application (database, message broker, cache cluster) onto Kubernetes and needs guidance on StatefulSet design, PVC configuration, and headless services
- A user is designing a batch processing or data pipeline workload and needs to choose between Jobs, CronJobs, and controller-managed pod patterns
- A user is building a multi-tenant platform on Kubernetes and needs namespace isolation, resource quotas, and LimitRange design
- A user needs to design horizontally scalable workloads with HorizontalPodAutoscaler (HPA) or vertically scalable workloads with VerticalPodAutoscaler (VPA), including custom metrics integration

**Do NOT use this skill when:**
- The user needs cluster infrastructure design (node pools, control plane HA, etcd tuning) -- that falls under cluster architecture skills
- The user needs Kubernetes networking design (CNI selection, NetworkPolicy authoring, Service Mesh configuration) -- use the kubernetes-networking skill
- The user needs CI/CD pipeline design for delivering to Kubernetes -- use the kubernetes-delivery or gitops skill
- The user needs Kubernetes RBAC policy design in depth -- use the kubernetes-security skill
- The user is asking about Helm chart authoring or Kustomize overlay structure -- use the kubernetes-packaging skill
- The user needs cloud-provider-specific node or managed service configuration (EKS managed node groups, GKE Autopilot, AKS virtual nodes) -- use the cloud-provider-kubernetes skill
- The user is asking about general Docker image building or Dockerfile optimization -- use the container-image-design skill

---

## Process

### Step 1: Classify the workload type

Before writing any YAML, determine the correct API kind. Getting this wrong requires a full replacement (not a rolling update), causing downtime.

- **Deployment** -- use for stateless, horizontally scalable services where any pod can serve any request. Examples: REST APIs, frontend servers, worker processes that pull from a queue. Supports rolling updates natively. This covers 70-80% of workloads.
- **StatefulSet** -- use when pods need stable network identity (pod-0, pod-1), stable persistent storage tied to a specific pod, or ordered startup/shutdown. Examples: PostgreSQL, Kafka, Elasticsearch, Redis Cluster. Never use a Deployment with a PVC for a clustered database.
- **DaemonSet** -- use when exactly one pod must run on every node (or a subset of nodes). Examples: log shippers (Fluent Bit), node exporters (Prometheus node-exporter), network agents (Calico, Cilium). Do not use for application workloads.
- **Job** -- use for finite, one-shot tasks that must run to completion. Examples: database migrations, report generation, data import. Set `restartPolicy: OnFailure` and configure `backoffLimit` (default 6 -- reduce to 3 for fast-fail behavior). Set `ttlSecondsAfterFinished: 300` to auto-clean completed Jobs.
- **CronJob** -- use for periodic Jobs. Be aware that CronJob schedules run in the timezone of the kube-controller-manager; use `timeZone` field (Kubernetes 1.27+) to set an explicit timezone. Set `concurrencyPolicy: Forbid` for jobs that must not overlap; set `startingDeadlineSeconds: 60` to skip missed runs rather than running a cascade.
- **ReplicaSet** -- do not use directly; always manage through a Deployment.

Decision test: Ask "if I delete this pod, does the application care which pod replaced it?" If yes -- StatefulSet. If no -- Deployment.

### Step 2: Define resource requests and limits

Resource misconfiguration is the number one cause of production instability on Kubernetes. Every container must have requests and limits defined.

- **Requests** determine scheduling. The scheduler only places a pod on a node where `allocatable - sum(requests) >= container request`. If requests are too low, pods schedule onto crowded nodes and get evicted under pressure.
- **Limits** determine enforcement. CPU limits throttle (not kill). Memory limits kill via OOMKill. Setting CPU limits too low causes latency spikes from throttling -- consider omitting CPU limits for latency-sensitive workloads and relying on namespace LimitRange defaults.
- **Golden ratio**: Set memory limit at 2x the memory request for most workloads. For JVM services (Java, Kotlin, Scala), set memory limit at 1.25x the heap max (-Xmx) plus 512Mi for off-heap overhead.
- **Starting benchmarks for sizing**: A typical Node.js API pod: `requests: {cpu: "100m", memory: "128Mi"}`, `limits: {cpu: "500m", memory: "256Mi"}`. A Java Spring Boot pod: `requests: {cpu: "250m", memory: "512Mi"}`, `limits: {cpu: "1000m", memory: "1Gi"}`.
- Never set CPU request below 10m -- the kernel scheduler granularity makes this meaningless.
- Enable VPA in recommendation mode first (not enforcement) to gather real usage data before setting final values.
- Use `resources.requests` to define QoS class: if requests == limits on all containers, the pod gets `Guaranteed` QoS -- it will never be evicted before `Burstable` or `BestEffort` pods.

### Step 3: Design health probes

Probes are the mechanism Kubernetes uses to route traffic and restart failed containers. Misconfigured probes cause more incidents than almost any other workload design error.

- **livenessProbe** -- signals that the container is alive and does not need to be restarted. Use conservatively. If the liveness probe trips during a slow startup or GC pause, Kubernetes restarts the pod, potentially causing a cascade. Set `initialDelaySeconds` long enough for the slowest observed startup (add 50% buffer). Typical: `initialDelaySeconds: 30`, `periodSeconds: 10`, `failureThreshold: 3`.
- **readinessProbe** -- signals that the container is ready to receive traffic. This is the most important probe. A failing readiness probe removes the pod endpoint from all Services and Ingress backends without restarting the pod. Use for dependency checks (database connection, cache warmup). Set `periodSeconds: 5`, `failureThreshold: 3` (15 seconds to detect unhealthy).
- **startupProbe** -- introduced in Kubernetes 1.18, prevents liveness probe from killing a slow-starting container. Set `failureThreshold: 30`, `periodSeconds: 10` to give a container up to 300 seconds to start. Once startupProbe succeeds, liveness takes over. Always use startupProbe for JVM or Python applications.
- **Probe implementation**: Prefer `httpGet` over `exec` for HTTP services -- exec probes fork a shell process, which is expensive under load. Use `tcpSocket` only as a last resort (it proves the port is open, not that the application is healthy).
- Never make a liveness probe check external dependencies (database, downstream APIs). If the database is down, all pods will restart simultaneously -- a thundering herd. Liveness should only check internal application health (deadlock detection, memory exhaustion).
- The readiness probe endpoint should check real dependency health (can it query the database, can it reach the cache).

### Step 4: Configure pod disruption budgets and availability guarantees

A pod disruption budget (PDB) is the primary mechanism to guarantee availability during voluntary disruptions (node drains, cluster upgrades). Without PDBs, a rolling node drain can take all pods of a Deployment offline simultaneously.

- For any production Deployment with 2+ replicas, create a PDB with `minAvailable: 1` at minimum. For critical services, use `maxUnavailable: 0` (zero disruption) or `minAvailable: "80%"`.
- PDBs only protect against *voluntary* disruptions (evictions via `kubectl drain`, Cluster Autoscaler scale-down). They do not protect against node hardware failures.
- Set `spec.strategy.rollingUpdate.maxUnavailable: 0` and `maxSurge: 1` on Deployments for zero-downtime rolling updates. This means during an update, Kubernetes creates a new pod before terminating an old one -- requiring enough capacity headroom.
- Set `terminationGracePeriodSeconds` to match your application's shutdown time. Default is 30 seconds. For services that process long-running requests (streaming, batch), set this to 60-120 seconds and implement graceful shutdown (catch SIGTERM, stop accepting new connections, drain in-flight work, exit).
- For StatefulSets, `spec.podManagementPolicy: Parallel` enables faster scaling at the cost of ordered startup -- only use for stateless-like stateful sets (e.g., read replicas). Use the default `OrderedReady` for clustered databases.

### Step 5: Design scheduling controls

Without scheduling controls, Kubernetes places pods wherever capacity exists, which causes availability failures, noisy neighbor problems, and uneven resource utilization.

- **Pod anti-affinity** -- spread replicas across failure domains. Use `requiredDuringSchedulingIgnoredDuringExecution` (hard) for critical services that must never co-locate on the same node. Use `preferredDuringSchedulingIgnoredDuringExecution` (soft) for non-critical services to allow scheduling even if anti-affinity cannot be fully satisfied. Never use hard anti-affinity for a service where `replicas > node count` -- pods will be pending forever.
- **Topology spread constraints** (preferred over anti-affinity for >= Kubernetes 1.19): Use `topologySpreadConstraints` with `maxSkew: 1` and `topologyKey: topology.kubernetes.io/zone` to spread pods evenly across availability zones. Set `whenUnsatisfiable: DoNotSchedule` for critical services, `ScheduleAnyway` for flexible ones.
- **Node affinity and taints/tolerations**: Use node affinity to target specific node pools (e.g., GPU nodes, high-memory nodes). Use taints/tolerations to dedicate nodes to specific workload classes. A common pattern: taint GPU nodes with `dedicated=gpu:NoSchedule` and add the matching toleration only to GPU workloads.
- **Priority classes**: Create a `PriorityClass` for production workloads (value: 1000) and a lower class for batch/background (value: 100). Kubernetes will preempt lower-priority pods to schedule higher-priority ones during resource pressure.
- **Pod topology awareness for Services**: Set `service.spec.internalTrafficPolicy: Local` to route service traffic only to pods on the same node -- reduces cross-zone egress costs but requires pods to be present on every node that runs clients.

### Step 6: Design storage for stateful workloads

Storage is the hardest part of Kubernetes workload design. Most production incidents involving StatefulSets are storage-related.

- **StorageClass selection**: Use the `WaitForFirstConsumer` volume binding mode to defer PVC binding until a pod is scheduled. This prevents a PVC from being bound to a zone where the pod cannot schedule. Always use this mode for zonal storage (AWS EBS, GCE PD).
- **PVC sizing**: Kubernetes cannot shrink PVCs (most CSI drivers). Provision 2x the expected initial data size and enable volume expansion (`allowVolumeExpansion: true` on the StorageClass). For StatefulSets, the `volumeClaimTemplates` field creates one PVC per pod -- deleting the StatefulSet does NOT delete PVCs (intentional, to protect data).
- **Access modes**: `ReadWriteOnce` (RWO) is supported by nearly all CSI drivers and is the correct mode for databases. `ReadWriteMany` (RWX) requires specialized storage (NFS, CephFS, AWS EFS) -- never assume your cloud block storage supports RWX.
- **Init containers for data initialization**: Use an init container to copy seed data or run schema migrations before the main container starts. The init container shares the same volume mount.
- **Backup strategy**: Do not rely on PVC snapshots alone. Implement application-consistent backups (e.g., `pg_dump` for PostgreSQL, Velero for cluster-level backups). Schedule backups via CronJob with the backup tool running as a sidecar or standalone pod.

### Step 7: Configure autoscaling

Autoscaling in Kubernetes has three dimensions. Understand all three before configuring any of them.

- **Horizontal Pod Autoscaler (HPA)**: Scales the number of pod replicas based on metrics. The default metric is CPU utilization relative to requests. Set `targetCPUUtilizationPercentage: 70` for CPU-bound services. For I/O-bound or queue-processing services, use custom metrics via the Prometheus Adapter or KEDA (Kubernetes Event-Driven Autoscaler). Always set `minReplicas: 2` for production (never 1 -- a single pod cannot be disrupted safely). Set `behavior.scaleDown.stabilizationWindowSeconds: 300` to prevent aggressive scale-down that causes cold-start latency spikes.
- **Vertical Pod Autoscaler (VPA)**: Adjusts CPU/memory requests based on observed usage. Do NOT run VPA in `Auto` mode alongside HPA CPU scaling -- they conflict, causing oscillation. Use VPA in `Off` mode for recommendations, or pair VPA with HPA only if HPA uses custom metrics (not CPU). VPA requires pod restarts to apply new resource values.
- **Cluster Autoscaler / Karpenter**: Provisions new nodes when pods are pending due to insufficient capacity. Design your node pool sizes so that one Cluster Autoscaler node can accommodate the largest single pod you run, plus overhead. Karpenter (AWS-native, increasingly used on other clouds) is faster and more cost-efficient than Cluster Autoscaler -- it provisions exact-fit instances rather than pre-defined node groups.
- Set HPA `scaleUp.policies` with a reasonable rate limit: `periodSeconds: 60, value: 4` means at most 4 new pods per minute. This prevents a sudden traffic spike from provisioning dozens of pods simultaneously.

---

## Output Format

When producing a workload design, deliver the following structured artifacts:

### Workload Classification Summary

```
Workload Name:       [service-name]
API Kind:            [Deployment | StatefulSet | DaemonSet | Job | CronJob]
Rationale:          [one sentence justification]
Replica Count:       [number] (min: [HPA min] / max: [HPA max])
QoS Class:          [Guaranteed | Burstable | BestEffort]
Storage Required:    [Yes/No -- type and size if yes]
```

### Primary Workload YAML

```yaml
# deployment.yaml -- production-ready Deployment for [service-name]
# QoS: Burstable | Replicas: 3 initial | HPA: 2-10
apiVersion: apps/v1
kind: Deployment
metadata:
  name: [service-name]
  namespace: [namespace]
  labels:
    app.kubernetes.io/name: [service-name]
    app.kubernetes.io/component: [api | worker | frontend]
    app.kubernetes.io/part-of: [platform-name]
    app.kubernetes.io/version: "1.0.0"
spec:
  replicas: 3
  selector:
    matchLabels:
      app.kubernetes.io/name: [service-name]
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0    # zero-downtime: never remove old pod before new one is ready
      maxSurge: 1          # allow one extra pod during rollout
  template:
    metadata:
      labels:
        app.kubernetes.io/name: [service-name]
        app.kubernetes.io/version: "1.0.0"
    spec:
      serviceAccountName: [service-name]  # never use default SA
      terminationGracePeriodSeconds: 60   # match application drain time
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        fsGroup: 2000
        seccompProfile:
          type: RuntimeDefault
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app.kubernetes.io/name: [service-name]
      containers:
        - name: [service-name]
          image: [registry]/[service-name]:[tag]
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 8080
              protocol: TCP
          resources:
            requests:
              cpu: "100m"      # sizing: adjust based on VPA recommendation after 7 days
              memory: "128Mi"
            limits:
              # cpu limit intentionally omitted to avoid throttling latency
              memory: "256Mi"  # 2x request -- adjust for observed p99 usage
          startupProbe:
            httpGet:
              path: /healthz
              port: http
            failureThreshold: 30   # allows up to 300s for startup
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /readyz        # checks DB connection, cache connectivity
              port: http
            initialDelaySeconds: 5
            periodSeconds: 5
            failureThreshold: 3    # 15s to detect unhealthy
            successThreshold: 1
          livenessProbe:
            httpGet:
              path: /healthz       # internal-only check, no external deps
              port: http
            initialDelaySeconds: 30
            periodSeconds: 10
            failureThreshold: 3
            successThreshold: 1
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: POD_NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
          envFrom:
            - configMapRef:
                name: [service-name]-config
            - secretRef:
                name: [service-name]-secrets  # secrets managed externally (ESO, Vault)
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - name: tmp
              mountPath: /tmp               # writable temp dir for read-only root fs
      volumes:
        - name: tmp
          emptyDir: {}
```

### Supporting Resources YAML

```yaml
---
# pdb.yaml -- Pod Disruption Budget
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: [service-name]-pdb
  namespace: [namespace]
spec:
  minAvailable: 1   # for 3 replicas: ensures 2 pods always available during drain
  selector:
    matchLabels:
      app.kubernetes.io/name: [service-name]
---
# hpa.yaml -- Horizontal Pod Autoscaler
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: [service-name]-hpa
  namespace: [namespace]
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: [service-name]
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0      # scale up immediately on demand
      policies:
        - type: Pods
          value: 4
          periodSeconds: 60              # max 4 new pods per minute
    scaleDown:
      stabilizationWindowSeconds: 300    # wait 5 min before scaling down
      policies:
        - type: Percent
          value: 25
          periodSeconds: 60             # remove at most 25% of pods per minute
```

### Decision Matrix

| Factor | Deployment | StatefulSet | DaemonSet | Job/CronJob |
|--------|-----------|-------------|-----------|-------------|
| Pod identity matters | No | Yes | N/A | No |
| Persistent per-pod storage | No | Yes | Optional | No |
| Ordered startup required | No | Yes | No | No |
| Runs on every node | No | No | Yes | No |
| Runs to completion | No | No | No | Yes |
| Supports HPA | Yes | Limited | No | No |
| Rolling updates | Yes | Yes (ordered) | Yes | No |

### Operational Checklist

```
[ ] Resource requests and limits set on all containers
[ ] startupProbe configured (especially for JVM/Python services)
[ ] readinessProbe checks real dependency health
[ ] livenessProbe does NOT check external dependencies
[ ] PodDisruptionBudget created
[ ] terminationGracePeriodSeconds >= application drain time
[ ] topologySpreadConstraints configured for multi-AZ
[ ] ServiceAccount created (not using default)
[ ] readOnlyRootFilesystem: true with emptyDir for /tmp
[ ] runAsNonRoot: true
[ ] Image tag is not "latest"
[ ] HPA minReplicas >= 2
[ ] PriorityClass assigned
```

---

## Rules

1. **Never use the `default` ServiceAccount for application pods.** The default ServiceAccount in most clusters has auto-mounted credentials that grant broader API access than intended. Create a dedicated ServiceAccount per workload and set `automountServiceAccountToken: false` unless the pod explicitly needs API server access.

2. **Never use `latest` as an image tag in production manifests.** The `latest` tag is mutable -- a redeployment with the same manifest can silently pull a different image. Always pin to a digest (`image@sha256:...`) or an immutable tag (git SHA or build number). This is non-negotiable for production.

3. **Never set memory requests and limits to the same value unless you explicitly need Guaranteed QoS.** Setting request == limit creates a Guaranteed QoS pod but also means any memory usage over the request triggers an OOMKill. Leave headroom between request and limit unless the workload is latency-critical and you have profiling data to justify exact sizing.

4. **Never make a liveness probe check external dependencies.** If a database goes down and the liveness probe fails, Kubernetes restarts all pods simultaneously, amplifying the outage. Liveness probes must only check internal application state.

5. **Never configure a StatefulSet without a headless Service (`clusterIP: None`).** StatefulSets require a governing headless Service to create stable DNS entries (`pod-0.service.namespace.svc.cluster.local`). Without it, peer discovery for clustered databases and message brokers will not work correctly.

6. **Always set `terminationGracePeriodSeconds` greater than or equal to your application's observed shutdown time.** If the grace period expires before the app finishes draining, the kubelet sends SIGKILL, immediately terminating in-flight requests. Measure your actual shutdown time under load and add 20% buffer.

7. **Never run a single replica in production for any user-facing service.** A single replica cannot safely tolerate a node drain (voluntary disruption) because the PDB `minAvailable: 1` would block the drain entirely, or if no PDB exists, the pod is simply evicted. Always deploy a minimum of 2 replicas and configure a PDB.

8. **Always use `autoscaling/v2` HPA API, not `autoscaling/v1`.** The v1 API only supports CPU-based scaling and is deprecated. The v2 API supports memory, custom metrics (via Prometheus Adapter), and external metrics (via KEDA). Kubernetes 1.23+ defaults to v2.

9. **Never mix VPA (in Auto mode) and HPA (on CPU metric) on the same Deployment.** VPA in Auto mode will continuously adjust resource requests, which changes the denominator for CPU utilization percentage, causing HPA to oscillate. Use VPA for recommendations only, or use HPA with custom metrics (request rate, queue depth) instead of CPU when VPA Auto mode is needed.

10. **Always label pods with the standard Kubernetes recommended labels (`app.kubernetes.io/*`).** Ad-hoc label schemas break tooling, dashboards, and service meshes that rely on label conventions. At minimum: `app.kubernetes.io/name`, `app.kubernetes.io/version`, `app.kubernetes.io/component`, `app.kubernetes.io/part-of`. This is required for PDB label selectors to work predictably and for cost attribution in multi-tenant clusters.

---

## Edge Cases

### 1. JVM-based services with heap sizing and OOMKill loops

JVM services (Java, Kotlin, Scala) are the most commonly misconfigured workloads because the JVM manages its own heap and the container memory limit operates at the cgroup level. If `-Xmx` is not set, the JVM defaults to 25% of detected memory, which in a container is the node's total RAM -- far exceeding the limit.

**Handling**: Always set explicit JVM flags: `-Xms256m -Xmx512m -XX:MaxMetaspaceSize=256m -XX:+UseContainerSupport`. The `+UseContainerSupport` flag (enabled by default in JDK 11+) makes the JVM respect cgroup limits. Set the container memory limit to `Xmx + Metaspace + 512Mi` for off-heap overhead (native memory, thread stacks, JIT compiled code). For a 512Mi heap, set limit to `512Mi + 256Mi + 512Mi = 1280Mi`. Use startupProbe with `failureThreshold: 30, periodSeconds: 10` to allow up to 5 minutes for JIT warmup.

### 2. Migrations and schema changes with StatefulSets

Running database schema migrations before application startup is a common requirement that creates ordering challenges in Kubernetes.

**Handling**: Use a Kubernetes `Job` (not an init container in the StatefulSet) to run migrations. The Job runs `flyway migrate` or `alembic upgrade head` and exits. The application Deployment depends on the Job completing -- implement this ordering via an init container in the Deployment that waits for a migration-complete ConfigMap or the Job's completion status. In CI/CD pipelines, sequence the Job apply before the Deployment apply with a `kubectl wait --for=condition=complete job/migrate --timeout=300s` check. Never run migrations as part of application startup code in a multi-replica Deployment -- concurrent migrations from multiple pods will cause lock contention or duplicate migrations.

### 3. Stateful workloads with zone-pinned storage

Cloud block storage (AWS EBS, GCE PD, Azure Disk) is zonal -- a PVC created in zone us-east-1a cannot be attached to a pod in us-east-1b. StatefulSet pods with PVCs can become permanently unschedulable after a zone failure.

**Handling**: Use `WaitForFirstConsumer` volume binding mode on the StorageClass to defer PVC creation until pod scheduling. For truly zone-resilient StatefulSets, configure a storage solution that supports cross-zone replication (Amazon EFS, GCP Filestore, Portworx, or Rook-Ceph). For database StatefulSets, design the application layer for active-passive failover across zones (PostgreSQL with Patroni, MySQL with Orchestrator) rather than assuming Kubernetes can transparently move stateful pods across zones. Document in the runbook which zone each StatefulSet pod is pinned to.

### 4. Burst traffic with slow scaling

HPA has a polling interval of 15 seconds by default and the Cluster Autoscaler takes 1-3 minutes to provision a new node. A sudden 10x traffic spike will cause request failures for 2-5 minutes before autoscaling catches up.

**Handling**: Set `minReplicas` higher than the baseline steady-state needs to maintain a warm buffer -- typically target 50-60% average CPU utilization at steady state rather than 80-90%. For services with predictable traffic spikes (scheduled promotions, market open), use KEDA's `CronScaler` trigger to pre-scale before the spike. Implement application-level circuit breakers and retry with exponential backoff so clients survive the 30-60 second window before scaling completes. Set HPA `scaleUp.stabilizationWindowSeconds: 0` to allow immediate upward scaling without any delay.

### 5. Privileged workloads that cannot run with restricted security contexts

Some workloads (log agents, network tools, storage provisioners) require elevated privileges. Applying `readOnlyRootFilesystem: true` or `runAsNonRoot: true` will break them.

**Handling**: Use Kubernetes Pod Security Admission (PSA) with the `privileged` policy namespace label only for infrastructure namespaces. Keep application namespaces under `restricted` or `baseline` policy. For specific containers that need elevated privileges (e.g., a log shipper sidecar that needs to read `/var/log`), place them in the DaemonSet on a dedicated infrastructure namespace rather than as sidecars in application pods. When a DaemonSet truly needs host path access, add explicit audit logging and document the business justification. Never relax security contexts across an entire cluster to accommodate one workload.

### 6. Long-running Jobs that must survive node preemption

Spot/preemptible instances can be reclaimed with 2-minute notice. A 4-hour batch Job running on a spot node will be lost mid-execution.

**Handling**: Set `spec.backoffLimit: 3` and implement checkpointing in the Job's application code -- write progress to a PVC or external storage at regular intervals (every 5 minutes or every N records processed). On restart, the application reads the checkpoint and resumes. Use `spec.completionMode: Indexed` for parallel Jobs so each pod processes a partition of work. Set a PriorityClass of `low` for batch Jobs so they are the first candidates for preemption when node capacity is needed for higher-priority workloads. Use node affinity to schedule batch Jobs on spot node pools and critical services on on-demand pools.

### 7. Canary and blue-green deployments within a single namespace

Kubernetes Deployment rolling updates do not natively support traffic splitting by percentage. A naive `replicas: 9 (v1) + 1 (v2)` approach gives roughly 10% traffic to v2, but this requires running 10 total pods permanently.

**Handling**: Use a Service Mesh (Istio VirtualService, Linkerd TrafficSplit) for percentage-based traffic splitting without requiring proportional replica counts. If a service mesh is unavailable, use two Deployments (`service-v1` and `service-v2`) with a single Service selector on a shared label (`app: service`), and vary the `replicas` ratio between them (9:1, 5:5, 0:10). Use Flagger for automated canary promotion based on Prometheus metrics (error rate < 1%, p99 latency < 500ms). Never delete the old Deployment before the new Deployment is confirmed healthy under production traffic.

### 8. Workloads requiring GPU resources

GPU scheduling in Kubernetes requires device plugin configuration and careful resource management because GPUs cannot be fractionally allocated by default.

**Handling**: Request GPUs with `resources.limits: nvidia.com/gpu: "1"` -- note that GPU resources must be specified in limits, not requests (they are always equal). Use node affinity to target GPU node pools. For workloads that need less than a full GPU (inference workloads), use NVIDIA's time-slicing feature (configured in the device plugin ConfigMap) to share one physical GPU among multiple pods. Set `runtimeClassName: nvidia` on the pod spec to use the NVIDIA container runtime. Never schedule GPU workloads without a dedicated namespace and PriorityClass -- GPU nodes are expensive and should not be wasted by misconfigured lower-priority workloads preempting capacity.

---

## Example

**Input:** "We have a Python FastAPI service that serves a recommendation engine. It reads from PostgreSQL and Redis, starts slowly (about 45 seconds due to model loading), handles ~500 req/s at peak, and our team got burned last month when a rolling deployment caused 30 seconds of 503 errors. We're running on Kubernetes 1.28 on AWS EKS. How should we design the workload?"

---

**Output:**

**Workload Classification Summary:**
```
Workload Name:       recommendation-api
API Kind:            Deployment
Rationale:          Stateless -- any pod can serve any request; no per-pod storage
                    identity needed
Replica Count:       4 baseline (min: 3 / max: 12 via HPA)
QoS Class:          Burstable (memory limits set, CPU limits omitted)
Storage Required:    No (model loaded from S3 at startup via init container)
```

**Root Cause of the 503 Errors -- and Fix:**

The 503s during rolling deployment were caused by one or both of:
1. The new pod receiving traffic before the model finished loading (readiness probe was missing or checked only the port, not model-ready state)
2. The old pod being terminated before it finished draining in-flight requests

The fix requires: a startupProbe that accounts for the 45-second startup, a readinessProbe that checks the `/readyz` endpoint (which returns 200 only after model load completes), and `terminationGracePeriodSeconds: 75` to allow in-flight requests to complete.

**recommendation-api deployment.yaml:**

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: recommendation-api
  namespace: ml-serving
  labels:
    app.kubernetes.io/name: recommendation-api
    app.kubernetes.io/component: api
    app.kubernetes.io/part-of: recommendation-platform
    app.kubernetes.io/version: "2.4.1"
spec:
  replicas: 4
  selector:
    matchLabels:
      app.kubernetes.io/name: recommendation-api
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0   # NEVER remove a pod until the replacement is fully ready
      maxSurge: 1         # run 5 pods briefly during rollout -- ensure capacity headroom
  template:
    metadata:
      labels:
        app.kubernetes.io/name: recommendation-api
        app.kubernetes.io/version: "2.4.1"
    spec:
      serviceAccountName: recommendation-api
      # 75s grace: 60s for in-flight request drain + 15s buffer
      terminationGracePeriodSeconds: 75
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        fsGroup: 2000
        seccompProfile:
          type: RuntimeDefault
      # Spread across all 3 AZs to survive zone failure
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              app.kubernetes.io/name: recommendation-api
      initContainers:
        # Download model artifact from S3 before main container starts
        - name: model-downloader
          image: 123456789.dkr.ecr.us-east-1.amazonaws.com/model-downloader:v1.2
          command: ["aws", "s3", "cp", "s3://ml-models/rec-model-v7.pkl", "/model/"]
          resources:
            requests:
              cpu: "100m"
              memory: "256Mi"
            limits:
              memory: "512Mi"
          volumeMounts:
            - name: model-storage
              mountPath: /model
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
      containers:
        - name: recommendation-api
          image: 123456789.dkr.ecr.us-east-1.amazonaws.com/recommendation-api:sha-a3f8c1d
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 8080
              protocol: TCP
          resources:
            requests:
              cpu: "500m"     # Python w/ numpy: I/O bound, generous request for scheduling
              memory: "1Gi"   # model in memory ~700Mi + Python overhead
            limits:
              # CPU limit omitted: Python GIL + numpy are bursty; throttling causes
              # p99 latency spikes that are worse than allowing occasional CPU burst
              memory: "2Gi"   # 2x request; OOMKill before noisy neighbor eviction
          startupProbe:
            httpGet:
              path: /healthz   # returns 200 immediately (pre-model-load, proves process up)
              port: http
            failureThreshold: 12  # 12 * 5s = 60s to start -- covers 45s model load + buffer
            periodSeconds: 5
          readinessProbe:
            httpGet:
              path: /readyz    # returns 200 ONLY after model loaded AND pg/redis reachable
              port: http
            initialDelaySeconds: 5
            periodSeconds: 5
            failureThreshold: 3   # 15s to detect unhealthy dependency
            successThreshold: 1
          livenessProbe:
            httpGet:
              path: /healthz   # internal only: checks for Python deadlocks, not pg/redis
              port: http
            initialDelaySeconds: 60  # longer than startup to avoid restart during model load
            periodSeconds: 15
            failureThreshold: 3
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: WORKERS
              value: "4"              # Uvicorn workers: match vCPU count on target node type
            - name: MODEL_PATH
              value: "/model/rec-model-v7.pkl"
            - name: LOG_LEVEL
              value: "info"
          envFrom:
            - configMapRef:
                name: recommendation-api-config
            - secretRef:
                name: recommendation-api-secrets  # DB_URL, REDIS_URL via External Secrets
          lifecycle:
            preStop:
              exec:
                # Signal Uvicorn to stop accepting new connections; wait for drain
                command: ["/bin/sh", "-c", "sleep 5 && kill -SIGTERM 1"]
          securityContext:
            allowPrivilegeEscalation: false
            readOnlyRootFilesystem: true
            capabilities:
              drop: ["ALL"]
          volumeMounts:
            - name: model-storage
              mountPath: /model
              readOnly: true
            - name: tmp
              mountPath: /tmp
      volumes:
        - name: model-storage
          emptyDir: {}          # populated by init container from S3
        - name: tmp
          emptyDir: {}
```

**recommendation-api-pdb.yaml:**
```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: recommendation-api-pdb
  namespace: ml-serving
spec:
  # With 4 replicas: always keep 3 available during node drains or EKS upgrades
  minAvailable: 3
  selector:
    matchLabels:
      app.kubernetes.io/name: recommendation-api
```

**recommendation-api-hpa.yaml:**
```yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: recommendation-api-hpa
  namespace: ml-serving
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: recommendation-api
  minReplicas: 3    # never below 3: PDB requires minAvailable 3
  maxReplicas: 12
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60   # lower target: Python is bursty; leave headroom
    # Custom metric: scale on request rate if CPU is not the bottleneck
    - type: Pods
      pods:
        metric:
          name: http_requests_per_second   # from Prometheus Adapter
        target:
          type: AverageValue
          averageValue: "150"              # 150 req/s per pod; 4 pods = 600 req/s capacity
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 0    # scale up immediately on demand
      policies:
        - type: Pods
          value: 3
          periodSeconds: 60            # add max 3 pods per minute
    scaleDown:
      stabilizationWindowSeconds: 300  # wait 5 minutes before scaling down
      policies:
        - type: Percent
          value: 25
          periodSeconds: 120
```

**Explanation of Key Design Decisions:**

1. **startupProbe with failureThreshold: 12 at 5s intervals** -- gives the pod 60 seconds to complete the 45-second model load, with a 15-second buffer. The startupProbe preventing the liveness probe from firing during startup is what eliminates the restart loop during slow startup.

2. **readinessProbe checks `/readyz` not `/healthz`** -- the `/readyz` endpoint in the FastAPI app should check `psycopg2.connect()` and `redis.ping()`. A failed database check removes the pod from the load balancer without restarting it, allowing the database to recover without a pod restart storm.

3. **maxUnavailable: 0 and maxSurge: 1** -- this is the direct fix for the 503 errors. The old behavior likely had `maxUnavailable: 1` (the default), which allowed one pod to be removed before a replacement was ready. With `maxUnavailable: 0`, Kubernetes will only terminate an old pod after the new pod passes its readinessProbe -- including the 45-second model load time.

4. **terminationGracePeriodSeconds: 75** -- the `preSto

…(truncated)
