From de21be218abb0cc75dce13a75add2c941b518094 Mon Sep 17 00:00:00 2001 From: nemunaire Date: Thu, 15 Nov 2018 23:38:25 +0100 Subject: [PATCH] docker-internals: tuto ready --- tutorial/docker-internals/Makefile | 2 +- tutorial/docker-internals/linuxkit.md | 264 +++++++++++++++++++++++++- tutorial/docker-internals/oci.md | 11 ++ tutorial/docker-internals/registry.md | 8 +- tutorial/docker-internals/rendu.md | 6 +- tutorial/docker-internals/runc.md | 128 +++++++++++-- tutorial/docker-internals/setup.md | 2 + 7 files changed, 395 insertions(+), 26 deletions(-) diff --git a/tutorial/docker-internals/Makefile b/tutorial/docker-internals/Makefile index 9bc06fc..05d5caa 100644 --- a/tutorial/docker-internals/Makefile +++ b/tutorial/docker-internals/Makefile @@ -1,4 +1,4 @@ -SOURCES = tutorial.md clair.md oci.md manifest.md runc.md linuxkit.md vxlan.md rendu.md +SOURCES = tutorial.md clair.md oci.md registry.md runc.md linuxkit.md rendu.md PANDOCOPTS = --latex-engine=xelatex \ --standalone \ --normalize \ diff --git a/tutorial/docker-internals/linuxkit.md b/tutorial/docker-internals/linuxkit.md index c37a80f..32af587 100644 --- a/tutorial/docker-internals/linuxkit.md +++ b/tutorial/docker-internals/linuxkit.md @@ -3,10 +3,268 @@ `linuxkit` ========== -Exemple avec une machine qui fait office de serveur DNS. +[`linuxkit`](https://github.com/linuxkit/linuxkit) est encore un autre projet +tirant parti des conteneurs. Il se positionne comme un système de construction +de machine. En effet, grâce à lui, nous allons pouvoir générer des systèmes +complets, *bootable* dans QEMU, VirtualBox, VMware ou même sur des machines +physiques, qu'il s'agisse de PC ou bien même de Raspberry Pi, ou même encore +des images pour les différents fournisseurs de cloud ! + +Bien entendu, au sein de ce système, tout est fait de conteneur ! Alors quand +il s'agit de donner une IP publique utilisable par l'ensemble des conteneurs, +il faut savoir jouer avec les *namespaces* pour arriver à ses fins ! + +Si vous vous rappelez du cours d'AdLin[^adlin] en début d'années, toutes les +VMs que vous avez utilisées reposaient entièrement sur `linuxkit`. En fait, +chaque conteneur représentait alors une machine différente : un conteneur +mattermost d'un côté, un conteneur `unbound` pour faire office de serveur DNS, +et les machines clientes étaient de simples conteneurs exécutant un client +dhcp. + +[^adlin]: toutes les sources des machines sont dans ce dépôt : + + + +## Prérequis + +Si vous n'avez pas déjà le binaire `linuxkit`, vous pouvez télécharger ici : +. + +Notez qu'étant donné qu'il est écrit en Go, aucune dépendance n'est nécessaire +en plus du binaire[^lollibc] ;-) + +[^lollibc]: à condition tout de même que vous utilisiez une libc habituelle. + +Vous aurez également besoin de QEMU pour tester vos créations. + + +## Structure d'un fichier `linuxkit.yml` + +Le fichier utilisé pour construire notre image se décompose en plusieurs +parties : + +- `kernel` : il est attendu ici une image OCI contenant le nécessaire pour + pouvoir utiliser un noyau : l'image du noyau, ses modules et un initramfs ; +- `init` : l'ensemble des images OCI de cette liste seront fusionnés pour + donner naissance au *rootfs* de la machine. On n'y place normalement qu'un + gestionnaire de conteneur, qui sera chargé de lancer chaque conteneur au bon + moment ; +- `onboot`, `onshutdown` et `services` : il s'agit de conteneurs qui seront + lancés par le système disponible dans l'`init`, au bon moment. Les conteneurs + indiqués dans `onboot` seront lancés **séquentiellement** au démarrage de la + machine, ceux dans `onshutdown` seront lancés lors de l'arrêt de la + machine. Les conteneurs dans `services` seront lancés simultanément une fois + que le dernier conteneur de `onboot` aura rendu la main ; +- `files` : des fichiers supplémentaires à placer dans le rootfs. + +Le format est documenté +[ici](https://github.com/linuxkit/linuxkit/blob/master/docs/yaml.md). + + +## Hello? + +L'image la plus simple que l'on puisse réaliser pourrait être : + +
+```yaml + kernel: + image: linuxkit/kernel:4.14.80 + cmdline: "console=tty0 console=ttyS0" + init: + - linuxkit/init:c563953a2277eb73a89d89f70e4b6dcdcfebc2d1 + - linuxkit/runc:83d0edb4552b1a5df1f0976f05f442829eac38fe + - linuxkit/containerd:326b096cd5fbab0f864e52721d036cade67599d6 + onboot: + - name: dhcpcd + image: linuxkit/dhcpcd:v0.6 + command: ["/sbin/dhcpcd", "--nobackground", "-f", "/dhcpcd.conf", "-1"] + services: + - name: getty + image: linuxkit/getty:2eb742cd7a68e14cf50577c02f30147bc406e478 + env: + - INSECURE=true + trust: + org: + - linuxkit +``` +
+ +L'image `getty` est très pratique pour déboguer, car elle permet d'avoir un +shell sur la machine ! + +On notera cependant que, positionné dans `services`, le shell que nous +obtiendrons sera lui-même exécuté dans un conteneur, nous n'aurons donc pas un +accès entièrement privilégier. Pour déboguer, il faut placer cette image dans +la partie `init`, elle sera alors lancé comme un équivalent de +`init=/bin/sh`.[^infogetty] + +[^infogetty]: Plus d'infos à + + + +## *namespaces* + +Chaque nouveau conteneur est donc lancé dans un espace distinct où il ne pourra +pas interagir avec les autres, ou déborder s'il s'avérait qu'il expose une +faille exploitable. + +Néanmoins, contrairement à Docker qui va de base nous dissocier du maximum de +*namespaces*, `linuxkit` ne le fait pas pour les *namespaces* `net`, `ipc` et +`uts`. C'est-à-dire que, par défaut, la pile réseau est partagée entre tous les +conteneurs, tout comme les IPC et le nom de la machine. + +Il reste possible de se dissocier également de ces namespaces, en précisant : + +
+```yaml + - name: getty + image: linuxkit/getty:2eb742cd7a68e14cf50577c02f30147bc406e478 + net: new +``` +
+ +Ou inversement, pour persister dans le namespace initial : + +
+```yaml + - name: getty + image: linuxkit/getty:2eb742cd7a68e14cf50577c02f30147bc406e478 + pid: host +``` +
+ + +### Partage de *namespace* + +Dans le cas où l'on souhaite que deux conteneurs partagent le même *namespace*, +il faut passer le chemin vers la structure du noyau correspondante. + +On commence donc d'abord par créer le nouveau *namespace*, en prenant soin de +*bind mount* la structure du noyau à un emplacement connu : + +
+```yaml + - name: getty + image: linuxkit/getty:2eb742cd7a68e14cf50577c02f30147bc406e478 + net: new + runtime: + bindNS: /run/netns/mynewnetns +``` +
+ +À la création du *namespace* `net`, le lien vers la structure du noyau +correspondante sera *bind mount* sur `/run/netns/synchro`. On pourra alors +réutiliser plus tard ce chemin, en remplacement du mot clef `new` : + +
+```yaml + - name: xxxx + image: linuxkit/xxxx:v0.6 + net: /run/netns/mynewnetns +``` +
+ + +## Construction et démarrage + +Toute la puissance de `linuxkit` repose dans son système de construction et +surtout de lancement. En effet, il peut construire des images pour un grand +nombre de plate-forme, mais il est également possible d'utiliser les API de ces +plates-formes pour aller y lancer des instances de cette image ! + +Pour construire l'image faite précédemment : + +
+```shell + linuxkit build hello.yml +``` +
+ +Cela va générer plusieurs fichiers dont un noyau (extrait de l'image de la +partie `kernel`) ainsi qu'une image. Exactement ce qu'attend QEMU ! Pour +tester, n'attendons pas davantage pour lancer : + +
+```shell + linuxkit run qemu -gui hello +``` +
+ + +## Ajouter un service + +Maintenant que notre machine fonctionne, que nous pouvons interagir avec elle, +tentons de se passer de l'interface de QEMU (option `-gui`) en ajoutant un +serveur SSH aux `services` : + +
+```yaml + - name: sshd + image: linuxkit/sshd:c4bc89cf0d66733c923ab9cb46198b599eb99320 +``` +
+ +Comme nous n'avons définis aucun mot de passe, il va falloir utiliser une clef +SSH pour se connecter. Voilà un bon début d'utilisation de la section `files` : + +
+```yaml + - path: root/.ssh/authorized_keys + source: ~/.ssh/id_rsa.pub + mode: "0600" +``` +
+ +Ceci va aller chercher votre clef RSA sur votre machine, pour la placer dans + +Notons qu'il est inutile d'ajouter un *bind mount* du dossier `.ssh` ainsi +recopié, car le *package* `linuxkit/sshd` défini déjà cela : +[pkg/sshd/build.yml#L5](https://github.com/linuxkit/linuxkit/blob/master/pkg/sshd/build.yml#L5). + + +## Interface réseau virtuelle + +Lorsque l'on souhaite se dissocier d'un *namespace* `net` afin de s'isoler, +mais que l'on veut tout de même pouvoir communiquer, il est nécessaire de créer +une interface `virtual ethernet` : + +
+```yaml + - name: db + image: mariadb:latest + net: new + runtime: + bindNS: + net: /run/netns/db + interfaces: + - name: vethin-db + add: veth + peer: veth-db +``` +
-Lien vers le fickit ## Exercice -Réaliser une recette `vault.yml` permettant de démarrer une instance. +Réalisez une recette `vault.yml` démarrant une instance du gestionnaire de +secrets [Hashicorp Vault](https://www.vaultproject.io/), utilisant une [base de +données au +choix](https://www.vaultproject.io/docs/configuration/storage/index.html) +(Consul, Etcd, MySQL, Cassandra, ...). + +Au démarrage, Vault devra déjà être configuré pour parler à sa base de données, +qui devra se trouver dans un conteneur isolé et non accessible d'internet. Il +faudra donc établir un lien `virtual ethernet` entre les deux conteneurs ; et +ne pas oublier de le configurer (automatiquement au *runtime*, grâce à un +[`poststart` +*hook*](https://github.com/opencontainers/runtime-spec/blob/master/config.md#posix-platform-hooks) +ou bien à un conteneur issu du *package* +[`ip`](https://github.com/linuxkit/linuxkit/tree/master/pkg/ip)). + +Les permissions étant généralement très strictes, vous aurez sans doute besoin +de les assouplir un peu en ajoutant des *capabilities* autorisées à vos +conteneurs, sans quoi vos conteneurs risquent d'être tués prématurément. + +En bonus, vous pouvez gérer la [persistance des +données](https://github.com/linuxkit/linuxkit/blob/master/examples/swap.yml) +stockées dans Vault. diff --git a/tutorial/docker-internals/oci.md b/tutorial/docker-internals/oci.md index c8730ae..0915bcc 100644 --- a/tutorial/docker-internals/oci.md +++ b/tutorial/docker-internals/oci.md @@ -73,3 +73,14 @@ des couches du système de fichiers, ainsi que l'historique de l'image. Dernière née de l'organisme, cette spécification fédère la notion de *registre* : une API REST sur HTTP où l'on peut récupérer des images, mais aussi en envoyer. + + +## Pour aller plus loin + +Si maintenant `docker` fait appel à un programme externe pour lancer +effectivement nos conteneurs, c'est que l'on peut changer cette implémentation +? la réponse dans l'article : + + +Et `containerd` dans l'histoire ? + diff --git a/tutorial/docker-internals/registry.md b/tutorial/docker-internals/registry.md index dab6c8b..2f34a87 100644 --- a/tutorial/docker-internals/registry.md +++ b/tutorial/docker-internals/registry.md @@ -33,6 +33,8 @@ souhaite récupérer le contenu du dépôt[^quiddepot] `hello-world` : ```shell 42sh$ curl "https://auth.docker.io/token"\ > "?service=registry.docker.io&scope=repository:library/hello-world:pull" | jq . +``` +```json { "token": "lUWXBCZzg2TGNUdmMy...daVZxGTj0eh", "access_token": "eyJhbGciOiJSUzI1NiIsI...N5q469M3ZkL_HA", @@ -111,7 +113,8 @@ Enfin, armé du `digest` de notre couche, il ne nous reste plus qu'à la demande
```shell - wget --header "Authorization: Bearer ${TOKEN}" "https://registry-1.docker.io/v2/library/hello-world/blobs/${LAYER_DIGEST}" + wget --header "Authorization: Bearer ${TOKEN}" \ + "https://registry-1.docker.io/v2/library/hello-world/blobs/${LAYER_DIGEST}" ```
@@ -147,11 +150,14 @@ Réalisez un script pour automatiser l'ensemble de ces étapes :
```shell 42sh$ cd $(mktemp) + 42sh$ ~/workspace/registry_play.sh library/hello + 42sh$ find . ./rootfs ./rootfs/hello + 42sh# chroot rootfs /hello Hello from Docker! [...] diff --git a/tutorial/docker-internals/rendu.md b/tutorial/docker-internals/rendu.md index 3c7a3f9..a43f1d1 100644 --- a/tutorial/docker-internals/rendu.md +++ b/tutorial/docker-internals/rendu.md @@ -31,14 +31,12 @@ cela dépendra de votre avancée dans le projet) :
``` login_x-TP5/ + login_x-TP5/docker-compose.yml login_x-TP5/clair_config/config.yaml login_x-TP5/nginx:mainline.html login_x-TP5/registry_play.sh - login_x-TP5/mydocker-export.sh login_x-TP5/config.json - login_x-TP5/unboundkit.yml login_x-TP5/vault.yml + login_x-TP5/pkg/... ```
- -Utilisez la même tarball pour le rendu que pour la partie précédente. diff --git a/tutorial/docker-internals/runc.md b/tutorial/docker-internals/runc.md index b9802d0..f622a6f 100644 --- a/tutorial/docker-internals/runc.md +++ b/tutorial/docker-internals/runc.md @@ -1,37 +1,131 @@ \newpage -https://ops.tips/blog/run-docker-with-forked-runc/ - `runc` ====== -`runc` est le programme qui est responsable de la création effective du conteneur : c'est lui qui va mettre en place les *namespaces*, les *capabilities*, les points de montages ou volumes, ... Attention, son rôle reste limité à la mise en place de l'environnement conteneurisé, ce n'est pas lui qui télécharge l'image, ni fait l'assemblage des couches de système de fichiers, entre autres. +`runc` est le programme qui est responsable de la création effective du +conteneur : c'est lui qui va mettre en place les *namespaces*, les +*capabilities*, les points de montages ou volumes, ... Attention, son rôle +reste limité à la mise en place de l'environnement conteneurisé, ce n'est pas +lui qui télécharge l'image, ni fait l'assemblage des couches de système de +fichiers, entre autres. -Si vous n'avez pas eu le temps de terminer l'exercice précédent, vous pouvez utiliser `docker export | tar -C rootfs xv`. +Aujourd'hui, le lancement de conteneur est faite avec `runc`, mais il est +parfaitement possible d'utiliser n'importe quel autre programme à sa place, à +partir du moment où il expose la même interface à Docker et qu'il accepte les +*bundle* OCI. -On va essayer de lancer un shell `alpine` avec un volume dans notre home :) +Pour appréhender l'utilisation de `runc` sans l'aide de Docker, nous allons +essayer de lancer un shell `alpine` avec un volume dans notre home. -D'abord on extraie l'image avec le script précédent, puis on crée le fichier de conf qui va bien. -Aujourd'hui, la création de conteneur est faite avec `runc`, mais il est parfaitement possible d'utiliser n'importe quel autre programme, à la place de `runc`, à partir du moment où il expose la même interface à Docker et qu'il accepte les bundle OCI. +## Prérequis -https://github.com/opencontainers/runtime-spec/blob/master/config.md +Vous devriez avoir le binaire `runc` ou `docker-runc`. Si ce n'est pas le cas, +vous pouvez télécharger la dernière version : +. La 1.0.0-rc5 est Ok. -https://hackernoon.com/docker-containerd-standalone-runtimes-heres-what-you-should-know-b834ef155426 + +## Extraction du rootfs + +À l'aide du script réalisé dans la partie précédentes, extrayons le rootfs +d'alpine : `library/alpine` dans le registre Docker. + +Si vous n'avez pas eu le temps de terminer l'exercice précédent, vous pouvez +utiliser `docker image save alpine | tar xv -C rootfs`. + + +## Modèle de configuration + +L'écriture complète d'un fichier `config.json` pour `runc` est plutôt +fastidieux et répétitif, nous allons donc gagner du temps et utiliser la +commande suivante, qui nous créera un modèle que nous adapterons un peu : + +
+```shell + runc spec +``` +
+ +Pour savoir à quoi correspondent tous ces éléments, vous pouvez consulter : + + + +## Test brut + +Voici comment nous pouvons tester le fonctionnement de notre *bundle* : + +
+```shell + 42sh$ ls + rootfs/ config.json + + 42sh# runc run --bundle . virli1 + / # _ +``` +
+ +Quelques informations sont disponibles, mais il ne faut pas s'attendre à +retrouver tout l'écosystème de `docker` ; ici il n'y a pas de gestion des +journaux, etc. : + +
+```shell + 42sh# runc list + ID PID STATUS BUNDLE CREATED OWNER + virli1 12345 running /tmp/work/runctest 2012-12-12T12:12:12.123456789Z root + + 42sh# runc state virli1 + ... +``` +
+ + +## Attacher notre *home* + +Dans le modèle de `config.json`, il y a déjà de nombreux systèmes de fichiers +qui sont montés. Nous pouvons les filtrer avec : + +
+```shell + 42sh$ jq .mounts config.json +``` +```json + [ + { + "destination": "/proc", + "type": "proc", + "source": "proc" + }, + [...] +``` +
+ +Pour avoir notre équivalent du `-v /home:/home` de `docker`, il va donc falloir +ajouter un élément à cette liste, demandant de *bind* : + +
+```json + { + "destination": "/home", + "type": "none", + "source": "/home", + "options": [ + "bind", + "ro" + ] + } +``` +
## Exercice -Réaliser un `config.json` qui permette de lancer le conteneur `nemunaire/fic-admin`. - - -## Exercice - -Serez-vous capable d'écrire un fichier `config.json` permettant d'obtenir le -même résultat que votre projet de moulette ? +Serez-vous capable de continuer l'édition de votre `config.json` afin d'obtenir +les mêmes restrictions que votre projet de moulette ? * CGroups : 1GB RAM, 100 PID, ... * strict minimum de capabilities ; -* volume étudiant pour correction ; +* filtres `seccomp` ; * carte réseau `veth` ; * ... diff --git a/tutorial/docker-internals/setup.md b/tutorial/docker-internals/setup.md index 4889831..05a05eb 100644 --- a/tutorial/docker-internals/setup.md +++ b/tutorial/docker-internals/setup.md @@ -3,6 +3,8 @@ Mise en place ============= +* `docker-compose` +* `venv` (Python3) * `jq` * `runc` * `containerd`