AI Intermediate: Infrastructure & Agentic SystemsDay 9 of 30 · Week 2: Kubernetes Foundation for MLOps
Course outline▾

Week 2 · Kubernetes Foundation for MLOps

  1. Day 8K3d and Lightweight Kubernetes — Running Multi-Node Clusters on 16 GB of RAM
  2. Day 9Kubernetes Core for Machine Learning — Pods, Deployments and Headless Services
  3. Day 10Persistent Volumes (PV/PVC): Managing Heavy Model Weights and Vector Stores
  4. Day 11StatefulSets for Vector Databases: Deploying Milvus or Qdrant in Kubernetes
  5. Day 12Auto-Scaling AI Inference: Using Horizontal Pod Autoscalers (HPA) for Spiky API Traffic
  6. Day 13Helm Charts: Packaging, Versioning, and Updating AI Deployments
  7. Day 14Week 2 Capstone: Deploying a Highly Available Local AI Application on K3d

Week 3 · High-Performance Networking & Security Mesh

  1. Day 15AI Network Bottlenecks: Transitioning from iptables to eBPF
  2. Day 16Cilium & eBPF: Sub-Millisecond Routing for Heavy AI Microservices
  3. Day 17Service Mesh 101: Installing Istio over Your Local Cluster
  4. Day 18Envoy Proxy as an AI Gateway: Load Balancing and Terminating LLM Prompts
  5. Day 19Zero-Trust mTLS: Encrypting Traffic Between Autonomous Agents
  6. Day 20API Rate Limiting & Egress Control: Preventing Agents from Exfiltrating Data
  7. Day 21Distributed Tracing: Debugging Agent Workflows with Jaeger and OpenTelemetry

Week 4 · Enterprise Agents & CI/CD Workflows

  1. Day 22Introduction to MLOps: Bridging AI Development with Continuous Integration
  2. Day 23GitOps with ArgoCD: Automated, Self-Healing AI Deployments
  3. Day 24Building Ephemeral AI Sandboxes: Secure Execution Environments for Agent Code
  4. Day 25Multi-Agent Orchestration Frameworks: Exploring Mastra, LangGraph, and CrewAI
  5. Day 26Tool-Calling in Production: Connecting Agents Securely to Internal Databases
  6. Day 27MLOps Guardrails: Evaluating Agents and Preventing Hallucinations in Real-Time
  7. Day 28Continuous Training Pipelines: Connecting Data Ingestion to Automated Model Retraining
  8. Day 29Managing Technical Debt and Infrastructure Drift in Enterprise AI
  9. Day 30The AI-Native Platform: Scaling from a Sandbox Environment to Global Cloud Architecture
Open the course page →

Day 9: Kubernetes Core for Machine Learning — Pods, Deployments and Headless Services

2026-11-15 · 8 min read

A basic Deployment suits a stateless web server. Model serving and vector stores add failure domains that web apps rarely have: memory-exhaustion crashes, cold-boot timeouts and broken load-balancing rings. This lesson covers the Kubernetes building blocks that handle them.

Deployments and Services for inference

An inference pod ingests context, generates tokens and discards its KV-cache. It is stateless, so a Deployment gives you horizontal scaling, rolling updates and automatic restarts. A ClusterIP Service puts one stable address in front of the pods and sends traffic only to those that pass their readiness probe, so a saturated pod can step out of rotation without being killed.

A SERVICE ROUTES ONLY TO READY PODSRequest/v1/chat/completionsClusterIPinference-svcPod 1readyPod 2readyPod 3KV-cache saturatedremoved from endpoints until ready againA failing readiness probe takes a pod out of rotation without killing it.
FIG 1 · The Service sends requests only to pods whose readiness probe passes. A saturated pod is skipped until it recovers. Tune the probe so it does not remove every pod at once.

Headless Services for peers

A normal Service gives one virtual IP. A headless Service (clusterIP: None) makes CoreDNS return one A record per pod, such as pod-0.db-svc and pod-1.db-svc. Distributed databases such as Qdrant and Milvus use that to find their peers.

CLUSTERIP AGAINST HEADLESS SERVICEClusterIP (default)DNS: inference-svc→ ONE virtual IPkube-proxy picks a podgood for stateless inferenceHeadless (clusterIP: None)DNS: db-svc→ one A record PER podpod-0.db-svc pod-1.db-svcgood for peer discoveryDistributed databases like Qdrant and Milvus need to find each other by name.
FIG 2 · A normal Service gives one virtual IP and load-balances. A headless Service (clusterIP None) makes DNS return each pod's own address, which database peers use to discover each other.

A model-serving manifest

apiVersion: apps/v1
kind: Deployment
metadata:
  name: inference-engine-gemma
  namespace: ai-platform
