Claude Code mit eigenem Modell auf EU-Kubernetes: die Kette steht
10.08.2026
Thema: alle Beiträge zu Kubernetes
Ein Coding-Agent gegen ein eigenes Modell auf einer GPU in Frankreich: Claude Code, LiteLLM, vLLM, GPU Operator, Managed Kubernetes. Die Kette läuft, ausprobiert mit genau einem Node. Ein Überblick, was damit geht und was die einfache Variante bewusst nicht kann.
Inhaltsverzeichnis
- Die Kette, in der Reihenfolge der Anfrage
- Was damit geht
- Zertifiziert, und was das wert ist
- Was offen ist und was nicht
- Die einfache Variante, und was sie nicht ist
- Was es kostet
- Wie es weitergeht
- Anhang: der ganze Aufbau in einer Datei
Gestern lief zum ersten Mal die komplette Kette: Claude Code auf meinem Rechner, dahinter ein LiteLLM-Proxy, dahinter vLLM mit einem eigenen Modell, dahinter der NVIDIA GPU Operator auf einem Managed-Kubernetes-Cluster in Gravelines in Frankreich. Kein Zwischenschritt fehlt, keine Anfrage verlässt die EU, und provisioniert ist das Ganze mit einem einzigen tofu apply.
Dieser Beitrag ist der Überblick, kein Tutorial. Er beantwortet die Frage, welche Bausteine es braucht, was die Kombination heute leistet und wo die Grenzen der einfachen Variante liegen. Die technischen Details je Baustein, und davon gibt es reichlich, kommen in eigenen Beiträgen.
Die Kette, in der Reihenfolge der Anfrage
Claude Code läuft lokal und spricht ausschließlich die Anthropic Messages API. Das ist der Grund, warum die Kette einen Übersetzer braucht.
LiteLLM ist dieser Übersetzer. Der Proxy nimmt Anfragen im Anthropic-Format entgegen und reicht sie im OpenAI-Format weiter. Er läuft als gewöhnliches Deployment auf dem System-Node, ohne GPU.
vLLM hostet das Modell als OpenAI-kompatible API. Gewählt ist Qwen3-Coder-30B-A3B-Instruct als 4-Bit-Quantisierung: ein Mixture-of-Experts-Modell mit 30 Milliarden Parametern, von denen pro Token nur 3,3 Milliarden aktiv sind. Auf einer Karte, deren Engpass die Speicherbandbreite ist, schlägt diese Bauart ein dichtes 14B-Modell um ein Mehrfaches.
Der NVIDIA GPU Operator macht die Karte für Kubernetes überhaupt erst sichtbar. Er bringt Treiber, Container-Toolkit und Device-Plugin auf den Node, danach ist nvidia.com/gpu eine anforderbare Ressource wie CPU oder Speicher. Installiert wird er als Helm-Release direkt aus OpenTofu heraus, ein manueller Helm-Aufruf entfällt.
Der Cluster ist ein Managed-Kubernetes-Cluster bei OVHcloud in der Region GRA11, also Gravelines in Frankreich. Zwei Node-Pools: ein kleiner System-Node für Infrastruktur-Pods und den Proxy, dazu ein stundenweise abgerechneter GPU-Pool mit einer NVIDIA L4 und 24 GB VRAM.
OpenTofu hält alles zusammen: Cluster, beide Node-Pools, GPU Operator, vLLM, LiteLLM, den persistenten Modell-Cache. Der State von Tofu liegt clientseitig verschlüsselt im eigenen Objektspeicher, denn der Kubeconfig des Clusters landet darin.
Was damit geht
Claude Code startet, verbindet sich gegen den Proxy, spricht das Modell aus dem eigenen Cluster und ruft dabei Werkzeuge auf: Dateien lesen, Kommandos ausführen, Änderungen schreiben. Der Agentenkreislauf funktioniert, nicht nur die Textausgabe. Genau daran scheitern die meisten Bastelaufbauten, weil Function Calling ein eigenes Format je Modellfamilie hat und der passende Parser aktiviert sein will.
Was ich damit ausdrücklich nicht behaupte: dass dieses Modell den gehosteten Dienst ersetzt. Tempo und Fähigkeit sind eine eigene Messung, und die gehört in einen eigenen Beitrag mit Zahlen statt Eindrücken. Die Aussage dieses Beitrags ist bescheidener und trotzdem die wichtigere: Der Weg ist offen und vollständig begehbar, mit Bausteinen, die jedes Plattform-Team kennt.
Zertifiziert, und was das wert ist
Der Cluster steht nicht zufällig bei OVHcloud. Managed Kubernetes von OVHcloud ist seit dem 24. März 2026 auf der CNCF-Liste der Certified Kubernetes AI Platforms, zusammen mit inzwischen rund 30 weiteren Plattformen. Das AI Conformance Program ist die Antwort der CNCF auf die entstehenden KI-Silos: Es beschreibt, was ein Cluster können muss, damit KI-Workloads portabel bleiben, von der Beschleuniger-Metrik bis zum Gang Scheduling.
Zwei Einordnungen gehören dazu, sonst wird aus einem Siegel ein Argument, das es noch nicht trägt.
Erstens: Die Zertifizierung läuft derzeit als Selbstauskunft. Der Anbieter reicht eine ausgefüllte Anforderungsliste ein, die CNCF prüft die Einreichung, automatisierte Konformitätstests sind für 2026 angekündigt. Das ist ein anderer Nachweis als bei Certified Kubernetes, wo eine Testsuite läuft.
Zweitens: Zertifiziert ist das Produkt, nicht mein Cluster. Was auf meiner L4 läuft, läuft dort, weil ich es hingestellt habe, nicht weil ein Siegel es garantiert.
Trotzdem ist die Liste genau der Hebel, um den es hier geht. Sie macht aus “der Anbieter sagt, er kann KI” eine überprüfbare Aussage mit einer Versionsnummer daran.
Was offen ist und was nicht
Die Kette besteht aus offenen Bausteinen, und das ist der Punkt der Übung. Aber “offen” ist kein Zustand, den man pauschal ausruft, sondern eine Liste, die man durchgeht.
| Baustein | Lizenz | Anmerkung |
|---|---|---|
| Kubernetes | Apache 2.0 | Managed, aber der Cluster selbst bleibt portabel |
| NVIDIA GPU Operator | Apache 2.0 | Der Treiber darunter ist proprietär, die Images kommen aus NVIDIAs Registry |
| vLLM | Apache 2.0 | Trägt die eigentliche Arbeit |
| LiteLLM | MIT | Der Übersetzer zwischen den beiden API-Dialekten |
| OpenTofu | MPL 2.0 | Hält den gesamten Aufbau als Code |
| Modellgewichte | Apache 2.0 | Offene Gewichte sind kein offener Quellcode, siehe unten |
| Claude Code | proprietär | Der geschlossene Teil der Kette |
Zwei Ehrlichkeiten stechen heraus. Die eine ist der NVIDIA-Treiber: Der Operator drumherum ist Apache 2.0, das Stück, das die Karte ansteuert, ist es nicht. Wer GPUs betreibt, betreibt heute proprietäre Software, und das sollte man aussprechen statt es im Wort Open Source zu verstecken.
Die andere sind die Modellgewichte. Apache 2.0 auf einem Gewichte-Archiv erlaubt Nutzung, Weitergabe und kommerziellen Einsatz, es liefert aber nicht die Trainingsdaten und nicht die Trainingsläufe. Man kann das Modell betreiben und weitergeben, aber nicht rekonstruieren. Das ist genau die Unterscheidung, die ich in Open Source ist mehr als sichtbarer Quellcode getroffen habe, und sie gilt bei Modellen strenger als bei Software.
Und dann steht am Anfang der Kette Claude Code selbst, ein proprietärer Client. Die Ironie ist beabsichtigt: Der interessante Teil an diesem Aufbau ist nicht, dass alles offen wäre, sondern dass die austauschbaren Teile austauschbar sind. Der Client spricht ein dokumentiertes API-Format, der Proxy übersetzt es, dahinter kann jedes Modell hängen. Genau das ist der Unterschied zwischen einer Kette und einer Kopplung.
Die einfache Variante, und was sie nicht ist
Der Aufbau ist bewusst die kleinstmögliche Version. Ein Node, eine Karte, ein Nutzer. Alles, was in einem echten Betrieb schwierig wird, kommt in diesem Aufbau schlicht nicht vor:
- Kein Scheduling-Problem. Bei genau einer GPU und genau einem Pod gibt es nichts zu verteilen. Gang Scheduling, Fractional GPUs, Warteschlangen und Prioritäten: alles offen.
- Kein Zugang von außen. Der Proxy ist per Port-Forward erreichbar, sonst nicht. Deshalb läuft er auch ohne Authentifizierung, was für einen Tunnel vertretbar ist und für einen Ingress grob fahrlässig wäre.
- Kein Autoscaling. Wecken und Parken sind zwei OpenTofu-Aufrufe von Hand. Kein Scale-to-Zero auf Anfrage, keine Node-Autoprovisionierung.
- Kein schneller Start. vLLM lädt beim ersten Mal rund 16 GB Gewichte und initialisiert danach die Engine, zusammen 10 bis 20 Minuten. Auch aus dem gefüllten Cache dauert es mehrere Minuten. Das ist kein Serverless-Verhalten, und wer es dafür hält, hält den Start für einen Hänger.
- Kein verteiltes Inferencing. Mehrere Karten, mehrere Nodes, ein Modell darüber verteilt: der Punkt, an dem Projekte wie llm-d anfangen, interessant zu werden. Auch das später.
- Keine Mandantenfähigkeit. Ein Namespace, ein Modell, kein Quota-Modell, keine Abrechnung nach Verbrauch.
Die Grenze des Modells ist übrigens nicht das Modell, sondern die Karte: 24 GB VRAM ergeben nach den Gewichten ein Kontextfenster von 56.000 Token. Der nächste Ausbauschritt wäre eine größere Karte, nicht ein anderes Modell.
Was es kostet
Die L4 wird stundenweise abgerechnet und kostet rund einen Euro pro Stunde. Das ist billig für eine Session und teuer für ein Wochenende, an dem man es vergisst: rund 700 Euro im Monat im Dauerbetrieb. Deshalb gehört das Parken in dasselbe Rezept wie der Aufbau. Ein tofu apply mit null GPU-Nodes, und die Rechnung hört auf zu laufen. Control Plane und Modell-Cache bleiben stehen, letzterer für etwa zwei Euro im Monat, und beim nächsten Wecken ist der Cluster in Minuten wieder einsatzbereit.
Das ist der eigentliche Grund, warum der Aufbau als Code existiert. Ein Lab, das man nur mit Klicks parken kann, parkt man nicht.
Wie es weitergeht
Der Aufbau steht, die interessanten Beiträge kommen jetzt erst. Vorgemerkt sind:
- der GPU Operator im Detail, also was da eigentlich alles auf den Node kommt, bis
nvidia.com/gpuallokierbar ist - das VRAM-Budget von vLLM: Gewichte, KV-Cache, CUDA-Graphs, und warum 24 GB schneller voll sind, als die Modellgröße vermuten lässt
- die Fallstricke beim Betrieb eines fremden Modells hinter einem Client, der für ein anderes gebaut wurde. Der teuerste Fehler dieses Labs war kein Infrastruktur-Fehler, sondern ein Kontextfenster, das aus einer Richtung anders aussah als aus der anderen
- der Weg von einem Node zu mehreren, und die Frage, ab wann sich die Kubernetes-Bordmittel dafür wirklich eignen
Wer bis dahin selbst loslaufen will: Der komplette Aufbau steht unten, in einer Datei.
Anhang: der ganze Aufbau in einer Datei
Ein öffentliches Repo für dieses Lab gibt es noch nicht, deshalb steht der Aufbau hier vollständig, Stand 7. August 2026. Herausgenommen ist genau ein Block, das S3-Backend für den State, weil Bucket und Zugangsdaten meine sind. Der State liegt damit lokal neben der Datei.
Die clientseitige Verschlüsselung habe ich bewusst drin gelassen, obwohl der State jetzt lokal liegt. Der Kubeconfig des Clusters landet im State, und ein unverschlüsselter Cluster-Zugang in einer Datei, die man versehentlich committet, ist genau die Art Fehler, die man nicht zweimal macht. Ohne gesetzte TF_VAR_state_passphrase läuft dieser Aufbau deshalb nicht los.
Was du sonst brauchst: ein OVHcloud-Konto mit Public-Cloud-Projekt, ein API-Token mit Schreibrechten auf dieses Projekt (OVH_APPLICATION_KEY, OVH_APPLICATION_SECRET, OVH_CONSUMER_KEY) und eine freigeschaltete GPU-Quota für den Flavor l4-90 in der gewählten Region.
terraform {
required_version = ">= 1.8"
required_providers {
ovh = {
source = "ovh/ovh"
version = "~> 2.0"
}
helm = {
source = "hashicorp/helm"
version = "~> 3.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.30"
}
}
# Clientseitige State-Verschlüsselung (OpenTofu ab 1.7): der Kubeconfig
# des Clusters landet im State, auch im lokalen. Passphrase via
# TF_VAR_state_passphrase setzen.
encryption {
key_provider "pbkdf2" "passphrase" {
passphrase = var.state_passphrase
}
method "aes_gcm" "default" {
keys = key_provider.pbkdf2.passphrase
}
state {
method = method.aes_gcm.default
enforced = true
}
plan {
method = method.aes_gcm.default
enforced = true
}
}
}
# Authentifizierung via Umgebungsvariablen (erzeugt über
# https://api.ovh.com/createToken/ oder als IAM Service Account):
# OVH_APPLICATION_KEY, OVH_APPLICATION_SECRET, OVH_CONSUMER_KEY
provider "ovh" {
endpoint = "ovh-eu"
}
variable "service_name" {
type = string
description = "ID des Public-Cloud-Projekts (OVHcloud Manager -> Public Cloud -> Projekt-ID)"
}
variable "region" {
type = string
description = "MKS-Region; GPU-Flavor-Verfügbarkeit vorab im Manager pruefen"
default = "GRA11"
}
variable "gpu_flavor" {
type = string
description = "GPU-Flavor des Worker-Pools; exakten Namen gegen die Region pruefen"
default = "l4-90"
}
variable "gpu_node_count" {
type = number
description = "Anzahl GPU-Nodes; 0 = Pool geparkt, es fallen keine GPU-Kosten an"
default = 1
}
variable "system_node_count" {
type = number
description = "Anzahl System-Nodes; 0 = Cluster komplett heruntergefahren, nur Control Plane (kostenlos) und PVC bleiben"
default = 1
}
variable "state_passphrase" {
type = string
description = "Passphrase fuer die State-Verschluesselung (TF_VAR_state_passphrase)"
sensitive = true
}
# Managed-Kubernetes-Cluster; die Control Plane ist bei OVHcloud kostenlos.
resource "ovh_cloud_project_kube" "gpu_lab" {
service_name = var.service_name
name = "gpu-lab"
region = var.region
}
# Kleiner Pool fuer System-Workloads (GPU Operator, CoreDNS etc.),
# damit die teure GPU-Maschine keine Infrastruktur-Pods traegt.
resource "ovh_cloud_project_kube_nodepool" "system" {
service_name = var.service_name
kube_id = ovh_cloud_project_kube.gpu_lab.id
name = "system"
flavor_name = "b3-8"
desired_nodes = var.system_node_count
min_nodes = 0
max_nodes = 1
}
# GPU-Pool, stundenbasiert abgerechnet. Parken nach der Session:
# tofu apply -var gpu_node_count=0
resource "ovh_cloud_project_kube_nodepool" "gpu" {
service_name = var.service_name
kube_id = ovh_cloud_project_kube.gpu_lab.id
name = "gpu"
flavor_name = var.gpu_flavor
desired_nodes = var.gpu_node_count
min_nodes = 0
max_nodes = 2
monthly_billed = false
}
# Helm spricht direkt mit dem frisch erzeugten Cluster; die Zugangsdaten
# kommen aus dem Kube-Ressource-Attribut, kein kubeconfig-Export noetig.
provider "helm" {
kubernetes = {
host = ovh_cloud_project_kube.gpu_lab.kubeconfig_attributes[0].host
cluster_ca_certificate = base64decode(ovh_cloud_project_kube.gpu_lab.kubeconfig_attributes[0].cluster_ca_certificate)
client_certificate = base64decode(ovh_cloud_project_kube.gpu_lab.kubeconfig_attributes[0].client_certificate)
client_key = base64decode(ovh_cloud_project_kube.gpu_lab.kubeconfig_attributes[0].client_key)
}
}
# GPU Operator: bringt Treiber, Container-Toolkit und Device-Plugin auf die
# GPU-Nodes; danach ist nvidia.com/gpu als Ressource allokierbar. Er laeuft
# auf dem System-Pool und reagiert auf GPU-Nodes, sobald welche da sind,
# deshalb reicht der System-Pool als Abhaengigkeit.
resource "helm_release" "gpu_operator" {
name = "gpu-operator"
repository = "https://helm.ngc.nvidia.com/nvidia"
chart = "gpu-operator"
namespace = "gpu-operator"
create_namespace = true
wait = true
timeout = 900
depends_on = [ovh_cloud_project_kube_nodepool.system]
}
# Gleiche Zugangsdaten wie der Helm-Provider, fuer die vLLM-Ressourcen.
provider "kubernetes" {
host = ovh_cloud_project_kube.gpu_lab.kubeconfig_attributes[0].host
cluster_ca_certificate = base64decode(ovh_cloud_project_kube.gpu_lab.kubeconfig_attributes[0].cluster_ca_certificate)
client_certificate = base64decode(ovh_cloud_project_kube.gpu_lab.kubeconfig_attributes[0].client_certificate)
client_key = base64decode(ovh_cloud_project_kube.gpu_lab.kubeconfig_attributes[0].client_key)
}
variable "vllm_model" {
type = string
description = "Hugging-Face-Modell fuer vLLM; AWQ-4-bit passt mit ~16 GB auf die L4"
default = "QuantTrio/Qwen3-Coder-30B-A3B-Instruct-AWQ"
}
variable "vllm_served_model_name" {
type = string
description = "Modellname, unter dem die OpenAI-kompatible API das Modell meldet"
default = "qwen3-coder"
}
resource "kubernetes_namespace" "vllm" {
metadata {
name = "vllm"
}
}
# Persistenter Hugging-Face-Cache: ohne ihn laedt jeder Pod-Start die
# ~15 GB Modellgewichte neu aus dem Netz.
resource "kubernetes_persistent_volume_claim" "hf_cache" {
metadata {
name = "hf-cache"
namespace = kubernetes_namespace.vllm.metadata[0].name
}
spec {
access_modes = ["ReadWriteOnce"]
resources {
requests = {
storage = "50Gi"
}
}
}
# Cinder bindet Volumes erst, wenn ein Pod sie nutzt (WaitForFirstConsumer).
wait_until_bound = false
}
# vLLM hostet das Modell als OpenAI-kompatible API auf Port 8000.
# Qwen3-Coder-30B-A3B (MoE, 3,3B aktiv): auf der bandbreitenschwachen L4
# deutlich schneller als ein dichtes 14B-Modell; AWQ-Gewichte ~16 GB plus
# FP8-KV-Cache ergeben 56k Token Kontext auf 24 GB VRAM (57344 = das
# Claude-Code-Budget aus 49152 Auto-Compact-Fenster + 8192 Output).
# CUDA-Graphs nur fuer Batch 1-8: volle Dekodier-Geschwindigkeit fuer
# den Einzelnutzer-Betrieb, aber ein Bruchteil des VRAM der
# Standard-Capture-Liste (die bis Batch 512 reicht). Der Parser
# qwen3_coder uebersetzt das Function-Call-Format des Modells; beides
# braucht die Anbindung an Claude Code (LiteLLM-Proxy).
resource "kubernetes_deployment" "vllm" {
metadata {
name = "vllm"
namespace = kubernetes_namespace.vllm.metadata[0].name
labels = {
app = "vllm"
}
}
spec {
replicas = 1
# Nur eine GPU im Pool: alter Pod muss weichen, bevor der neue startet.
strategy {
type = "Recreate"
}
selector {
match_labels = {
app = "vllm"
}
}
template {
metadata {
labels = {
app = "vllm"
}
}
spec {
# Kubernetes wuerde sonst VLLM_PORT=tcp://... aus dem Service "vllm"
# injizieren, was vLLM als eigene (kaputte) Konfiguration liest.
enable_service_links = false
container {
name = "vllm"
image = "vllm/vllm-openai:latest"
args = [
"--model", var.vllm_model,
"--served-model-name", var.vllm_served_model_name,
"--max-model-len", "57344",
"--kv-cache-dtype", "fp8",
"--gpu-memory-utilization", "0.93",
"--max-num-seqs", "8",
"--compilation-config", "{\"cudagraph_capture_sizes\": [1, 2, 4, 8]}",
"--enable-auto-tool-choice",
"--tool-call-parser", "qwen3_coder",
]
env {
name = "HF_HOME"
value = "/root/.cache/huggingface"
}
port {
container_port = 8000
}
resources {
requests = {
cpu = "4"
memory = "16Gi"
"nvidia.com/gpu" = "1"
}
limits = {
memory = "48Gi"
"nvidia.com/gpu" = "1"
}
}
volume_mount {
name = "hf-cache"
mount_path = "/root/.cache/huggingface"
}
volume_mount {
name = "shm"
mount_path = "/dev/shm"
}
# Erster Start laedt die Gewichte herunter; grosszuegig warten.
startup_probe {
http_get {
path = "/health"
port = 8000
}
period_seconds = 15
failure_threshold = 80
}
readiness_probe {
http_get {
path = "/health"
port = 8000
}
period_seconds = 10
}
}
volume {
name = "hf-cache"
persistent_volume_claim {
claim_name = kubernetes_persistent_volume_claim.hf_cache.metadata[0].name
}
}
volume {
name = "shm"
empty_dir {
medium = "Memory"
size_limit = "2Gi"
}
}
}
}
}
# Nicht auf den Rollout warten: bei geparktem GPU-Pool (gpu_node_count=0)
# bleibt der Pod absichtlich Pending und apply soll trotzdem durchlaufen.
wait_for_rollout = false
depends_on = [helm_release.gpu_operator]
}
resource "kubernetes_service" "vllm" {
metadata {
name = "vllm"
namespace = kubernetes_namespace.vllm.metadata[0].name
}
spec {
selector = {
app = "vllm"
}
port {
port = 8000
target_port = 8000
}
}
}
# LiteLLM-Proxy: uebersetzt die Anthropic Messages API (/v1/messages), die
# Claude Code spricht, auf die OpenAI-API von vLLM. Zugriff nur per
# Port-Forward, deshalb bewusst ohne Master-Key; sobald der Proxy je
# oeffentlich exponiert wird, braucht er Authentifizierung.
resource "kubernetes_config_map" "litellm" {
metadata {
name = "litellm-config"
namespace = kubernetes_namespace.vllm.metadata[0].name
}
data = {
"config.yaml" = yamlencode({
model_list = [
{
model_name = var.vllm_served_model_name
litellm_params = {
model = "hosted_vllm/${var.vllm_served_model_name}"
api_base = "http://vllm.${kubernetes_namespace.vllm.metadata[0].name}.svc.cluster.local:8000/v1"
}
}
]
})
}
}
resource "kubernetes_deployment" "litellm" {
metadata {
name = "litellm"
namespace = kubernetes_namespace.vllm.metadata[0].name
labels = {
app = "litellm"
}
}
spec {
replicas = 1
selector {
match_labels = {
app = "litellm"
}
}
template {
metadata {
labels = {
app = "litellm"
}
annotations = {
# Pod bei Config-Aenderung neu ausrollen; der Kubernetes-Provider
# macht das nicht von selbst.
"checksum/config" = sha256(kubernetes_config_map.litellm.data["config.yaml"])
}
}
spec {
# Gleiche Falle wie bei vLLM: der Service "litellm" wuerde sonst
# LITELLM_*-Umgebungsvariablen injizieren.
enable_service_links = false
container {
name = "litellm"
image = "ghcr.io/berriai/litellm:main-stable"
args = ["--config", "/etc/litellm/config.yaml", "--port", "4000"]
port {
container_port = 4000
}
resources {
requests = {
cpu = "250m"
memory = "1Gi"
}
# Das LiteLLM-Image braucht beim Start deutlich mehr als 1 Gi,
# darunter wird der Pod OOMKilled.
limits = {
memory = "2Gi"
}
}
volume_mount {
name = "config"
mount_path = "/etc/litellm"
read_only = true
}
readiness_probe {
http_get {
path = "/health/readiness"
port = 4000
}
period_seconds = 10
}
}
volume {
name = "config"
config_map {
name = kubernetes_config_map.litellm.metadata[0].name
}
}
}
}
}
}
resource "kubernetes_service" "litellm" {
metadata {
name = "litellm"
namespace = kubernetes_namespace.vllm.metadata[0].name
}
spec {
selector = {
app = "litellm"
}
port {
port = 4000
target_port = 4000
}
}
}
output "kubeconfig" {
description = "Kubeconfig des Clusters; abrufen mit: tofu output -raw kubeconfig > gpu-lab-kubeconfig.yaml"
value = ovh_cloud_project_kube.gpu_lab.kubeconfig
sensitive = true
}
Aufbauen, Kubeconfig ziehen, Cluster ansprechen:
tofu init
tofu apply -var service_name="<projekt-id>"
tofu output -raw kubeconfig > gpu-lab-kubeconfig.yaml
export KUBECONFIG="$PWD/gpu-lab-kubeconfig.yaml"
Der erste Start von vLLM lädt rund 16 GB Modellgewichte, das dauert. Zusehen statt raten:
kubectl -n vllm logs deploy/vllm -f
Claude Code anbinden, zwei Shells. In der ersten der Tunnel auf den Proxy:
kubectl -n vllm port-forward svc/litellm 4000:4000
In der zweiten Claude Code auf den Proxy zeigen:
export ANTHROPIC_BASE_URL="http://localhost:4000"
export ANTHROPIC_AUTH_TOKEN="dummy"
export ANTHROPIC_MODEL="qwen3-coder"
export ANTHROPIC_SMALL_FAST_MODEL="qwen3-coder"
export CLAUDE_CODE_MAX_OUTPUT_TOKENS=8192
export CLAUDE_CODE_AUTO_COMPACT_WINDOW=49152
claude --strict-mcp-config --mcp-config '{"mcpServers":{}}'
Die letzten drei Zeilen sehen nach Kleinkram aus und sind der Grund, warum das Ganze überhaupt durchläuft. Sie passen Kontextfenster und Werkzeugliste an ein Modell an, dessen Fenster um ein Vielfaches kleiner ist als das, wofür der Client gebaut wurde. Was dahintersteckt, ist ein eigener Beitrag wert, und den schreibe ich noch.
Und zum Schluss der wichtigste Aufruf von allen, der nach jeder Session:
tofu apply -var service_name="<projekt-id>" -var gpu_node_count=0
© 2026 Trutz Software Consulting GmbH. CC BY 4.0: Dieser Text darf mit Namensnennung weiterverwendet und bearbeitet werden, auch kommerziell. Code-Beispiele darin stehen unter Apache 2.0. Ausgenommen sind Marke und Logo sowie gekennzeichnete Zitate Dritter. Was das genau heißt.
Newsletter
Erhalte praxisnahe Tutorials, Best Practices und echte Lösungen für die häufigsten DevOps-Probleme direkt in Dein Postfach.
Datenschutz: Deine E-Mail-Adresse wird DSGVO-konform in Deutschland gespeichert. Keine Spam, nur relevante Inhalte. Eine E-Mail alle 1 bis 2 Wochen.