586 lines
18 KiB
Markdown
586 lines
18 KiB
Markdown
% Virtualisation légère
|
|
% Pierre-Olivier *Nemunaire* Mercier
|
|
% Samedi 29 novembre 2014
|
|
|
|
# Installation
|
|
|
|
## Noyau Linux
|
|
|
|
Ce TP requiert d'avoir un noyau Linux en version 3.8 au minimum. De plus, il
|
|
doit être compilé avec les options suivantes :
|
|
|
|
```
|
|
General setup --->
|
|
[*] Control Group support --->
|
|
[*] Freezer cgroup subsystem
|
|
[*] Device controller for cgroups
|
|
[*] Cpuset support
|
|
[*] Include legacy /proc/<pid>/cpuset file
|
|
[*] Simple CPU accounting cgroup subsystem
|
|
[*] Resource counters
|
|
[*] Memory Resource Controller for Control Groups
|
|
[*] Memory Resource Controller Swap Extension
|
|
[*] Memory Resource Controller Swap Extension enabled by default
|
|
[*] Enable perf_event per-cpu per-container group (cgroup) monitoring
|
|
[*] Group CPU scheduler --->
|
|
[*] Group scheduling for SCHED_OTHER
|
|
[*] Group scheduling for SCHED_RR/FIFO
|
|
<*> Block IO controller
|
|
-*- Namespaces support
|
|
[*] UTS namespace
|
|
[*] IPC namespace
|
|
[*] User namespace
|
|
[*] PID Namespaces
|
|
[*] Network namespace
|
|
[*] Networking support --->
|
|
Networking options --->
|
|
<M> 802.1d Ethernet Bridging
|
|
<M> 802.1Q VLAN Support
|
|
Device Drivers --->
|
|
[*] Network device support --->
|
|
<M> MAC-VLAN support
|
|
<M> Virtual ethernet pair device
|
|
Character devices --->
|
|
-*- Unix98 PTY support
|
|
[*] Support multiple instances of devpts
|
|
```
|
|
|
|
Une fois que vous aurez installé LXC, vous pouvez vérifier la compatibilité de
|
|
la configuration de votre noyau en utilisant la commande `lxc-checkconfig`.
|
|
|
|
|
|
## Docker
|
|
|
|
### Par le gestionnaire de paquets
|
|
|
|
Sous Debian et ses dérivés (Ubuntu, Mint, ...) le paquet et la commande ont été
|
|
nommés `docker.io`. Vous pouvez vous créer un alias `alias docker=docker.io`.
|
|
|
|
Sous les autres distribution, `docker` correspond a priori bien à la solution
|
|
de virtualisation légère que l'on va utiliser.
|
|
|
|
### Manuellement
|
|
|
|
L'équipe en charge de Docker met à disposition un script pour installer Docker
|
|
sur n'importe quel système :
|
|
|
|
```sh
|
|
curl -sSL https://get.docker.com/ | sh
|
|
```
|
|
|
|
### Vérifier la bonne marche de l'installation
|
|
|
|
Vous devriez maintenant être capable de lancer la commande suivante :
|
|
|
|
```
|
|
docker version
|
|
```
|
|
|
|
Une sortie similaire au bloc suivant devrait apparaître sur votre écran :
|
|
|
|
```
|
|
Client version: 1.3.2
|
|
Client API version: 1.15
|
|
Go version (client): go1.3.3
|
|
Git commit (client): 39fa2fa
|
|
OS/Arch (client): linux/amd64
|
|
Server version: 1.3.2
|
|
Server API version: 1.15
|
|
Go version (server): go1.3.3
|
|
Git commit (server): 39fa2fa
|
|
```
|
|
|
|
Si vous avez cette erreur : `dial unix /var/run/docker.sock: permission
|
|
denied.`, ajoutez votre utilisateur au groupe `docker` et **relancer votre
|
|
session** :
|
|
|
|
```
|
|
sudo gpasswd -a $USER docker
|
|
```
|
|
|
|
|
|
## LXC
|
|
|
|
Votre distribution fournit sans doute un paquet `lxc`.
|
|
|
|
|
|
# Docker
|
|
|
|
Docker est un outil haut niveau permettant de faire fonctionner
|
|
facilement les conteneurs.
|
|
|
|
## Composition de Docker
|
|
|
|
Docker est un daemon lancé au démarrage de votre machine, avec lequel
|
|
vous interagissez via un client qui se connecte au daemon au moyen
|
|
d'une socket (le client peut donc être sur une machine distincte du
|
|
daemon où sont exécutés les conteneurs).
|
|
|
|
|
|
## Mon premier conteneur
|
|
|
|
Afin de tester la bonne marche de votre installation, exécutez la
|
|
commande :
|
|
|
|
```
|
|
docker run hello-world
|
|
```
|
|
|
|
Cette commande va automatiquement exécuter une série de commandes pour
|
|
vous, comme indiqué dans le message affiché en retour :
|
|
|
|
D'abord, le démon va rechercher s'il possède localement l'image
|
|
*hello-world*. Si ce n'est pas le cas, il va aller la récupérer sur
|
|
hub.docker.com. Ce site met à votre disposition un grand nombre
|
|
d'images prêt à l'emploi : des systèmes de base comme Ubuntu, Debian,
|
|
Centos, etc. jusqu'à des conteneurs prêts à l'emploi : le serveur web
|
|
nginx, la base de données MySQL, un serveur node.js, etc.
|
|
|
|
Vous pouvez directement utiliser le client pour rechercher une image
|
|
sur le hub, en utilisant la commande `search` :
|
|
|
|
```
|
|
docker search mariadb
|
|
```
|
|
|
|
Vous pouvez mettre à jour vos images locales ou simplement
|
|
pré-télécharger des images depuis le hub en utilisant la commande
|
|
`pull` :
|
|
|
|
```
|
|
docker pull ubuntu
|
|
```
|
|
|
|
Pour consulter la liste des images dont vous disposez localement (soit
|
|
parce que vous les avez téléchargées, soit parce que vous les avez
|
|
créées vous-même), utilisez la commande `images` :
|
|
|
|
```
|
|
docker images
|
|
```
|
|
|
|
Vous devez constater la présence de deux images « Ubuntu », ayant un
|
|
*TAG* différent. Souvent, il existe plusieurs versions d'une même
|
|
image. Pour Ubuntu par exemple, vous avez la possibilité de lancer la
|
|
version `vivid`, `trusty` ou `precise`.
|
|
|
|
Chaque image est identifiable par son *Image ID* unique, les noms
|
|
d'images ainsi que leurs tags sont, comme les tags Git, une manière
|
|
humainement plus simple de faire référence aux identifiants.
|
|
|
|
Chaque nom d'image possède au moins un tag associé: *latest*, c'est le
|
|
tag qui est automatiquement recherché lorsque vous ne le précisez pas
|
|
en lançant l'image
|
|
|
|
|
|
## Exécuter un programme dans un conteneur
|
|
|
|
Maintenant que nous avons à notre disposition l'image d'un conteneur
|
|
Ubuntu, lançons-la !
|
|
|
|
La commande `run` de Docker prend comme derniers arguments le
|
|
programme à lancer dans le conteneur ainsi que ses éventuels
|
|
arguments. Essayons d'afficher un Hello World :
|
|
|
|
```
|
|
docker run ubuntu /bin/echo "Hello World"
|
|
```
|
|
|
|
Dans notre exemple, c'est bien le `/bin/echo` présent dans le
|
|
conteneur qui est appelé (et non pas le programme `/bin/echo` de la
|
|
machine hôte qui est transféré dans le conteneur).
|
|
|
|
|
|
## Modifier un conteneur
|
|
|
|
À chaque fois que vous lancez un `run`, un nouveau conteneur est créé
|
|
à partir de l'image que vous précisez (via un mécanisme de
|
|
Copy-On-Write, c'est donc très rapide et ne consomme pas beaucoup
|
|
d'espace disque). Cela signifie que lorsque vous exécutez une commande
|
|
modifiant le contenu d'un conteneur, cela ne modifie pas l'image de
|
|
base, mais crée une nouvelle image. Que vous pouvez ensuite utiliser
|
|
comme image de base.
|
|
|
|
Commençons par entrer dans un nouveau conteneur pour modifier l'image
|
|
:
|
|
|
|
```
|
|
docker run -it ubuntu /bin/bash
|
|
```
|
|
|
|
Vous voilà maintenant dans le conteneur ! Il est assez épuré, il n'y a
|
|
rien de superflu : vous n'avez pas d'éditeur de texte : ni vim, ni
|
|
emacs, même pas vi !
|
|
|
|
La première chose à faire est de mettre à jour la liste des paquets :
|
|
|
|
```
|
|
apt-get update
|
|
```
|
|
|
|
Il peut arriver que des paquets présents dans l'image officielle ne
|
|
soient pas à jour. Afin de garder un environnement cohérent, il est
|
|
recommandé de ne pas utiliser le gestionnaire de paquets pour mettre à
|
|
jour les paquets, mais plutôt de contacter le mainteneur de l'image
|
|
pour qu'il la mette à jour.
|
|
|
|
Installons maintenant un programme :
|
|
|
|
```
|
|
apt-get install nano
|
|
```
|
|
|
|
En attendant la fin de l'installation, jetez un œil à la commande dans
|
|
un autre terminal :
|
|
|
|
```
|
|
docker ps
|
|
```
|
|
|
|
Cette commande liste les conteneurs actifs. Notez le *Container ID*
|
|
ainsi que le *NAMES* du conteneur du conteneur actuellement en cours
|
|
d'installation de nano.
|
|
|
|
Lorsque l'installation de `nano` est terminée, quittez l'image en tapant `exit`.
|
|
|
|
Sauvegardez votre image modifiée avec la commande `commit` pour
|
|
pouvoir commencer directement de votre image avec `nano` :
|
|
|
|
```
|
|
docker commit CONTAINER my_nano
|
|
```
|
|
|
|
En remplaçant `CONTAINER` par le nom ou l'identifiant de votre
|
|
container. `my_nano` est le nom que vous voudrez utiliser à la place
|
|
d'`ubuntu` :
|
|
|
|
```
|
|
docker run -it my_nano /bin/bash
|
|
```
|
|
|
|
Vous constatez cette fois que vous pouvez lancer `nano`, alors que
|
|
vous ne pouvez toujours pas le faire dans un conteneur issue d'une
|
|
image `ubuntu` !
|
|
|
|
|
|
## Dockerfile
|
|
|
|
Pour construire une image, vous n'êtes pas obligé de passer par une
|
|
série de commits. Docker dispose d'un mécanisme permettant
|
|
d'automatiser la construction de nouvelles images. Vous pouvez arriver
|
|
au même résultat que ce que l'on a réussi à faire précédemment en
|
|
utilisant le Docker file suivant :
|
|
|
|
```
|
|
FROM ubuntu:latest
|
|
|
|
RUN apt-get update
|
|
RUN apt-get install -y nano
|
|
```
|
|
|
|
La syntaxe d'un `Dockerfile` est simple, le premier mot de chaque
|
|
ligne est l'intitulé d'une instruction (que l'on écrit généralement en
|
|
majuscule), elle est suivie de ses arguments.
|
|
|
|
Dans notre exemple, nous utilisons `FROM` qui indique une image de
|
|
départ à utiliser ; `RUN` est une commande qui sera exécutée dans le
|
|
conteneur.
|
|
|
|
Pour lancer la construction de la nouvelle image, créer un nouveau
|
|
dossier ne contenant que votre fichier `Dockerfile`, placez-vous
|
|
dedans, puis utilisez la commande `build` :
|
|
|
|
```
|
|
docker build --tag=my_editor .
|
|
```
|
|
|
|
Une fois la construction de l'image terminée, vous pouvez la lancer et
|
|
constater l'existence de notre éditeur favori :
|
|
|
|
```
|
|
docker run -it my_editor /bin/bash
|
|
```
|
|
|
|
Consultez <https://docs.docker.com/reference/builder/> pour la liste
|
|
complète des instructions reconnues.
|
|
|
|
|
|
## Exposer des ports
|
|
|
|
Construisons maintenant un conteneur avec un serveur web :
|
|
|
|
```
|
|
FROM my_editor
|
|
|
|
RUN apt-get update
|
|
RUN apt-get install -y nginx
|
|
|
|
EXPOSE 80
|
|
```
|
|
|
|
L'instruction `EXPOSE` sera traité plus tard par le client Docker
|
|
(équivalent à l'argument `--expose`). Il s'agit de préciser les ports
|
|
sur lesquels votre image écoute.
|
|
|
|
En utilisant l'option `-P` du `run`, vous allez pouvoir assigner une
|
|
redirection de port aléatoire sur la machine hôte vers votre
|
|
conteneur :
|
|
|
|
```
|
|
docker build --tag=my_webserver .
|
|
docker run -it -P my_webserver /bin/bash
|
|
service nginx start
|
|
```
|
|
|
|
Dans un autre terminal, lancer un `docker ps` et consulter la colonne
|
|
*PORTS* pour connaître le port choisit par Docker pour effectuer la
|
|
redirection.
|
|
|
|
Rendez-vous ensuite dans votre navigateur sur <http://localhost:49153/>.
|
|
|
|
À vous de jouer : utilisez l'instruction `COPY` pour afficher votre
|
|
propre `index.html` remplaçant celui installé de base par nginx.
|
|
|
|
|
|
## Lancement de commande automatique
|
|
|
|
Vous pouvez placer dans un `Dockerfile` une instruction `CMD` qui sera
|
|
exécutée si aucune commande n'est passée lors du `run`, par exemple :
|
|
|
|
```
|
|
CMD nginx -g "daemon off;"
|
|
```
|
|
|
|
```
|
|
docker build --tag=my_nginx .
|
|
docker run -d -P my_nginx
|
|
```
|
|
|
|
L'option `-d` passée au `run` lance le conteneur en tâche de
|
|
fond. Si vous constatez via un `docker ps` que le conteneur s'arrête
|
|
directement, retirer cette option pour voir ce qui ne va pas, ou
|
|
utilisez la commande `docker logs`.
|
|
|
|
|
|
## Volumes
|
|
|
|
Il est possible de partager des répertoires entre plusieurs
|
|
conteneurs. Pour ce faire, il faut déclarer dans le `Dockerfile` une
|
|
ou plusieurs instructions `VOLUME` avec le chemin du répertoire à
|
|
considérer comme volume (il est également possible de le faire via
|
|
l'option `--volume` du client). Ces deux lignes sont équivalentes :
|
|
|
|
```
|
|
VOLUME /var/log/nginx
|
|
```
|
|
|
|
```
|
|
docker run -v /var/log/nginx my_nginx
|
|
```
|
|
|
|
Pour monter les volumes dans un autre conteneur, on utilise l'argument
|
|
`--volume-from` du client, en indiquant le nom du conteneur avec
|
|
lequel on souhaite partager les volumes :
|
|
|
|
```
|
|
docker run -it --volume-from romantic_archimedes ubuntu /bin/bash
|
|
```
|
|
|
|
Vous constaterez que le répertoire `/var/log/nginx` est partagé entre
|
|
`romantic_archimedes` et le dernier conteneur lancé.
|
|
\newline
|
|
|
|
Le concept principal de Docker est de concevoir des conteneurs applicatifs : on
|
|
va préférer assigner un unique rôle à un conteneur (donc géralement on ne va
|
|
lancer qu'une seule application par conteneur) et concevoir un service complet
|
|
en créant un groupe de conteneur, partageant des données entre-eux par des
|
|
volumes.
|
|
|
|
Une lecture intéressante sur ce sujet est sans doute [cet article de Michael
|
|
Crosby](http://crosbymichael.com/advanced-docker-volumes.html).
|
|
|
|
### Data Volume Container
|
|
|
|
Dans de nombreuses situation, il est intéressant de séparer les données de
|
|
l'application, et donc d'avoir un conteneur exécutant l'application et un
|
|
second stockant les données.
|
|
|
|
Cela est particulièrement utile dans le cas d'une base de données : on veut
|
|
pouvoir mettre à jour le conteneur exécutant le serveur, sans pour autant
|
|
perdre les données.
|
|
|
|
L'idée derrière le concept de `Data Volume Container` est de partager un volume
|
|
avec un conteneur dont le seul rôle est de stocker les données.
|
|
|
|
Il est parfaitement possible de partager un volume avec un conteneur qui n'est
|
|
plus lancé. En effet, tant que vous n'avez pas demandé explicitement à un
|
|
conteneur d'être supprimé, il est préservé dans un coin en attendant des jours
|
|
meilleurs.
|
|
|
|
Voici comment on pourrait lancer un conteneur exécutant une base de données :
|
|
|
|
```
|
|
docker run -v /var/lib/mysql --name dbdata busybox
|
|
docker run --volume-from dbdata -e MYSQL_ROOT_PASSWORD=mysecretpassword -d mysql
|
|
```
|
|
|
|
Le premier conteneur, sans commande passée, va s'arrêter dès son
|
|
lancement. Busybox est l'une des plus petites images possédant tous les outils
|
|
de base (il est possible d'obtenir un shell en cas de besoin). Il expose un
|
|
volume qui sera utiliser comme stockage persistant.
|
|
|
|
Le second conteneur va lancer le serveur MySQL et utiliser le répertoire
|
|
partagé pour stocker les données.
|
|
|
|
Lorsqu'il y aura besoin de mettre à jour le conteneur MySQL, les données ne
|
|
seront pas perdues (et s'il y avait besoin de migrer les données entre les deux
|
|
versions des conteneurs, un conteneur intermédiaire pourrait parfaitement s'en
|
|
charger).
|
|
|
|
Cela facile également les sauvegardes, qui peuvent s'exécuter dans un conteneur
|
|
distinct, dédié à la tâche de sauvegarde.
|
|
|
|
|
|
## Lier les conteneurs
|
|
|
|
En plus de vouloir partager des répertoires entre deux conteneurs, il est
|
|
souvent nécessaire de partager des ports.
|
|
|
|
Pour automatiser le partage d'informations sur les IP et ports exposés, la
|
|
commande `run` possède l'option `--link` qui permet de définir dans les
|
|
variables d'environnement du conteneur que l'on va lancer.
|
|
|
|
Le détail des variables ajoutées dans cette situation est disponible
|
|
[ici](https://docs.docker.com/userguide/dockerlinks/#environment-variables).
|
|
|
|
On utiliser généralement cette liaison pour fournir au conteneur hébergeant un
|
|
site web dynamique l'IP et le port où trouver la base de données :
|
|
|
|
```
|
|
docker run -e MYSQL_ROOT_PASSWORD=mysecretpassword -d --name db1 mysql
|
|
docker run --link db1 my_nginx
|
|
```
|
|
|
|
### Ambasador
|
|
|
|
Afin d'abstraire le plus possible l'infrastructure sous-jacente et d'autoriser
|
|
les migrations de conteneurs, on utilise le modèle *ambassador*.
|
|
|
|
On lancera systématiquement un conteneur entre deux conteneurs que l'on veut
|
|
lier : l'ambassadeur. Celui-ci s'occupera de router correctement le trafic. En
|
|
cas de changement de route (si l'un des conteneurs change de machine hôte par
|
|
exemple), on a simplement à redémarrer l'ambassadeur plutôt que le conteneur
|
|
principal.
|
|
|
|
La documentation officielle pour ce modèle est disponible
|
|
(ici)[https://docs.docker.com/articles/ambassador_pattern_linking/].
|
|
|
|
|
|
# LXC
|
|
|
|
Contrairement à Docker, l'utilisation de LXC est beaucoup plus proche de
|
|
l'administration système classique, car l'approche est beaucoup plus bas
|
|
niveau.
|
|
|
|
## Lancer un conteneur
|
|
|
|
Avec le paquet LXC que vous avez installé, vous avez également récupéré un
|
|
certain nombre de modèles de système (souvent installés dans le dossier
|
|
`/usr/share/lxc/templates/`).
|
|
|
|
La méthode la plus simple pour lancer un conteneur LXC est d'utiliser l'un de
|
|
ces modèles qui va installer tout un environnement pour vous. On utilise pour
|
|
cela la commande `lxc-create` :
|
|
|
|
```
|
|
lxc-create --name toto_first --template ubuntu
|
|
```
|
|
|
|
Ce modèle va créer un dossier dans `/var/lib/lxc/` portant le nom que vous avez
|
|
précisé. Ce dossier va contenir la configuration LXC du conteneur, la table des
|
|
partitions s'il y a besoin de faire des montages particuliers et enfin le
|
|
dossier `rootfs` contenant le système en lui-même.
|
|
|
|
On peut maintenant démarrer le conteneur :
|
|
|
|
```
|
|
lxc-start --name toto_first
|
|
```
|
|
|
|
À la différence de Docker qui va ne lancer que l'application (ou les
|
|
applications listées dans la ligne de commande) dans son environnement, LXC va
|
|
appeler `/sbin/init` et démarrer tous les services que l'on peut s'attendre à
|
|
trouver dans n'importe quelle machine virtuelle plus classique (la seule
|
|
différence réside donc dans le fait que le noyau est partagé avec l'hôte).
|
|
|
|
Généralement on lance `lxc-start` avec l'option `--daemon`, puis on utilise
|
|
`lxc-console` qui permet de se détacher de la console via le binding `^A+q`.
|
|
|
|
Connectez-vous, lancez quelques commandes puis éteignez la machine avec `sudo
|
|
poweroff` ou dans un autre terminal via `lxc-stop --name toto_first`.
|
|
|
|
|
|
## Persistance des données
|
|
|
|
Contrairement à Docker, lorsque vous arrêtez un conteneur, les modifications
|
|
apportées sont conservées. Si vous appelez à nouveau `lxc-start --name
|
|
toto_first`, vous constaterez que votre historique contient les dernières
|
|
commandes que vous avez tapé et si vous avez apporté d'autres modifications sur
|
|
le système, celles-ci sont toujours visibles.
|
|
|
|
|
|
## Le réseau
|
|
|
|
Le modèle ubuntu que vous avez utilisé initialise un fichier de configuration
|
|
sans paramètres pour le réseau. Vous n'avez donc pas d'interface dans le
|
|
conteneur pour le connecter au réseau.
|
|
|
|
Un excellent article détaillant les différents types de réseau est accessible
|
|
(ici)[https://blog.flameeyes.eu/2010/09/linux-containers-and-networking].
|
|
|
|
N'ayant qu'une seule interface physique sur la machine et n'ayant pas accès à
|
|
la configuration des VLAN de la pièce, il ne nous reste que deux méthodes pour
|
|
obtenir du réseau dans nos conteneurs : Virtual Ethernet ou MACVLAN.
|
|
|
|
### Virtual Ethernet
|
|
|
|
Virtual Ethernet est la configuration la plus simple. On met en place un pont
|
|
sur la machine hôte, puis on crée une interface `veth` par conteneur que l'on
|
|
veut lancer. On n'oubliera pas d'ajouter ces interfaces au pont.
|
|
|
|
Voici un extrait de configuration correspondant au paramétrage d'une interface
|
|
`eth0` pour un conteneur donné :
|
|
|
|
```
|
|
lxc.network.type = veth
|
|
lxc.network.flags = up
|
|
lxc.network.link = br0
|
|
```
|
|
|
|
Cette technique a pour inconvénient de laisser au noyau le soin de router les
|
|
paquets selon leur adresse IP, ce qui peut être lent et coûteux étant donné que
|
|
la carte est placé en mode de promiscuité.
|
|
|
|
|
|
### MACVLAN
|
|
|
|
Ici, le noyau va orienter les paquets en fonction de leur adresse MAC de
|
|
destination.
|
|
|
|
```
|
|
lxc.network.type = macvlan
|
|
lxc.network.macvlan.mode = bridge
|
|
lxc.network.flags = up
|
|
lxc.network.link = br0
|
|
```
|
|
|
|
|
|
## Stockage
|
|
|
|
Par défaut, le stockage se fait dans l'arborescence du système hôte, mais il
|
|
est possible d'utiliser d'autres backends tels que Btrfs, LVM, overlayfs, AUFS
|
|
ou ZFS.
|
|
|
|
Pour utiliser un type de stockage particulier, préciser lors de la création de
|
|
l'environnement du conteneur `-B [btrfs|zfs|lvm|...]`.
|