Spécification Technique : Déploiement Souverain, Inférence GPU Locale (vLLM) & CAs Privées (Air-Gap)
Statut : Document de Cadrage Technique (Jalon M11)
Date : 2026-09-05
Auteur : Équipe Atelier
Principes directeurs : Conforme à00-architecture-principles-substitutability.md, étend03-litellm-proxy.mdet s'articule avec14-devex-cli-simulateurs-hitl.md.
1. Contexte & Objectifs
Dans les environnements d'entreprise régulés (banque, défense, santé, OIV) ou les installations bare-metal souveraines :
1. L'inférence LLM doit pouvoir être 100% locale : Aucune donnée ne doit transiter vers un fournisseur d'API cloud (OpenAI, Anthropic). Sur une machine ou un cluster équipé de GPUs (NVIDIA/AMD), Atelier doit fournir un backend d'inférence local prêt à l'emploi.
2. Le réseau peut être déconnecté (Air-Gap) : Les images, manifestes et dépendances doivent pouvoir être ingérés depuis des registres d'entreprise privés (Harbor, Nexus).
3. Les certificats d'autorité d'entreprise (PKI interne) doivent être reconnus : Les outils de développement in-VM (git, npm, pip, cargo, curl) et net-proxy doivent valider les certificats internes signés par la CA d'entreprise sans désactiver la sécurité TLS.
2. Architecture Globale
flowchart TD
subgraph Node["Nœud Single-Node / Cluster avec Accélérateur GPU"]
subgraph LocalGPU["Inférence GPU Locale (Optionnelle)"]
GPU_HARDWARE[("GPU NVIDIA / Passthrough")]
VLLM_SERVICE["Pod vLLM / Backend OpenAI-compatible
(Activé via gpu.enabled=true)"]
GPU_HARDWARE --- VLLM_SERVICE
end
subgraph AtelierControlPlane["Control Plane Atelier"]
LITELLM["LiteLLM Proxy
(Routage par défaut vers vLLM local)"]
API["api-server (Axum)"]
CTRL["controller (K8s Operator)"]
NETPROXY["net-proxy
(Truststore CA d'entreprise)"]
end
subgraph GuestVM["MicroVM Firecracker (Workshop)"]
AGENT["Agent IA (Claude Code / OpenCode)"]
CA_STORE["/etc/ssl/certs/ (CA injectée)"]
end
subgraph CorpNetwork["Réseau d'Entreprise Privé"]
CORP_CA[("PKI d'Entreprise (CA Racine)")]
CORP_HARBOR[("Harbor Interne (Images)")]
CORP_MIRROR[("Nexus / Artifactory (Miroirs TLS)")]
end
end
VLLM_SERVICE -->|"API OpenAI /v1"| LITELLM
LITELLM -->|"Endpoint LLM souverain"| AGENT
CORP_CA -.->|"Injectée dans Helm"| NETPROXY
CORP_CA -.->|"Injectée dans rootfs"| CA_STORE
NETPROXY -->|"Valide avec CA privée"| CORP_MIRROR
CTRL -->|"Pull images signées"| CORP_HARBOR
AGENT -->|"Trafic réseau contrôlé"| NETPROXY
3. Spécification Détaillée
3.1. Backend vLLM Local dans LiteLLM (tâche 11.3, implémentée)
Pour les déploiements mono-machine (atelier server install --enable-gpu, tâche 11.4) ou les clusters Kubernetes disposant de ressources GPU :
- Composant Helm Dédié —
charts/atelier/templates/infra/vllm-statefulset.yaml(image officiellevllm/vllm-openai, un seul réplica,volumeClaimTemplatespour le cache HuggingFace afin de ne pas retélécharger les poids à chaque redémarrage) etvllm-service.yaml, déclarés danscharts/atelier/values.yamlsectiongpu(enabled,model,maxModelLen,tensorParallelSize,resources.limits."nvidia.com/gpu",persistence.size). - Câblage Automatique dans LiteLLM — correction par rapport à la première rédaction : il n'existe PAS de
charts/atelier/templates/litellm-configmap.yamldans ce chart, ni nulle part ailleurs —litellm-deployment.yamlpositionneSTORE_MODEL_IN_DB=Trueet TOUS les modèles LiteLLM de ce chart sont gérés dynamiquement via l'API d'administration (POST /model/new, spec11-admin-litellm-model-config.md), jamais via un fichier statique. Nouveaucharts/atelier/templates/jobs/litellm-vllm-model-init-job.yaml(hook Helmpost-install,post-upgrade, imagecurlimages/curl) enregistre donc le backend vLLM local en appelant cette même API, gardé derrièregpu.enabled && litellm.enabled && initJobs.litellmVllmModelInit.enabled: model_name: "*"(wildcard), pas"default"— même convention quedeploy/dev/llm-proxy/config.yaml(le nom de modèle envoyé par l'agent change selon sa version/son CLI ; un wildcard capte tout ce qui n'est pas explicitement routé ailleurs, exactement le comportement recherché pour "modèle par défaut GPU local").litellm_params.model: "hosted_vllm/<gpu.model>"(préfixe de provider LiteLLM dédié aux backends vLLM auto-hébergés compatibles OpenAI, pasopenai/...),api_basevers le Servicevllminterne,api_key: "not-needed"(vLLM n'exige aucune authentification en local).- Idempotence sans
jq(absent de l'imagecurlimages/curl) :GET /model/infoavant toute création, détection par simple présence de la chaîneapi_base(stable et propre à ce backend) dans la réponse — nécessaire car LiteLLM ne garantit pas l'unicité demodel_name(spec 11 §3.2), un/model/newrejoué à chaquehelm upgradedupliquerait sinon l'entrée wildcard indéfiniment. Piège vérifié et écarté empiriquement contre l'instance LiteLLM réelle du cluster de dev (atelier-llm-proxy,STORE_MODEL_IN_DB=True) : la RÉPONSE dePOST /model/newchiffrelitellm_params.api_base(chaîne illisible, cohérent avec la note non expliquée de la spec 11 §3.1) — maisGET /model/info, LUI, le renvoie bien EN CLAIR, confirmé par un Job jetable réel (création réussie200,api_baseretrouvé en clair via/model/info, second passage détecté comme "déjà présent", entrée de test supprimée après coup). - L'agent de code in-VM (
Claude Code,opencode) consomme le proxy LiteLLM surhttp://169.254.0.1:4000(aliasllm-proxydenet-proxy) sans changer sa configuration standard ; budgets et Virtual Keys par Workshop continuent de s'appliquer normalement (mécanisme LiteLLM inchangé, orthogonal au choix du backend). - Intégration CLI Single-Node (
atelier server install --enable-gpu, tâche 11.4, implémentée) :crates/cli/src/commands/server.rs::detect_nvidia_gpuinterroge réellement le pilote vianvidia-smi(NVML, pas seulementls /dev/nvidia*— même principe que la vérification KVM déjà existante viakvm_ioctls::Kvm::new()) ET vérifie la présence denvidia-container-runtimedans lePATH(sans lui, k3s/containerd ne peut exposer aucun GPU aux pods, quel que soit le pilote hôte). Intégré àdoctorcomme diagnostic non bloquant ("GPU NVIDIA (optionnel, --enable-gpu)"). Le flag--enable-gpud'installrend ce même diagnostic BLOQUANT (échec explicite avant toute modification système si aucun GPU utilisable n'est détecté, plutôt qu'un chart installé avecgpu.enabled: truedont leStatefulSetvLLM resteraitPendingindéfiniment) et positionnegpu.enabled: true/gpu.resources.limits."nvidia.com/gpu": 1dans les values Helm générées (write_values_file). Vérifié en réel sur cette machine de développement (GPU physique NVIDIA RTX 3060 présent) :atelier server doctordétecte correctementNVIDIA GeForce RTX 3060, 8192 MiBvianvidia-smi, et signale à raison l'absence denvidia-container-runtimesur cette machine (non installé) comme avertissement non bloquant et actionnable. Non vérifié :install --enable-gpude bout en bout (installerait réellement k3s sur cette machine hôte partagée, même limite déjà documentée pour la tâche 9.8) — la fonction de détection elle-même est vérifiée en réel, son câblage dansinstall/write_values_fileest vérifié par lecture de code et compilation, pas par une exécution complète. StatefulSetvLLM non déployé/testé en conditions réelles d'inférence :nvidia-container-toolkitabsent de cette machine (confirmé ci-dessus) etdocker run --gpus ally échoue explicitement (could not select device driver) — les installer serait un changement système invasif (modification du runtime Docker par défaut) hors du périmètre raisonnable de cette tâche sur une machine de dev partagée. Resterait de toute façonPendingsans device plugin Kubernetesnvidia.com/gpu(prérequis non couvert par ce chart, voir tâche 11.3) : seulshelm lint/helm template(rendu vérifié avecgpu.enabled: true) etshellcheck(script du Job d'enregistrement) le couvrent côté template ; la logique métier du Job (idempotence, forme du payload) est, elle, vérifiée en réel comme décrit en 11.3.
3.2. Prise en Charge des CAs Privées d'Entreprise
Pour résoudre les blocages SSL: CERTIFICATE_VERIFY_FAILED fréquents en entreprise :
- Configuration Centralisée Helm (tâche 11.1, implémentée) :
Monté par le chart en ConfigMap
tls: customCaBundle: | -----BEGIN CERTIFICATE----- MIIE... (Certificat racine d'entreprise) -----END CERTIFICATE-----<release>-ca-bundle(charts/atelier/templates/infra/ca-bundle-configmap.yaml). - Correction par rapport à la première rédaction de cette spec :
net-proxyne termine jamais TLS sur le chemin de relais egress — vérifié dans le code (crates/net-proxy/src/proxy.rs,crates/net-proxy/src/tls_sni.rs) : que ce soit viaCONNECT(tunnelTcpStreambrut, octets non interprétés) ou via la détection SNI transparente (lecture du seul champserver_nameen clair duClientHello, jamais un déchiffrement), le trafic HTTPS relayé reste chiffré de bout en bout entre la microVM et sa vraie destination. Une CA d'entreprise dansnet-proxyne pourrait donc rien "inspecter" sans une réécriture MITM complète — hors périmètre, et contraire à la conception documentée du module. Le point réel où la CA d'entreprise doit être approuvée est donc où TLS est réellement terminé : côté client, à chaque appelant Rust (reqwest, backendrustls, qui ne consulte ni le magasin système niSSL_CERT_FILE) et côté outillage in-VM (git/npm/pip/cargo/curl, §3.3 ci-dessous). - Mécanisme générique implémenté dans
crates/common/src/tls_client.rs::client_builder_trusting_extra_ca(généralise l'ATELIER_JWT_CA_PATHpré-existant d'api-server) : construit unreqwest::ClientBuilderqui fait confiance à la CA d'entreprise en plus des CA publiques. Piège trouvé en vérifiant :reqwest::Certificate::from_pem(backendrustls-tls-native-roots) accepte silencieusement un PEM structurellement invalide (octets arbitraires, ou en-têtes PEM avec un corps base64 corrompu) sans jamais retourner d'erreur — validé explicitement au préalable avecrustls_pemfile::certs. - Câblé côté
api-server: quandtls.customCaBundleest renseignée et qu'aucunATELIER_JWT_CA_PATHexplicite n'est déjà fourni viaapiServer.env, le chart positionne automatiquementATELIER_JWT_CA_PATHsur le fichier monté — couvre le cas réelkeycloak.external.enabledavec un IdP d'entreprise derrière une CA privée. Vérifié parhelm template(ConfigMap rendue, montage, variable positionnée, et override explicite respecté). - Non fait dans cette tâche, laissé à une suite : aucun autre
reqwest::Clientdu workspace n'a aujourd'hui de véritable besoin de cette CA (les autres appels HTTPS sortants observés sont soit en clair intra-cluster, soit inexistants) — le mécanisme est en place et testé unitairement, maisapi-server/JWKS reste son seul consommateur réel pour l'instant. - Propagation dans les MicroVMs (tâche 11.2, implémentée) :
- Le controller retransmet le nom de la
ConfigMap(ctx.ca_bundle_configmap,ATELIER_CA_BUNDLE_CONFIGMAP) au Jobimage-builderqu'il crée, monté à/etc/atelier/ca/ca.crt(ensure_image_build_job,crates/controller/src/reconcile.rs) et transmis parATELIER_CA_BUNDLE_PATH. - Correction par rapport à la première rédaction :
update-ca-certificatesn'est jamais invoqué — ce binaire n'existe pas forcément dans l'image de base cible, et l'exécuter enchrootdepuis le conteneurimage-builder(architecture/libc potentiellement différentes) est fragile. À la place,crates/image-builder/src/main.rs::append_pem_to_bundle_fileajoute directement la CA au bundle système déjà présent (/etc/ssl/certs/ca-certificates.crt, paquetca-certificates) — un ajout, jamais un remplacement : remplacer casserait tout accès HTTPS public (PyPI, npm, GitHub...) pour les mécanismes qui font confiance à ce seul fichier. Ce mécanisme sert à DEUX endroits distincts :trust_enterprise_ca_bundle_for_this_process: le PROPRE magasin de confiance du conteneurimage-builder— nécessaire pour songit clone(ensure_workspace_clone, tourne sur ce pod, PAS relayé/déchiffré parnet-proxy, voir la correction de la tâche 11.1).inject_enterprise_ca_bundle: le rootfs PRODUIT, pourgit/curl/pip/cargo(OpenSSL) IN-VM au runtime du Workshop, viaGIT_SSL_CAINFO/CURL_CA_BUNDLE/REQUESTS_CA_BUNDLE/PIP_CERT/SSL_CERT_FILEpointés sur le bundle combiné.
- Cas particulier Node.js/
npm: Node ne consulte JAMAIS le magasin système, même après cet ajout —NODE_EXTRA_CA_CERTS(mécanisme additif propre à Node, documenté officiellement) pointe sur une copie brute dédiée (/usr/local/share/ca-certificates/atelier-ca.crt), déposée dans tous les cas. - Limite assumée et documentée : seules les images de base Debian/Ubuntu (bundle système à
/etc/ssl/certs/ca-certificates.crt) bénéficient de la couverture complète — une image RHEL/Fedora (/etc/pki/tls/certs/ca-bundle.crt) ne reçoit queNODE_EXTRA_CA_CERTS, jamais testée faute d'image de ce type dans ce workspace.envbuilder(microVM builder jetable,crates/builder-vm-init) — utilisé pour RÉSOUDRE le devcontainer.json et exécuterpostCreateCommand/features avant que ce rootfs ne soit exporté — n'est PAS couvert par cette tâche : un devcontainer dont les features ont besoin d'un miroir npm/pip d'entreprise AU MOMENT DU BUILD échouerait encore. Laissé à une suite (nécessiterait d'injecter la CA dans le rootfs de LA microVM builder elle-même,crates/builder-vm-init/Dockerfile, un chantier distinct).
3.3. Outillage Déconnecté (Air-Gap Packaging) — tâche 11.5, implémentée
- Préfixage des Registres — correction par rapport à la première rédaction :
.Values.global.imageRegistryn'existe pas dans ce chart (vérifié : aucun template decharts/atelier/templates/ne le lit). Deux mécanismes bien réels, mais séparés : - Les 5 composants injectés par le CONTROLLER dans les pods Workshop (
net-proxy/identity-proxy/vm-supervisor/mcp-gateway/image-builder) se reconfigurent globalement viacontroller.env.ATELIER_COMPONENT_IMAGE_REGISTRY(crates/controller/src/reconcile.rs::component_image_ref, déjà existant). - Tout le reste (
api-server,controller,dashboard,pm-engine,kvm-device-plugin, et toutes les dépendances tierces — Postgres, Keycloak, Forgejo, OpenBao, LiteLLM, Redis, RustFS, observability, vLLM) n'a PAS de préfixe global : chaque section devalues.yamlporte son propre<composant>.image.repository, à surcharger individuellement. Non fait dans cette tâche (chantier de chart distinct) : ajouter un véritableglobal.imageRegistrycohérent à toutes les sections serait une amélioration future légitime, découverte en implémentant celle-ci. - Script d'Export/Import (
scripts/airgap-bundle.sh) — correction :skopeo/docker saveabandonnés au profit decrane pull/crane push(déjà vendu et utilisé ailleurs dans ce dépôt —deploy/dev/crane/crane,crates/image-builder— aucune nouvelle dépendance externe).export [--with-gpu] [output-dir]télécharge chaque image en tarball individuel (crane pull) puis les archive enatelier-images-<version>.tar.gz(version lue depuischarts/atelier/Chart.yaml,--with-gpuinclutvllm/vllm-openai— plusieurs Go, exclu par défaut) ;import --registry <registre>extrait l'archive et rejoue chaque image (crane push), en re-taguant les 5 composants injectés par le controller endev(même convention quedeploy/terraform/aws/mirror-images.sh). Liste des images tenue à jour manuellement contrevalues.yaml—mirror-images.sha pris du retard (ometobservability/RustFS/gpu/litellmVllmModelInit, confondpostgres:16-alpine— clientpsqldes Jobs d'init — avecpgvector/pgvector:pg16, l'image réelle de la base) : liste corrigée dans ce nouveau script,mirror-images.shnon touché (hors périmètre de cette tâche). - Piège trouvé et corrigé en testant réellement :
do_importdéclaraitlocal work_dirpuis untrap 'rm -rf "$work_dir"' EXIT— le trap s'exécute après le RETOUR de la fonction, oùwork_dir(une variablelocal) est déjà hors de portée, provoquantvariable sans liaisonsousset -u(import réussi, mais message d'erreur final trompeur,exit 1). Corrigé en retirantlocal(variable de script, pas de fonction). - Vérifié de bout en bout contre de vraies images et un vrai registre :
crane pull ghcr.io/philippevienne/atelier-net-proxy:latest/curlimages/curl:8.11.0réels,exportproduit une archive réelle (~45 Mo),import --registry localhost:<port>contre un vrairegistry:2local confirme les deux images présentes (GET /v2/_catalog),atelier-net-proxybien re-taguédev.shellcheck(koalaman/shellcheck:stable) propre après correctif. - Commande d'import (identique à la première rédaction) :
4. Garde-Fous & Pièges Évités
- Substituabilité Respectée :
- Atelier n'impose pas vLLM : si l'entreprise utilise déjà Ollama, TGI, TensorRT-LLM ou un cluster GPU externe, il suffit de renseigner son URL dans LiteLLM. vLLM n'est qu'un composant optionnel de commodité.
- Pas d'Ordonnancement Multi-Cluster Artificiel :
- Chaque instance d'Atelier reste scopée à son cluster. Pour une séparation physique (cluster public vs cluster confidentiel), on déploie deux instances d'Atelier indépendantes.
- Zéro Baisse de Sécurité TLS :
- Ne jamais proposer de flag
insecure-skip-tls-verifygénéralisé. L'injection de la CA racine est la seule approche propre et conforme aux politiques de sécurité d'entreprise.
5. Phasage Opérationnel
| Tâche | Description | Composants |
|---|---|---|
| 11.1 | Support de customCaBundle dans Helm et injection dans net-proxy |
charts/atelier, crates/net-proxy |
| 11.2 | Injection automatique de la CA d'entreprise dans le rootfs des microVMs | crates/image-builder, crates/guest-init |
| 11.3 | Composant Helm optionnel gpu.vllm et templating de route par défaut LiteLLM |
charts/atelier, templates/litellm |
| 11.4 | Détection GPU et flag --enable-gpu dans atelier server install |
crates/cli |
| 11.5 | Script d'export/import d'images conteneurs déconnecté (airgap-bundle.sh) |
scripts/ |