URL: https://linuxfr.org/news/mirageos-un-micro-os-unikernel-en-ocaml Title: MirageOS - un micro OS (unikernel) en OCaml Authors: Dinosaure Snark, Ysabeau, palainp, theojouedubanjo, bobble bubble, BAud, palm123, Pierre Jarillon, Xavier Claude, nokomprendo, dourouc05, tisaac et Nicolas Casanova Date: 2020-08-25T13:41:21+02:00 License: CC By-SA Tags: Score: 5 [MirageOS](http://mirage.io/) est un outil permettant de créer un [unikernel](https://fr.m.wikipedia.org/wiki/Unikernel#:~:text=Les%20unikernels%20sont%20des%20images,des%20syst%C3%A8mes%20d'exploitation%20biblioth%C3%A8ques.) (un système d’exploitation) pouvant faire office de micro-service comme un site Internet, un service SMTP ou encore un service DNS. L’objectif de MirageOS est de proposer une solution modulaire afin que l’utilisateur puisse créer son propre système selon ce qu’il souhaite **vraiment**. La modularité et l’approche _clean-state_ de MirageOS permet de délester le système final d’éléments superficiels _à priori_. De ce fait, MirageOS est capable de produire un système d’exploitation complet comme un simple site internet ne pesant au final que ~16 Mo. L’approche de MirageOS est de reconstruire tous les éléments de votre application finale en [OCaml](https://ocaml.org/) (en partant de votre API REST à la pile TCP/IP). Les logiques d’abstraction et la modularité d’OCaml sont les bases de MirageOS afin de s’abstraire de tout ce qui est à proprement parler lié au système (les _syscalls_) et de pouvoir interchanger une implémentation avec une autre sans changer le reste de l’application. Par ces mécanismes-là, MirageOS a la possibilité de produire un simple exécutable ou de produire un système complet capable d’être virtualisé avec [Xen](https://xenproject.org) ou [KVM](https://www.linux-kvm.org). Dans cette dépêche, nous allons voir ce qu’est concrètement MirageOS et expliquer comment l’utiliser. ---- [MirageOS](https://mirage.io/) [OCaml](https://ocaml.org/) ---- # Un peu d’OCaml Avant de parler de MirageOS, nous avons besoin d’une petite introduction à OCaml. OCaml est un langage né en 1996 et maintenu par l’Inria venant de la famille des langages [ML](https://fr.wikipedia.org/wiki/ML_(langage)) (contraction de Meta Language : un langage de programmation généraliste fonctionnel). Il propose un système de types ainsi qu’une gestion automatique de la mémoire. Nous allons ici nous intéresser aux points qui ont dirigé MirageOS vers ce langage. ## Un système de modules OCaml a un système de modules, qui est régi par une règle : un fichier OCaml, un module ou unité de compilation. Un module est une implémentation d’un certain type de données ainsi que des fonctions permettant de traiter/manipuler ce type de données. On peut par exemple parler du module `String` qui implémente les chaînes de caractères ainsi que les fonctions comme `find_character`/`trim`/ etc. La particularité d’OCaml réside dans l’idée qu’une implémentation peut être décrite par une signature/interface, le fichier `*.mli`. Ce dernier décrit ce qui doit être montré aux autres modules de son implémentation associée. Il est courant en OCaml d’abstraire/cacher le type de données et de décrire uniquement les fonctions associées au type. Il est même commun d’utiliser une signature/interface à plusieurs implémentations tel que : ```ocaml module type ORDERED = sig type t val compare : t -> t -> int end ``` Ici, on décrit une interface qui expose `compare`. Cette dernière peut être utilisée avec `type t = string` (et utiliser `memcmp`) ou un entier `type t = int` (et utiliser la soustraction). ## Foncteur Grâce à ce mécanisme de modules, on a la possibilité de _produire_ un module/une implémentation à partir d’un autre module décrit par une interface tel que `Ordered`. Ainsi, un simple dictionnaire associant une clé avec une valeur peut se spécialiser selon la clé tel que : ```ocaml module Make (S : ORDERED) : sig type 'a t val empty : 'a t val add : S.t -> 'a -> 'a t -> 'a t end ``` Ce principe d’abstraction en OCaml est largement utilisé par la communauté, mais on le retrouve à une autre échelle en ce qui concerne MirageOS. ## _Fonctoriser_ les _syscalls_ Le principe de MirageOS et de son écosystème est de ne pas dépendre d’un appel système tel que ceux offerts par le noyau Linux. L’objectif est de s’abstraire des _syscalls_ et d’_injecter_ leurs implémentations à la production du système d’exploitation. De ce fait, on est en capacité autant d’introduire les _syscalls_ usuels proposés par le noyau Linux comme ceux d’un _micro-noyau_ pouvant être virtualisé avec Xen ou KVM. L’idée est d’être en capacité de produire la logique applicative (votre service comme un site-web) qui est complétement abstrait de la logique du système - la _stack_ TCP/IP, la couche de cryptographie, le système de fichiers. De cette manière, il devient possible de _compiler_ votre application comme un exécutable ou comme un système entier qui peut être virtualisé via Xen ou KVM ou encore un système pouvant fonctionner sur [des puces ESP32](https://mirage.io/blog/2018-esp32-booting). L’objectif est que le côté applicatif ne devrait pas changer. ## Gestion de la mémoire Depuis la version [3.9](https://github.com/ocaml/opam-repository/pull/17485) (octobre 2020), seul le mode [PVH(VM)](https://wiki.xenproject.org/wiki/PV_on_HVM) est pris en charge, la partie bas niveau permettant le boot est assurée par [Solo5](https://github.com/solo5/solo5) et la gestion de la mémoire est passée à la version de [Doug Lea pour malloc](https://github.com/mirage/ocaml-freestanding/blob/master/nolibc/dlmalloc.i). # L’éco-système de MirageOS Ainsi, tout l’éco-système de MirageOS se fonde sur un principe d’abstraction de ce qui peut être considéré comme le système d’exploitation. Au-delà de ça, les composants du système sont eux-mêmes abstraits à l’aide d’interfaces comme `ORDERED`. Ainsi, MirageOS se définit plus comme un outil permettant de faire la _glue_ entre plusieurs composants à l’aide d’interfaces. Par ce biais, il devient simple par exemple d’interchanger l’implémentation d’un type de données de l’un à l’autre sans changer le reste de la logique du système. C’est, concrètement, ce qui se passe lorsqu’on _injecte_ la _stack_ TCP/IP du système hôte lorsqu’on veut produire un simple exécutable avec MirageOS ou qu’on utilise une implémentation de la _stack_ TCP/IP en OCaml: `mirage-tcpip`. Bien entendu, cette approche requiert que ces implémentations existent ! Et c’est le principal travail de l’équipe de MirageOS : implémenter des formats/protocoles/logiques qui peuvent être utilisés avec MirageOS. Au travers de ce travail d’abstraction, ces différentes implémentations peuvent être utilisées en dehors de MirageOS. Ainsi, la plupart des projets de MirageOS sont utilisés par la communauté dans d’autres contextes que celui de MirageOS comme : - Windows - Mac OSX - votre navigateur web grâce à `js_of_ocaml` ## Quelques super-stars Dans ces projets qui sont utilisés par d’autres personnes en dehors de MirageOS, nous avons : - `irmin` - `mirage-tcpip` - `cohttp` - `ocaml-tls` - `ocaml-dns` ### Irmin L’idée d’un système de fichiers n’est pas garantie par MirageOS et, même si nous avons essayé d’implémenter certains formats, l’équipe MirageOS a décidé de concentrer ses efforts dans l’implémentation d’un _Key-Value store_. Irmin est une abstraction de ce que devrait être une telle base de données. Mais, à la différence des systèmes tels que LMDB ou encore Git, Irmin n’offre qu’une abstraction commune. Ensuite, c’est à l’utilisateur de choisir son implémentation. MirageOS utilise aujourd’hui Irmin avec une implémentation de Git en OCaml. Par ce dernier, un _unikernel_ peut obtenir une base de données clé-valeur interne qui peut se synchroniser au _boot_ avec un dépôt Git. Sur ce dernier, il peut se synchroniser ou il peut le mettre à jour. On parle alors de système de base de données persistant – même si le système s’éteint, il peut reprendre l’état dans lequel la base de données était juste avant de s’éteindre. Irmin est actuellement utilisé par la crypto-monnaie [Tezos](https://tezos.com/) afin de manipuler la _block-chain_. Une autre utilisation concrète d’Irmin avec MirageOS est un système d’exploitation qui fait office de serveur DNS primaire dont le fichier [`zone`](https://en.wikipedia.org/wiki/Zone_file) est stocké dans un dépôt Git. Ainsi, l’_unikernel_ se synchronise avec ce dépôt (avec le protocol Git - comme un `git clone`), il peut le modifier (comme `git push`) et l’utilisateur peut tout autant modifier aussi et demander ensuite à l’_unikernel_ de se resynchroniser (comme `git pull`). Dans le dernier cas, l’utilisateur peut décrire sa politique de _merge_ (comment résoudre un conflit s’il y en a un) avec Irmin. Cela permet d’assurer la persistance des données en dehors de l’_unikernel_ lui-même. ### mirage-tcpip Du fait que MirageOS est un système d’exploitation complet, l’équipe de MirageOS a finalement implémenté la _stack_ TCP/IP au travers du projet [mirage-tcpip](https://github.com/mirage/mirage-tcpip). Au-delà de l’intérêt technique de ré-implementer une _stack_ TCP/IP, cette dernière est industriellement utilisée par [Docker](https://www.docker.com/blog/docker-unikernels-open-source/). Ce projet permet d’introduire un concept fondateur de MirageOS. L’objectif de l’outil `mirage` est de produire, au mieux, un système complet capable d’être virtualisé sur KVM ou Xen, mais il permet aussi de produire un simple exécutable UNIX comme nous aurions l’habitude d’avoir. Le point crucial ici est la capacité de `mirage` à orchestrer (indépendamment de l’application) les _stacks_ selon la cible. Pour ce qui est de la production d’un simple exécutable, `mirage` va injecter la _stack_ TCP/IP du système hôte. Pour ce qui est de KVM ou Xen, il va tout simplement injecter `mirage-tcpip` (qui n’est fait qu’en OCaml). L’idée est de n’utiliser, du point de vue de l’application, qu’une interface (en l’occurrence [mirage-stack](https://github.com/mirage/mirage-stack/blob/master/src/mirage_stack.ml)) nous permettant de séparer la logique de l’application des autres fondements de notre système. ### cohttp Bien entendu, en tant que premier exemple réel d’un _unikernel_, il nous faut aussi une implémentation du protocole HTTP 1.1 : [cohttp](https://github.com/mirage/ocaml-cohttp). Pour l’exemple, le site officiel de [MirageOS](https://mirage.io/) est un _unikernel_ utilisant `cohttp`. Mais ce projet, comme la plupart des projets MirageOS, dépasse l’écosystème et fait partie intégrante du plus large écosystème d’OCaml. Puisque le développement d’une bibliothèque OCaml pour MirageOS se fait toujours en abstraction du système, spécialiser le cœur pour un système comme Linux ou Windows devient plus facile. La qualité la plus reconnue des projets Mirage est leur capacité à pouvoir être utilisés sur pratiquement tous les systèmes. Bien entendu, cette qualité est fortement liée à OCaml aussi qui propose un _runtime_ s’exécutant nativement sur une multitude de plateformes. ### ocaml-tls La conception d’un système entier nécessite aussi de disposer de primitives de cryptographie, qui sont usuellement disponibles avec OpenSSL (ou l’un de ses forks). Il n’est, pour autant, pas aussi _simple_ de ré-intégrer un code C existant (dépendant généralement des _syscalls_ POSIX) dans MirageOS qui n’a, pour le coup, rien de toutes ces primitives. Un effort colossal a donc été fait pour ré-implémenter les primitives de cryptographie essentielles afin de pouvoir ré-implémenter le protocole TLS, nécessaire pour servir un site internet accessible en HTTPS. Là aussi, `ocaml-tls` est un projet phare limitant le prérequis d’instructions assembleur tout en proposant une bibliothèque avec des performances raisonnables. Encore une fois, il n’a absolument aucune notion de ce que peut être un _socket_ ou de tout ce qui peut être _POSIX-compliant_. ### ocaml-dns Enfin, le domaine mirage.io est géré par un serveur primaire DNS qui est aussi un _unikernel_. Plusieurs services DNS tel qu’un résolveur et un service s’occupant du challenge DNS de let's encrypt sont disponibles grâce à `ocaml-dns`. Ce projet s’inscrit dans l’ambition de proposer des micro-services : un système d’exploitation pour un service spécifique. # Vous lancer dans l’écriture d’un unikernel Des exemples prêts à compiler sont disponibles sur le [github](https://github.com/mirage/mirage-skeleton) dont le classique _hello world_ entièrement reproduit ici : open Lwt.Infix module Hello (Time : Mirage_time.S) = struct let start _time = let rec loop = function | 0 -> Lwt.return_unit | n -> Logs.info (fun f -> f "hello"); Time.sleep_ns (Duration.of_sec 1) >>= fun () -> loop (n-1) in loop 4 end Ce « noyau » se contente d’écrire quatre fois hello en 4quatresecondes et se termine. Si vous voulez expérimenter chez vous, vous devez mettre en place un environnement propice à la compilation et à l’exécution, par exemple avec les commandes suivantes (à adapter en fonction de votre distribution) : sudo dnf install opam && \ opam init && opam update -yu && \ opam install mirage && eval $(opam env) && \ git clone https://github.com/mirage/mirage-skeleton && \ cd mirage-skeleton/tutorial/hello && \ mirage configure -t unix && make depend && make && \ ./hello (Dans cet exemple on produit un binaire exécutable mais en utilisant l’option de configuration `mirage configure -t xen` on peut par exemple produire un noyau utilisable avec l’hyperviseur Xen.) ## Abstraction avec les _functors_ Comme vous pouvez le constater, un _functor_ `Time` est utilisé pour _générer_ la fonction _start_ (qui est comme le `main` en C). C’est un module qui respect la signature [`Mirage_time.S`](https://mirage.github.io/mirage-time/mirage-time/Mirage_time/module-type-S/index.html). Grâce à ce module, nous pouvons utiliser la fonction `sleep_ns`. Son implémentation dépend bien entendu de la cible de votre MirageOS: - pour UNIX, comme dans cette exemple, nous allons utiliser `Unix.sleep` - pour Solo5 (KVM ou Xen), nous allons utiliser une fonction spécifique disponible dans `mirage-solo5` Ce choix d’implémentation est fait par l’outil `mirage`, lorsque vous lancez: `mirage configure`. Dans ce cas, on prend l’implémentation par défaut proposée par `mirage` mais l’utilisateur peut très bien choisir **son** implémentation (tant qu'elle respecte la signature `Mirage_time.S`). ## Autres aspects Il y aurait encore beaucoup à dire, car cet article ne peut être exhaustif ! Vous êtes invités à le compléter dans vos commentaires.