PI-GROUPE
KubernetesSécuritéVirtualisationEdge ComputingInfrastructureOpen SourceArchitecture

vk-cocoon : exécuter des pods Kubernetes en MicroVM plutôt qu'en conteneurs

Un fournisseur Virtual Kubelet qui intercepte les pods et les fait tourner en machines virtuelles légères sous Cloud Hypervisor ou Firecracker. Isolation matérielle sans quitter l'API Kubernetes.

Un conteneur Linux partage le noyau de son hôte. C'est ce qui le rend léger — et c'est aussi sa limite de sécurité. Une faille d'échappement dans le noyau compromet tout ce qui tourne sur la machine.

Pour la plupart des charges internes, ce risque reste acceptable. Il cesse de l'être dès qu'on exécute du code dont on ne maîtrise pas l'origine : charge d'un client tiers, fonction fournie par un partenaire, traitement en périphérie sur un site physiquement accessible.

Le projet vk-cocoon propose une réponse élégante. Plutôt que de remplacer le runtime de conteneur, il se présente au cluster comme un nœud virtuel. Les pods qui lui sont affectés ne deviennent pas des conteneurs mais des MicroVM, chacune avec son propre noyau.

Le mécanisme du Virtual Kubelet

Virtual Kubelet est un projet de la CNCF qui simule un kubelet. Il enregistre un Node dans le cluster, l'ordonnanceur lui affecte des pods, et le fournisseur derrière fait ce qu'il veut de ces pods.

text
   kube-scheduler
         │ affecte le pod au nœud « cocoon-edge-01 »
         ▼
   ┌─────────────────────────┐
   │  vk-cocoon              │  se présente comme un Node
   │  (Virtual Kubelet)      │  traduit PodSpec → MicroVM
   └────────────┬────────────┘
                ▼
   ┌─────────────────────────┐
   │  Cloud Hypervisor       │  un noyau par charge
   │  ou Firecracker         │  démarrage ~125 ms
   └─────────────────────────┘

L'intérêt tient à ce qui ne change pas. Vos manifestes restent des manifestes Kubernetes. kubectl logs fonctionne, kubectl exec aussi. Les sondes, les secrets, les ConfigMap : tout se comporte normalement. Seul le mode d'exécution diffère.

Installation

yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: vk-cocoon
  namespace: cocoon-system
spec:
  selector:
    matchLabels: { app: vk-cocoon }
  template:
    metadata:
      labels: { app: vk-cocoon }
    spec:
      nodeSelector:
        cocoon.io/hypervisor: "enabled"    # nœuds avec /dev/kvm accessible
      hostNetwork: true
      serviceAccountName: vk-cocoon
      containers:
        - name: provider
          image: ghcr.io/cocoonstack/vk-cocoon:v0.9.3
          args:
            - --provider=cocoon
            - --nodename=cocoon-$(NOM_HOTE)
            - --hypervisor=cloud-hypervisor
            - --kernel-image=/var/lib/cocoon/vmlinux-6.12-microvm
            - --rootfs-template=/var/lib/cocoon/rootfs-base.img
            - --vm-boot-timeout=20s
            - --klog.v=2
          env:
            - name: NOM_HOTE
              valueFrom: { fieldRef: { fieldPath: spec.nodeName } }
            - name: KUBELET_PORT
              value: "10260"
          securityContext:
            privileged: true                # accès à /dev/kvm et à la création de tap
          volumeMounts:
            - { name: kvm,     mountPath: /dev/kvm }
            - { name: cocoon,  mountPath: /var/lib/cocoon }
            - { name: run,     mountPath: /run/cocoon }
          resources:
            requests: { cpu: 500m, memory: 512Mi }
      volumes:
        - name: kvm
          hostPath: { path: /dev/kvm, type: CharDevice }
        - name: cocoon
          hostPath: { path: /var/lib/cocoon, type: DirectoryOrCreate }
        - name: run
          hostPath: { path: /run/cocoon, type: DirectoryOrCreate }

Le nœud virtuel apparaît ensuite avec une souillure qui empêche l'ordonnanceur d'y placer n'importe quoi.

