docker-internals: tuto ready

This commit is contained in:
nemunaire 2018-11-15 23:38:25 +01:00
parent 5291bd365e
commit de21be218a
7 changed files with 395 additions and 26 deletions

View File

@ -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 \

View File

@ -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 :
<https://git.nemunai.re/?p=lectures/adlin.git;a=tree>
## Prérequis
Si vous n'avez pas déjà le binaire `linuxkit`, vous pouvez télécharger ici :
<https://github.com/linuxkit/linuxkit/releases/latest>.
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 :
<div lang="en-US">
```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
```
</div>
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 à
<https://github.com/linuxkit/linuxkit/blob/master/pkg/getty/README.md#linuxkit-debug>
## *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 :
<div lang="en-US">
```yaml
- name: getty
image: linuxkit/getty:2eb742cd7a68e14cf50577c02f30147bc406e478
net: new
```
</div>
Ou inversement, pour persister dans le namespace initial :
<div lang="en-US">
```yaml
- name: getty
image: linuxkit/getty:2eb742cd7a68e14cf50577c02f30147bc406e478
pid: host
```
</div>
### 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 :
<div lang="en-US">
```yaml
- name: getty
image: linuxkit/getty:2eb742cd7a68e14cf50577c02f30147bc406e478
net: new
runtime:
bindNS: /run/netns/mynewnetns
```
</div>
À 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` :
<div lang="en-US">
```yaml
- name: xxxx
image: linuxkit/xxxx:v0.6
net: /run/netns/mynewnetns
```
</div>
## 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 :
<div lang="en-US">
```shell
linuxkit build hello.yml
```
</div>
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 :
<div lang="en-US">
```shell
linuxkit run qemu -gui hello
```
</div>
## 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` :
<div lang="en-US">
```yaml
- name: sshd
image: linuxkit/sshd:c4bc89cf0d66733c923ab9cb46198b599eb99320
```
</div>
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` :
<div lang="en-US">
```yaml
- path: root/.ssh/authorized_keys
source: ~/.ssh/id_rsa.pub
mode: "0600"
```
</div>
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` :
<div lang="en-US">
```yaml
- name: db
image: mariadb:latest
net: new
runtime:
bindNS:
net: /run/netns/db
interfaces:
- name: vethin-db
add: veth
peer: veth-db
```
</div>
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.

View File

@ -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 :
<https://ops.tips/blog/run-docker-with-forked-runc/>
Et `containerd` dans l'histoire ?
<https://hackernoon.com/docker-containerd-standalone-runtimes-heres-what-you-should-know-b834ef155426>

View File

@ -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
<div lang="en-US">
```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}"
```
</div>
@ -147,11 +150,14 @@ Réalisez un script pour automatiser l'ensemble de ces étapes :
<div lang="en-US">
```shell
42sh$ cd $(mktemp)
42sh$ ~/workspace/registry_play.sh library/hello
42sh$ find
.
./rootfs
./rootfs/hello
42sh# chroot rootfs /hello
Hello from Docker!
[...]

View File

@ -31,14 +31,12 @@ cela dépendra de votre avancée dans le projet) :
<div lang="en-US">
```
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/...
```
</div>
Utilisez la même tarball pour le rendu que pour la partie précédente.

View File

@ -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 :
<https://github.com/opencontainers/runc/releases>. 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 :
<div lang="en-US">
```shell
runc spec
```
</div>
Pour savoir à quoi correspondent tous ces éléments, vous pouvez consulter :
<https://github.com/opencontainers/runtime-spec/blob/master/config.md>
## Test brut
Voici comment nous pouvons tester le fonctionnement de notre *bundle* :
<div lang="en-US">
```shell
42sh$ ls
rootfs/ config.json
42sh# runc run --bundle . virli1
/ # _
```
</div>
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. :
<div lang="en-US">
```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
...
```
</div>
## 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 :
<div lang="en-US">
```shell
42sh$ jq .mounts config.json
```
```json
[
{
"destination": "/proc",
"type": "proc",
"source": "proc"
},
[...]
```
</div>
Pour avoir notre équivalent du `-v /home:/home` de `docker`, il va donc falloir
ajouter un élément à cette liste, demandant de *bind* :
<div lang="en-US">
```json
{
"destination": "/home",
"type": "none",
"source": "/home",
"options": [
"bind",
"ro"
]
}
```
</div>
## 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` ;
* ...

View File

@ -3,6 +3,8 @@
Mise en place
=============
* `docker-compose`
* `venv` (Python3)
* `jq`
* `runc`
* `containerd`