spec:
  replicas: 2
  strategy:
    type: RollingUpdate
    rollingUpdate: { maxSurge: 1, maxUnavailable: 0 }
  selector:
    matchLabels: { app: inference-engine-gemma }
  template:
    metadata:
      labels: { app: inference-engine-gemma }
    spec:
      containers:
        - name: vllm
          image: vllm/vllm-openai:v0.5.0        # pin a tested tag, not "latest"
          args:
            - --model=/models/gemma-2-2b-it-awq
            - --quantization=awq
            - --max-model-len=4096
            - --gpu-memory-utilization=0.85
          ports: [{ name: http-api, containerPort: 8000 }]
          resources:
            requests: { cpu: "2000m", memory: 4Gi }
            limits:   { cpu: "4000m", memory: 8Gi }   # add nvidia.com/gpu: 1 on GPU nodes
          startupProbe:
            httpGet: { path: /health, port: http-api }
            initialDelaySeconds: 15
            periodSeconds: 10
            failureThreshold: 18                  # 15 + 18 x 10 = about 3 minutes to load
          readinessProbe:
            httpGet: { path: /v1/models, port: http-api }
            periodSeconds: 5
            timeoutSeconds: 2
          livenessProbe:
            httpGet: { path: /health, port: http-api }
            periodSeconds: 15
            failureThreshold: 3
          volumeMounts:
            - { name: model-weights, mountPath: /models, readOnly: true }
      volumes:
        - name: model-weights
          persistentVolumeClaim: { claimName: shared-model-weights-pvc }
---
apiVersion: v1
kind: Service
metadata:
  name: inference-engine-service
  namespace: ai-platform
spec:
  type: ClusterIP
  selector: { app: inference-engine-gemma }
  ports: [{ port: 8000, targetPort: http-api }]

Treat this as a structure to adapt. vLLM needs a GPU in practice, so on GPU nodes you would also request nvidia.com/gpu: 1 and run the NVIDIA device plugin, and the model files must exist on the volume you mount (Day 10).

Why the startupProbe is critical

Inference runtimes read gigabytes of weights before they open a network socket. If you rely on a livenessProbe alone, Kubernetes sees a silent port, concludes the pod is dead and restarts it, over and over. The startupProbe holds off the other probes until the server first passes, and here it allows about 3 minutes.

WHY A startupProbe IS CRITICAL FOR MODEL SERVERS0 s195 stime →loading weights, socket closed: up to 195 sservingWithout it, livenessProbe sees a silent port and restarts the pod, forever.startupProbe: 15 s delay + 18 failures × 10 s = about 3 minutes of gracethen readinessProbe gates traffic and livenessProbe watches for hangs
FIG 3 · Model servers read gigabytes of weights before opening a socket. A startupProbe holds off the liveness check during that window, so Kubernetes does not kill the pod mid-load.

After startup, the readinessProbe decides whether the pod receives traffic, and the livenessProbe catches real hangs.

Rolling updates without a capacity dip

With maxSurge: 1 and maxUnavailable: 0, Kubernetes starts a new pod, waits for it to become ready, and only then retires an old one.

ROLLING UPDATE: maxSurge 1, maxUnavailable 0STARTv1v12 servingSURGEv1v1v2 starting+1 loadingSWAPv1v2v2retire a v1DONEv2v22 servingCapacity never drops: a new pod must be Ready before an old one stops.
FIG 4 · With maxUnavailable set to 0, Kubernetes starts a new pod, waits for its readiness check, then retires an old one, so serving capacity never dips during an update.

Together these settings turn a fragile model process into a service that restarts, scales and updates without dropping requests.

Run it yourself

This is the exact example from the video. Run against a real K3d cluster. A small nginx image stands in for the vLLM image so the pods start in seconds. The video wraps the command in a throwaway namespace and cleans up afterwards.

apiVersion: apps/v1
kind: Deployment
metadata: {name: vllm}
spec:
  replicas: 2
  selector: {matchLabels: {app: vllm}}
  strategy: {rollingUpdate: {maxSurge: 1, maxUnavailable: 0}}
  template:
    metadata: {labels: {app: vllm}}
    spec:
      containers:
        - name: vllm
          image: nginx:1.27-alpine
          startupProbe:
            httpGet: {path: /, port: 80}
            periodSeconds: 2
            failureThreshold: 30

Run it:

kubectl apply -f deploy.yaml

Output I got while recording the video:

vllm ready: 2/2
startup budget: 60 s
rolling update: done
ready pods: 2
revisions: 2

Coming Up Next

Day 10: Persistent volumes (PV and PVC): managing heavy model weights and vector stores.

#Kubernetes #K8sDeployments #MLOps #SystemDesign #vLLM #PlatformEngineering #HighAvailability