bash
kubectl get nodes -l type=virtual-kubelet
# NAME                STATUS   ROLES   VERSION
# cocoon-edge-01      Ready    agent   v1.31.4-vk-cocoon

kubectl describe node cocoon-edge-01 | grep -A2 Taints
# Taints: cocoon.io/microvm=true:NoSchedule

Affecter une charge

Seuls les pods qui tolèrent explicitement la souillure atterrissent en MicroVM.

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: traitement-partenaire
  namespace: charges-externes
spec:
  replicas: 4
  selector:
    matchLabels: { app: traitement-partenaire }
  template:
    metadata:
      labels: { app: traitement-partenaire }
      annotations:
        cocoon.io/vcpus: "2"
        cocoon.io/memory: "2048"
        cocoon.io/kernel-cmdline: "quiet reboot=k panic=1 pci=off"
        cocoon.io/balloon: "true"          # restitution de mémoire inutilisée
    spec:
      nodeSelector: { type: virtual-kubelet }
      tolerations:
        - key: cocoon.io/microvm
          operator: Equal
          value: "true"
          effect: NoSchedule
      containers:
        - name: traitement
          image: registry-partenaire.io/traitement:4.1.0
          resources:
            requests: { cpu: "2", memory: 2Gi }
            limits:   { cpu: "2", memory: 2Gi }   # égalité : la MicroVM est fixe
          ports: [{ containerPort: 8080 }]

Notez l'égalité entre demandes et limites. Une MicroVM reçoit une allocation fixe au démarrage ; la notion de dépassement contrôlé n'existe pas comme pour un cgroup. Le ballon de mémoire permet de restituer ce qui n'est pas utilisé, mais le plafond reste ferme.

Ce que l'isolation coûte réellement

CritèreConteneurMicroVM Cocoon
Démarrage50 à 200 ms120 à 400 ms
Surcoût mémoire~2 Mio25 à 40 Mio (noyau invité)
Surcoût processeurnégligeable2 à 5 %
Entrées-sorties disquenatif−10 à −20 % (virtio)
Réseaunatif−5 à −15 %
Surface d'attaquenoyau partagéhyperviseur seul

Ces chiffres tranchent avec l'image d'une virtualisation lourde. Cloud Hypervisor démarre une MicroVM en cent vingt millisecondes parce qu'il n'émule presque rien : pas de BIOS, pas de PCI, juste virtio et un noyau taillé pour.

Le surcoût mémoire reste le point le plus tangible. Quarante mégaoctets par pod comptent quand vous en exécutez cinq cents. Sur une charge externe qui en consomme deux gigaoctets, c'est du bruit.

Les limites à connaître

Pas de partage de noyau, donc pas de fonctionnalités noyau partagées. Un pod qui a besoin de hostPID, hostNetwork ou de capacités privilégiées ne fonctionnera pas. C'est le but, mais cela exclut les agents de supervision qui inspectent l'hôte.

Les volumes persistants passent par virtio-fs ou virtio-blk. Les performances sont correctes, mais un emptyDir en mémoire se comporte différemment. Testez vos charges à forte intensité d'écriture.

Le maillage de services demande attention. Un conteneur adjacent Istio tourne dans la même MicroVM que l'application — ce qui reste cohérent. Mais l'injection automatique doit être vérifiée, certains fournisseurs ne la gèrent pas.

Le projet reste jeune. La version 0.9 n'est pas une 1.0. Testez sérieusement avant d'y mettre une charge critique.

Quand cela vaut le détour

Trois situations justifient l'investissement.

L'exécution de code tiers. Une plateforme qui fait tourner des fonctions fournies par des clients ne peut pas se contenter d'un noyau partagé.

Les environnements physiquement exposés. Un serveur en périphérie, dans une usine ou un local technique, auquel quelqu'un peut brancher un câble.

Les exigences réglementaires d'isolation. Certains référentiels imposent une séparation par hyperviseur entre traitements de niveaux de sensibilité différents. Cocoon la fournit sans quitter Kubernetes.

Pour tout le reste, un conteneur bien configuré — utilisateur non privilégié, système de fichiers en lecture seule, capacités retirées, profil seccomp — couvre l'essentiel du risque à un coût nul.