Overview
This post covers the AI platform built on top of the Kubernetes cluster. The cluster foundation (kubeadm, HA control plane, GPU setup) is in Post 1. The networking and GitOps layer (Cilium, Flux, Democratic CSI) is in Post 2. This picks up from a running cluster with persistent storage and GitOps in place.
The goal was a fully self-hosted AI stack with no external API dependencies for inference:
- LiteLLM: OpenAI-compatible API gateway routing to local models
- Ollama: GPU inference on a GTX 1070
- Open-WebUI: Chat interface for team use
- Qdrant: Vector database for embeddings and RAG
- RAG pipeline: Knowledge base ingestion and semantic search
- sre-cli + MCP server: Tooling that connects the AI stack to cluster observability
Architecture
PLATFORM (in-cluster, managed by Flux)
├─ Ollama → ollama.ai-platform.svc.cluster.local:11434 (wk4, GPU)
├─ LiteLLM → litellm.home.<domain>
├─ Open-WebUI → open-webui.home.<domain>
└─ Qdrant → qdrant.home.<domain>
TOOLING
└─ sre-cli (Python) → MCP server → optional external AI assistant
Ollama runs as a Kubernetes Deployment on wk4 with runtimeClassName: nvidia. The NVIDIA Container Toolkit injects GPU access at runtime, no host-level Ollama install required. LiteLLM routes to the in-cluster service URL.
Ollama: GPU Inference on Kubernetes
Ollama runs as a Kubernetes Deployment on wk4 using the NVIDIA device plugin and runtimeClassName: nvidia. The NVIDIA Container Toolkit handles GPU library injection at runtime, no CUDA paths to manage in the image.
| |
A 50Gi TrueNAS iSCSI PVC persists the model weights across pod restarts.
Models in use:
| Model | Use |
|---|---|
mistral:latest | General queries, SRE context |
llama3.2:3b | Fast lightweight tasks |
qwen2.5-coder:7b | Code, YAML, debugging |
nomic-embed-text | Embeddings (dim=768) |
The GTX 1070 has 8GB VRAM. Quantized 7B models fit comfortably: the practical sweet spot on this hardware. 12B+ models offload layers to RAM and reduce throughput significantly.
Ollama exposes an OpenAI-compatible API at ollama.ai-platform.svc.cluster.local:11434. LiteLLM routes to this in-cluster endpoint.
LiteLLM: Unified API Gateway
LiteLLM presents a single OpenAI-compatible API endpoint to all consumers. Routing rules, model aliases, and spend tracking all live in one place. Swapping an underlying model doesn’t require changes to any client.
Helm Deployment (via Flux)
| |
The master key is a SOPS-encrypted secret in the GitHub repo. Flux decrypts at reconcile time: same pattern as all other secrets in this cluster.
Model Configuration
| |
Any client that speaks OpenAI’s API works against LiteLLM unchanged. The gateway abstracts the underlying model routing entirely.
Open-WebUI: Chat Interface
Open-WebUI provides a browser-based chat interface connected to LiteLLM. It handles conversation history, model switching, and basic document upload.
| |
strategy: Recreate is required: Open-WebUI’s PVC is backed by an iSCSI RWO volume. A rolling update blocks indefinitely waiting for the old pod to release the volume before the new one can attach. This applies to every stateful workload in this stack.
Exposed at open-webui.home.<domain> via the nginx Ingress controller on the cluster’s Cilium L2 LoadBalancer IP.
Qdrant: Vector Database
Qdrant stores embeddings for the RAG pipeline. It runs in-cluster with a TrueNAS iSCSI persistent volume.
| |
Creating the collection for the knowledge base:
| |
768 dimensions matches the output of nomic-embed-text. Cosine distance is the right metric for semantic similarity over text embeddings.
RAG Pipeline: Knowledge Base Ingestion
The knowledge base is an Obsidian vault (98 Markdown files covering runbooks, incident notes, infrastructure decisions, and project plans).
Chunking and Embedding
| |
Ingestion
| |
Result: 247 chunks from 103 files ingested into the vault collection.
The chunking strategy is paragraph-based with a 500-character cap. This keeps chunks semantically coherent: a paragraph on one topic stays together rather than being split mid-sentence.
Semantic Search
| |
Direct httpx POST to the Qdrant REST API rather than using the qdrant-client Python library. The library introduced breaking API changes between minor versions that silently changed the response shape. The REST API is stable and the dependency is eliminated entirely.
sre-cli: Cluster-Aware AI Tooling
sre-cli is a Python command-line tool that combines cluster observability with AI-augmented workflows.
sre alert - active Prometheus alerts
sre cluster - node status, resource utilisation
sre runbook <q> - semantic search across runbooks
sre incident <q> - recent incident notes and post-mortems
runbook and incident both call search_vault with the query and return the top-scored chunks formatted as terminal output, no LLM inference required for the search itself. Relevant context surfaces in under a second.
MCP Server
The same tools are exposed as an MCP server. This is optional: the core platform (local inference, RAG, semantic search) is fully self-hosted and air-gapped. The MCP server is a bridge for any external AI assistant that supports the protocol.
| |
Four tools exposed:
| Tool | What it does |
|---|---|
get_cluster_status | Node status and resource usage from the Kubernetes API |
get_alerts | Active Prometheus alerts from the Alertmanager API |
prometheus_query | Arbitrary PromQL execution |
search_vault | Semantic search over the Qdrant vault collection |
With the MCP server active, an external AI assistant can query cluster state and search the knowledge base mid-conversation, no copy-pasting from terminal. The cluster data being queried stays on-prem; only the query and retrieved context leave the network if an external assistant is used.
How It Comes Together
A realistic workflow:
- Alert fires: Alertmanager routes to ntfy, phone notification arrives
sre alert: Check what’s firing from terminalsre runbook "etcd quorum": Pull relevant runbook chunks without opening a browser- sre-cli + MCP (optional): If using an external AI assistant via the MCP server, it can call
get_alertsandsearch_vaultto correlate the alert with knowledge base context. The cluster data stays on-prem; the assistant just receives the query results.
The AI platform isn’t replacing SRE judgment: it’s reducing the time spent finding information that already exists in runbooks and incident notes.
Lessons Learned
runtimeClassName: nvidia makes GPU scheduling straightforward. The NVIDIA Container Toolkit injects GPU libraries at runtime, no manual CUDA path configuration in the image. The device plugin handles GPU resource accounting. Ollama inside the cluster means the inference endpoint is a proper k8s Service, not a host IP dependency.
strategy: Recreate for every stateful workload. iSCSI RWO volumes block rolling updates indefinitely. Every deployment backed by a PVC in this stack uses Recreate. Make it a default, not an afterthought.
REST over SDK for Qdrant. The qdrant-client library introduced a breaking change between minor versions that silently changed the response shape. The REST API hasn’t changed. For a search endpoint called in production tooling, that stability matters.
768 dimensions must match at collection creation. Creating the Qdrant collection with the wrong dimension silently accepts points but returns garbage similarity scores. Verify the embedding model’s output dimension before ingestion: fix requires dropping and recreating the collection.
Paragraph-based chunking preserves coherence. Fixed-size character splits break context at arbitrary points. Splitting on double newlines (paragraph boundaries) keeps each chunk semantically self-contained, which improves retrieval relevance meaningfully.
SOPS for AI platform secrets. LiteLLM’s master key and Open-WebUI’s API key live encrypted in git. The same encrypted_regex: "^(data|stringData)$" pattern used across the cluster keeps diffs readable while protecting values.
What’s Running
| |
All inference stays on-premises. No data leaves the network.
Building a private AI platform? We take on short-term infrastructure engagements and platform work.