Sommaire


Introduction

Les containers Linux:

Namespaces:

Control Groups (cgroups):

Architecture micro-services

VM vs Containers

archi-micro-services.png

archi-micro-service-ex.png

Avantages:

inconvenient:

Application Cloud Native:

DevOps:

DevoOps, quelques outils et produits:

Docker Hub

Registry officiel de docker, on retrouve des logiciels packages dans des images utilisable immediatement Docker Hub

Si l’image n’est pas present sur la machine, la commande docker container run recupere l’image sur le docker hub.

On peut lancer un interpreteur interactif avec le flag -ti (pour sortir d’un shell interactif du container lance sans le fermer Ctrl+P Ctrl+Q) depuis une image docker (REPL), utile pour tester des commandes:

# pour lancer un interperteur python dans un container on utilise la commande "docker container run"
# l'interpreteur est package dans l'image "python" en version ou plutot de tag "3"
# le flag "-ti" permet l'interactivite avec le processus du container
# -t alloue un pseudo TTY et -i garde STDIN ouvert
$ docker container run -ti python:3
$ docker container run -ti ruby:2.5.1
# ici en fin de commande nous avons "-alpine" ceci permet d'utiliser une image qui est base sur
# la distibution Alpine Linux de taille tres reduite et beaucoup plus securise
$ docker container run -ti node:8.12-alpine

Exemple, BDD MongoDB:

# le flag "-d" permet d'executer l'application en tache de fond, on recupere un ID permettant d'interagir avec
# la flag "-p" permet de rendre disponible l'application MongoDB en local
$ docker container run -p 27017:27017 -d mongo:4.0

On peut ensuite lancer Compass pour explorer la BDD en utilisant le port 27017.

Exemple, Redmine (logiciel de gestion de projets):

permet d’avoir redmine (fait en ruby) dans un container. user/pass par defaut “admin”

# lancement du container base sur redmine 3
# ici on a mappe le port 3000 du container vers le port 80 de la machine hote
$ docker container run -p 80:3000 redmine:3

Des stacks completes

Elastic (gestion des logs)

Interface de visualisation:

Stockage, Indexation, Analyse:

Ingestion:

Creation d’un docker compose, chaque service est une image:

# docker-compose.yml

version: '3.6'
services:
  logstash:
    image: logstash:5.5.2
    volumes:
      - ./logstash.conf:/config/logstash.conf
    command: ["logstash", "-f", "/config/logstash.conf"]
    ports:
      - 8080:8080
  elasticsearch:
    image: elasticsearch:5.5.2
    environment:
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
  kibana:
    image: kibana:5.5.2
    ports:
      - 5601:5601

Il faut configurer la conf de logstash logstash.conf. Pour resumer, le bloc input{} permet d’ecouter sur du HTTP. Le bloc filter{} permet sur chaque entrer de log d’extraire des parametres et d’en ajouter d’autres. Le bloc output{} une fois le filter fait, on renvoie le resultat sous JSON a elasticsearch.

puis on lance l’application sur la machine locale en utilisant le binaire docker compose:

$ docker-compose up -d

si par exemple dans le repertoire courant il y a un fichier log nginx.log, on utilise le script send-logs.sh pour envoyer les logs a logstash.

$ ./send-logs.sh

puis on va sur l’interface de Kibana localhost:5601. Quand un INDEX est trouve on peut cliquer sur le bouton Create pour creer un index.

TICK (series temporelles)

tick

Cette stack est bien adapte pour une application IOT.

Telegraf:

Kapacitor:

InfluxDB:

Chronograf:

Fichier docker compose:

# tick.yml

version: "3.6"
services:
  telegraf:
    image: telegraf
    configs:
    - source: TELEGRAF_CONF
      target: /etc/telegraf/telegraf.conf
    ports:
    - 8186:8186
  influxdb:
    image: influxdb
  chronograf:
    image: chronograf
    ports:
    - 8888:8888
    command: ["chronograf", "--influxdb-url=http://influxdb:8086"]
  kapacitor:
    image: kapacitor
    environment:
    - KAPACITOR_INFLUXDB_0_URLS_0=http://influxdb:8086
configs:
  TELEGRAF_CONF:
    file: ./telegraf.conf

puis on execute la stack en utilisant docker stack deploy qui permet de lancer l’application sur l’ensemble des machines:

$ docker stack deploy -c tick.yml tick

La plateforne Docker

Presentation:

Le client docker communique avec le daemon dockerd via une API REST (ils sont ecrit en Go).

Doc api et daemon:

Cote serveur:

Cote client:

On peut configuer un client pour communiquer avec un daemon distant en modifiant la variable d’environement DOCKER_HOST.

Les concepts essentiels:

concept essentiels

Docker Hub:

cluster swarm

docker Swarm:

c’est un ensemble de docker host ayant dockerd mit en cluster, permet d’orchestrer des aplications (comme kubernetes) de tel sorte qu’elles tournent constament de la meme facon dont elles ont ete specifiees

Differentes editions

Docker Community Edition (CE):

Docker Entreprise Edition (EE):

Installation: differents produits

Playground Docker en ligne: https://labs.play-with-docker.com/

Quelques commandes:

Configuration du daemon

PDF : Configuration du Daemon

Communication client / serveur

PDF : Communication entre Client/Daemon


Les containers avec Docker

Creation d’un container

exemple:

Foreground vs Background

exemple:

$ docker container run -d alpine ping 8.8.8.8

ici, si on ne le met pas en tache de fond alors on a plus la main sur le prompt, il faut ajouter -d :

$ docker container run nginx:1.14-alpine

La publication de port

exemple:

# le port 80 de l'instance Nginx tournant dans le container est publie sur le port 8080 de l'hote
$ docker container run -d -p 8080:80 nginx
sadd516545s6a4d5dg135d4fsd318d4fs9fd4d83sd84fg6sd4s684s6f81f6s12sd3sf4f8

$ docker container ls
CONTAINER ID      IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
sadd516545s6a4    nginx     "nginx -g 'daemon of..."    About a minut ago   Up 2 seconds    0.0.0.0:8080->80/tcp    angry_chandrasek

Bind-mount

(bind-mount Interdite en Production Askip)

$ docker container run -v HOST_PATH:CONTAINER_PATH ...
$ docker container run --mount type=bind, src=HOST_PATH, dst=CONTAINER_PATH ...

Bind-mount: socket /var/run/docker.sock:

Exemple 1, creation d’un container depuis un autre container:

# lancement d'un container avec bind mount de la socket
$ docker container run --name admin -ti -v /var/run/docker.sock:/var/run/docker.sock alpine

# creation d'un container depuis le container admin
/  curl -XPOST --unix-socket /var/run/docker.sock -d '{"Image":"nginx:1.12.2"}' -H 'Content-Type:application/json' http://localhost/containers/create
{"Id":"aa15w6ad54as6d51a6cac801fa3df5466a5s1d8a6cac2as1c3a4sxff84xfgg","Warnings":null}

# lancement du container depuis le container admin
/ curl -XPOST --unix-socket /var/run/docker.sock http://localhost/containers/aa15...xfgg/start

Exemple 2, acces aux evenements du daemon docker depuis container:

# lancement d'un container avec bind mount de la socket
$ docker container run --name admin -ti -v /var/run/docker.sock:/var/run/docker.sock alpine

# suivi des evenements du daemon docker en temps reel
$ curl --unix-socket /var/run/docker.sock http://localhost/events
# resultat: voir le screenshot log-example.png ci-dessous

log example

Exemple 3, gestion d’un hote docker depuis un container avec Portainer:

# lancement de Portainer, application web de gestion d'hotes Docker et de cluster Swarm
$ docker container run -d -p 9000:9000 -v /var/run/docker.sock:/var/run/docker.sock portainer/portainer

Portainer a besoin de la socket /var/run/docker.sock pour avoir acces a l’API du daemon Docker sur lequel il tourne et pouvoir administrer.

ATTENTION: il est possible de supprimer des fichier de la machine hote via le container, pour eviter cela, il faut utiliser le flag ro Read-Only

Exemple: suppression d’un fichier sur la machine hote

# lancement d'un container avec bind mount du systeme de fichiers de l'hote
$ docker run -v /:/host -ti alpine
/ rm /host/bin/sh
/ exit
$ sh
bash: sh: command not found

# bind-mount avec flag ro (read-only)
$ docker run -ti -v /:/host:ro alpine
/ rm /host/bin/sh
mv: can't rename 'sh': Read-only file system

Limitation des ressources (Bonne Pratique!)

# limite de consommation de la RAM
## ici l'image estesp/hogit contient un process dont le seul but est de
## consommer de la ram, une fois que le process atteint 32Mo, Docker va kill le process
$ docker container run --memory 32m estesp/hogit
=> 00MKilled

# differentes valeurs de l'option --cpus sur un processeur 4-core
## utilisation des 4 cores soit 100% du CPU
## l'image progrium/stress permet de stresser les CPU
$ docker run -it --rm progrium/stress --cpu 4
=> CPU:   98% usr   1% sys  0% nic  0% idle   0% io   0% irq  0% sirq

## utilisation de 50% d'un core soit environ 12% du CPU
$ docker run --cpus 0.5 -it --rm progrium/stress --cpu 4
=> CPU:   12% usr   1% sys  0% nic  86% idle   0% io   0% irq  0% sirq

## utilisation de 2 cores soit 50% du CPU
$ docker run --cpus 2 -it --rm progrium/stress --cpu 4
=> CPU:   50% usr   2% sys  0% nic  47% idle   0% io   0% irq  0% sirq

## utilisation des cores 1 et 4
$ docker container run --cpuset-cpus 0,3

## utiliser pas plus de 3 CPU maximum
$ docker conainer run --cpus="3"

Lancer un container redis avec une limite RAM

$ docker container run --memory 4g --memory-reservation 2g -d redis

Les droits d’un container

# lancement de la commande sleep dans un container base sur l'image Alpine
$ docker container run -d alpine sleep 1000
0as5d1g8c354df64sd2f4164sd8f4vc23s8d4f635497f5h4y842

# verification du owner du processus depuis la machine hote
## le container est lance avec l'user root
$ ps aux | grep sleep
591 root    0:00 sleep 1000

# lancement d'un container base sur l'image officielle de MongoDB
$ docker container run -d mongo:4.0

# verification du owner du processus depuis la machine hote
## le container n'est pas lance avec l'utilisateur root
## mais avec l'utilisateur dont le uid est 999
$ ps aux | grep mongo
5866 999    0:00 mongod --bind_ip_all

Des options utiles

# specification du nom avec le flag --name
$ docker container run -d --name debug alpine:3.7 sleep 1000

# suppression du container quand il est stoppe avec le flag --rm
## par defaut apres arret du container ses fichiers ne sont pas supprimes
$ docker container run -rm --name debug alpine:3.7 sleep 1000

# redemarrage automatique si le container s'arrete avec le flag --restart=on-failure
$ docker container run --name api --restart=on-failure username/api

Les commandes de base

les commandes de base ls:

commande ls

les commandes de base inspect:

commande inspect

les commandes de base logs:

$ docker container run -d --name ping ubuntu ping 8.8.8.8
3asd3sa212ad1a5da1sd31sa8dad

$ docker container logs -f ping
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=55 time=4.09 ms
64 bytes from 8.8.8.8: icmp_seq=2 ttl=55 time=4.31 ms

les commandes de base exec:

# lancement du processus "sleep 10000" dans un container base sur alpine
$ docker container run -d --name debug alpine:3.6 sleep 10000

# execution d'un shell dans le container debug
$ docker container exec -ti debug sh
/ ps aux
PID   USER   TIME   COMMAND
1      root   0:00   sleep 1000
5      root   0:00   sh
9      root   0:00   ps aux

Tant que le process PID 1 est en cours d’execution le container reste actif

les commandes de base stop:

les commandes de base rm:

Des alias utiles

# liste les containers actifs
$ alias dls='docker container ls'

# liste les containers actifs et ceux arretes
$ alias dlsa='docker container la -a'

# stoppe tous les containers
$ alias dstopall='docker container stop $(docker container ls -aq)'

# supprime tous les containers
$ alias drmall='docker container rm $(docker container ls -aq)'

# lance un shell interactif (bash ou sh) dans un container
$ dshell (){
  (docker container exec -ti $1 bash) || (docker container exec -ti $1 sh);
}

Exercices: Les commandes de bas de gestion des containers

Une commande utile

Lancer un shell dans la machine virtuelle dans laquelle tourne le daemon:

sur linux, le client et le daemon tourne sur la meme machine. Mais dans docker for Mac/Windows ou docker toolbox, le daemon tourne sur une VM alors que le client tourne sur la machine locale. Pour obtenir un shell dans la VM il faut lancer un container dans cette VM et utiliser quelques options pour que le process lance dans ce container au proces de PID 1 de la machine hote.


Les images Docker

Definition

Que contient une image ?

Union filesystem

union filesystem

Copy-on-Write

Modification d’un fichier dans le container:

  1. Le fichier original est copie dans la layer du container depuis une layer sous-jacente
  2. La modification est persistee dans la layer associee au container

copy on write

en gros, on recupere les layers de l’image et on les copie dans le container, une fois dedans on peut modifier les layers car les layers de l’images sont en Read Only alors que une fois copie dans le container les layers sont en +RW.

La layer d’un container, est la layer read-write créé lorsqu’un container est lancé. C’est la layer dans laquelle tous les changements effectués dans le container sont sauvegardés. Cette layer est supprimée avec le container et ne doit donc pas être utilisée comme un stockage persistant.

Execices: Layer associee au container et system de fichier d’une image

Telecharger une image en local

$ docker image pull mongo:3.6

Voir les etapes de constructions d’une image

$ docker image history mongo:3.6

Si l’on regarde le Dockerfile utilisé pour la création de l’image, on peut voir que chaque entrée de la commande précédente correspond à une instruction du Dockerfile.

Obtenir l’ensemble des informations de l’image

$ docker image inspect mongo:3.6

lister les ports exposes

docker image inspect --format '{​{json .ContainerConfig.ExposedPorts }}' mongo:3.6 | jq .

On peut exporter une image dans un fichier .tar

$ docker save -o mongo-3.6.tar mongo:3.6

# on peut ouvrir l'archive pour voir ce qu'il y a
$ tar -xvf mongo-3.6.tar

0669adaf1d338e563cddf2357c7cb23160d8351ed6531fda6361114610c7c8d8/
0669adaf1d338e563cddf2357c7cb23160d8351ed6531fda6361114610c7c8d8/VERSION
0669adaf1d338e563cddf2357c7cb23160d8351ed6531fda6361114610c7c8d8/json
0669adaf1d338e563cddf2357c7cb23160d8351ed6531fda6361114610c7c8d8/layer.tar
08cc52baf3ebad3caad28998914ac0baceb06e99269f85de99ff1df74aa8781f/
08cc52baf3ebad3caad28998914ac0baceb06e99269f85de99ff1df74aa8781f/VERSION
08cc52baf3ebad3caad28998914ac0baceb06e99269f85de99ff1df74aa8781f/json
08cc52baf3ebad3caad28998914ac0baceb06e99269f85de99ff1df74aa8781f/layer.tar
29ad4814ef0661b8b4d90bc74e3b3d006287ca94e6441b597ae01383d07b518e.json
2efd7711b196f9eb64b2f5ac68b27049db794f577c93be7cc543b2639254f278/
2efd7711b196f9eb64b2f5ac68b27049db794f577c93be7cc543b2639254f278/VERSION
2efd7711b196f9eb64b2f5ac68b27049db794f577c93be7cc543b2639254f278/json
2efd7711b196f9eb64b2f5ac68b27049db794f577c93be7cc543b2639254f278/layer.tar
3a12fe84f5307da469767e144d9927b8befac39a0d1d0c27ff48aeaf94601480/
3a12fe84f5307da469767e144d9927b8befac39a0d1d0c27ff48aeaf94601480/VERSION
3a12fe84f5307da469767e144d9927b8befac39a0d1d0c27ff48aeaf94601480/json
3a12fe84f5307da469767e144d9927b8befac39a0d1d0c27ff48aeaf94601480/layer.tar
4c62e5488f84ca6b3fd5da923f50f56c23270284a93c3b27113c615438cdd73c/
4c62e5488f84ca6b3fd5da923f50f56c23270284a93c3b27113c615438cdd73c/VERSION
4c62e5488f84ca6b3fd5da923f50f56c23270284a93c3b27113c615438cdd73c/json
4c62e5488f84ca6b3fd5da923f50f56c23270284a93c3b27113c615438cdd73c/layer.tar
6ce39836d016cfe616e38e93c370887e26bf1bb3bb28b61d9287bedc5dc5854b/
6ce39836d016cfe616e38e93c370887e26bf1bb3bb28b61d9287bedc5dc5854b/VERSION
6ce39836d016cfe616e38e93c370887e26bf1bb3bb28b61d9287bedc5dc5854b/json
6ce39836d016cfe616e38e93c370887e26bf1bb3bb28b61d9287bedc5dc5854b/layer.tar
6ffb582f37c3205cd326d5ba682105b7391bec8db1d8d218d1522a193c3629b1/
6ffb582f37c3205cd326d5ba682105b7391bec8db1d8d218d1522a193c3629b1/VERSION
6ffb582f37c3205cd326d5ba682105b7391bec8db1d8d218d1522a193c3629b1/json
6ffb582f37c3205cd326d5ba682105b7391bec8db1d8d218d1522a193c3629b1/layer.tar
7c6f095e2dee15e868558c4debf6272ccbe028e854fb146e5f6d518982084bec/
7c6f095e2dee15e868558c4debf6272ccbe028e854fb146e5f6d518982084bec/VERSION
7c6f095e2dee15e868558c4debf6272ccbe028e854fb146e5f6d518982084bec/json
7c6f095e2dee15e868558c4debf6272ccbe028e854fb146e5f6d518982084bec/layer.tar
b61eb2912db4a8e40d9ee142f40fdd3856f19433f2ae0285a49f240ce4011cde/
b61eb2912db4a8e40d9ee142f40fdd3856f19433f2ae0285a49f240ce4011cde/VERSION
b61eb2912db4a8e40d9ee142f40fdd3856f19433f2ae0285a49f240ce4011cde/json
b61eb2912db4a8e40d9ee142f40fdd3856f19433f2ae0285a49f240ce4011cde/layer.tar
bcc4785d63a54e4648c250b515e8f0962dcf488d8f8257eac3abd7db8726a392/
bcc4785d63a54e4648c250b515e8f0962dcf488d8f8257eac3abd7db8726a392/VERSION
bcc4785d63a54e4648c250b515e8f0962dcf488d8f8257eac3abd7db8726a392/json
bcc4785d63a54e4648c250b515e8f0962dcf488d8f8257eac3abd7db8726a392/layer.tar
e00c8c90283abfde2570ea569be13b16455f19df5ae3ddb56e783f4401bed19e/
e00c8c90283abfde2570ea569be13b16455f19df5ae3ddb56e783f4401bed19e/VERSION
e00c8c90283abfde2570ea569be13b16455f19df5ae3ddb56e783f4401bed19e/json
e00c8c90283abfde2570ea569be13b16455f19df5ae3ddb56e783f4401bed19e/layer.tar
e31c06a18fd79ea6fd5f42cb878d0c62eeafd12292fc5a8906a2501ec7080db7/
e31c06a18fd79ea6fd5f42cb878d0c62eeafd12292fc5a8906a2501ec7080db7/VERSION
e31c06a18fd79ea6fd5f42cb878d0c62eeafd12292fc5a8906a2501ec7080db7/json
e31c06a18fd79ea6fd5f42cb878d0c62eeafd12292fc5a8906a2501ec7080db7/layer.tar
e530bf9b170faaaa23f82dbcf2384dd7e99bb7b979893a19dfa513eeb69989b2/
e530bf9b170faaaa23f82dbcf2384dd7e99bb7b979893a19dfa513eeb69989b2/VERSION
e530bf9b170faaaa23f82dbcf2384dd7e99bb7b979893a19dfa513eeb69989b2/json
e530bf9b170faaaa23f82dbcf2384dd7e99bb7b979893a19dfa513eeb69989b2/layer.tar
manifest.json
repositories

On peut remarquer ici que cette archive est un ensemble de layer, pour chacune d’entre elle on a un répertoire contenant:

Si l’on extrait le contenu de l’une de ces layers, on obtient un système de fichier. L’ensemble de ces layers, et donc de ces systèmes de fichiers, constitue le système de fichier global de l’image.

Exemple de contenu de l’une des layers de l’image:

$ tar -xvf e00c8c90283abfde2570ea569be13b16455f19df5ae3ddb56e783f4401bed19e/layer.tar
etc/
etc/apt/
etc/apt/sources.list.d/
etc/apt/sources.list.d/mongodb-org.list

Pour supprimer l’image:

$ docker image rm mongo:3.6

Ou est stocke mon image ?

Stockage d’une image

Dans un execice precedent, nous avons cree une image nommee ping:1.0, nous allons voir ici ou cette image est stockee.

Reprenons le Dockerfile de l’exercice:

FROM ubuntu:16.04
RUN apt-get update -y && apt-get install -y iputils-ping
ENTRYPOINT ["ping"]
CMD ["8.8.8.8"]

A partir de ce Dockerfile, l’image est buildée avec la commande suivante:

$ docker image build -t ping:1.0 .
Sending build context to Docker daemon  2.048kB
Step 1/4 : FROM ubuntu:16.04
 ---> 5e8b97a2a082
Step 2/4 : RUN apt-get update -y && apt-get install -y iputils-ping
 ---> Using cache
 ---> 4cd5304ad0fb
Step 3/4 : ENTRYPOINT ["ping"]
 ---> Using cache
 ---> d2846bbd30e8
Step 4/4 : CMD ["8.8.8.8"]
 ---> Using cache
 ---> 00a905f2bd5a
Successfully built 00a905f2bd5a
Successfully tagged ping:1.0

Pour lister les images présentes localement on utilise la commande docker image ls (on reverra cette commande un peu plus loin). Pour ne lister que les images qui ont le nom ping on le précise à la suite de ls.

$ docker image ls ping
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ping                1.0                 00a905f2bd5a        4 weeks ago         159MB

Notre image est constituée d’un ensemble de layers, il faut voir chaque layer comme un morceau de système de fichiers. L’ID de l’image (dans sa version courte) est 00a905f2bd5a, nous allons voir à partir de cette identifiant comment l’image est stockée sur la machine hôte (la machine sur laquelle tourne le daemon Docker).

Tout se passe dans le répertoire /var/lib/docker, c’est le répertoire ou Docker gère l’ensemble des primitives (containers, images, volumes, networks, …). Et plus précisément dans /var/lib/docker/image/overlay2/, overlay2 étant le driver en charge du stockage des images.

Note: si vous utilisez “Docker for Mac” ou “Docker for Windows”, il est nécessaire d’utiliser la commande suivante pour lancer un shell dans la machine virtuelle dans laquelle tourne le daemon Docker. On pourra ensuite explorer le répertoire /var/lib/docker depuis ce shell.

$ docker run -it --privileged --pid=host debian nsenter -t 1 -m -u -n -i sh

Plusieurs fichiers/répertoires ont un nom qui contient l’ID de notre image comme on peut le voir ci-dessous:

/var/lib/docker/image/overlay2 # find . | grep 00a905f2bd5a
./imagedb/content/sha256/00a905f2bd5aa3b1c4e28611704717679352a619bcdc4f8f6851cf459dc05816
./imagedb/metadata/sha256/00a905f2bd5aa3b1c4e28611704717679352a619bcdc4f8f6851cf459dc05816
./imagedb/metadata/sha256/00a905f2bd5aa3b1c4e28611704717679352a619bcdc4f8f6851cf459dc05816/lastUpdated
./imagedb/metadata/sha256/00a905f2bd5aa3b1c4e28611704717679352a619bcdc4f8f6851cf459dc05816/parent

Content :

le premier fichier contient un ensemble d’information concernant cette image, notamment les paramètres de configuration, l’historique de création (ensemble des commandes qui ont servi à construire le système de fichiers contenu dans l’image), et également l’ensemble des layers qui la constituent. Une grande partie de ces informations peuvent également être retrouvées avec la commande:

$ docker image inspect ping:1.0

Parmi ces éléments, on a donc les identifiants de chaque layer:

"rootfs": {
    "type": "layers",
    "diff_ids": [
      "sha256:644879075e24394efef8a7dddefbc133aad42002df6223cacf98bd1e3d5ddde2",
      "sha256:d7ff1dc646ba52a02312b535446d6c9b72cd09fda0480524e4828554efb2f748",
      "sha256:686245e78935e73b737c9a82111c3c7df35f5529d06ce8c2f9a7cd32ec90b456",
      "sha256:d73dd9e652956dccbbef716de4b172cc15fff644cc92fc69d221cc3a1cb89a39",
      "sha256:2de391e51d731ba02b708038a7f98b7103061b916727bcd165e9ee6402f4cdde",
      "sha256:3045bfad4cfefecabc342600d368863445b12ed18188f5f2896c5389b0e84b66"
    ]
  }

Si l’on considère la première layer (celle dont l’ID est 6448...), on voit dans /var/lib/docker/image/overlay2 qu’il y a un répertoire dont le nom correspond à l’ID de cette layer, celui-ci contient plusieurs fichiers :

/var/lib/docker/image/overlay2 # find . | grep '644879075e24394efef8a7dddefbc133aad42'
./layerdb/sha256/644879075e24394efef8a7dddefbc133aad42002df6223cacf98bd1e3d5ddde2
./layerdb/sha256/644879075e24394efef8a7dddefbc133aad42002df6223cacf98bd1e3d5ddde2/size
./layerdb/sha256/644879075e24394efef8a7dddefbc133aad42002df6223cacf98bd1e3d5ddde2/tar-split.json.gz
./layerdb/sha256/644879075e24394efef8a7dddefbc133aad42002df6223cacf98bd1e3d5ddde2/diff
./layerdb/sha256/644879075e24394efef8a7dddefbc133aad42002df6223cacf98bd1e3d5ddde2/cache-id
./distribution/v2metadata-by-diffid/sha256/644879075e24394efef8a7dddefbc133aad42002df6223cacf98bd1e3d

Ceux-ci contiennent différentes information sur la layer en question. Parmi celles-ci, le fichier cache-id nous donne l’identifiant du cache qui a été généré pour cette layer.

/var/lib/docker/image/overlay2 # cat ./layerdb/sha256/644879075e24394efef8a7dddefbc133aad42002df6223cacf98bd1e3d5ddde2/cache-id
49908d07e177f9b61dc273ec7089efed9223d3798ad1d86c78d4fe953e227668

Le système de fichier construit dans cette layer est alors accessible dans le répertoire:

/var/lib/docker/overlay2/49908d07e177f9b61dc273ec7089efed9223d3798ad1d86c78d4fe953e227668/diff/`

LastUpdated:

Ce fichier contient la date de dernière mise à jour de l’image:

/var/lib/docker/image/overlay2 # cat ./imagedb/metadata/sha256/00a905f2bd5...459dc05816/lastUpdated
2018-07-31T07:32:04.6840553Z

parent:

Ce fichier contient l’identifiant du container qui a servi à créer l’image.

/var/lib/docker/image/overlay2 # cat ./imagedb/metadata/sha256/00a905f2bd5459dc05816/parent
sha256:d2846bbd30e811ac8baaf759fc6c4f424c8df2365c42dab34d363869164881ae

On retrouve d’ailleurs ce container dans l’avant dernière étape de création de l’image.

Step 3/4 : ENTRYPOINT ["ping"]
 ---> Using cache
 ---> d2846bbd30e8

Ce container est celui qui a été commité pour créer l’image finale.

En résumé: il est important de garder en tête qu’une image est constituée de plusieurs layers. Chaque layer est une partie du système de fichiers de l’image finale. C’est le rôle du driver de stockage de stocker ces différentes layers et de construire le système de fichiers de chaque container lancé à partir de cette image.

Dockerfile

Exemple de Dockerfile utilise pour une application Node.js:

# image de base
FROM node:8.11.1-alpine

# copie de la liste des dependances
COPY package.json /app/package.json

# installation/compilation des dependances
RUN cd /app && npm install

# copie du code applicatif
COPY . /app/

# exposition du port HTTP
EXPOSE 80

# positionnement du repertoire de travail
WORKDIR /app

# commande executee au lancement d'un container (c'est un alias equivalent a node index.js)
CMD ["npm","start"]

Les instructions principales

FROM:

ENV:

CPOY / ADD:

RUN:

Shell: lance dans un shell avec /bin/sh -c par defaut

RUN apt-get update -y && apt-get install

Exec: non lance dans un shell

RUN ["/bin/bash", "-c", "echo hello"]

EXPOSE:

# exemple mongoDB
EXPOSE 27017
CMD ["mongod"]

# exemple nginx
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

VOLUME:

# exemple mongoDB
RUN mkdir -p /data/db /data/configdb \
      && chown -R mongodb:mongodb /data/db /data/configdb
VOLUME /data/db /data/configdb

COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]

EXPOSE 27017
CMD ["mongod"]

USER: (Bonne pratique!)

HEALTHCHECK: (Bonne pratique!)

FROM node:8.11-alpine
RUN apk update && apk add curl
HEALTHCHECK --interval=5s --timeout=3s --retries=3 CMD curl -f http://localhost:8000/health || exit 1
COPY package.json /app/package.json
WORKDIR /app
RUN npm install
COPY . /app
CMD ["npm","start"] #(equivalent a node index.js pour executer le serveur)

ENTRYPOINT / CMD:

ENTRYPOINT ["curl"]
CMD ["--help"]

Creation d’images

Exemple: Application Node.js

le serveur web index.js renvoie l’heure a chaque requete recu:

//index.js

var express = require('express');
var util = require('util');
var app = express();
app.get('/', function(req,res){
  res.setHeader('Content-Type','text/plain');
  res.end(util.format("%s - %s", new Date(), 'Got HTTP Get Request'));
});
app.listen(process.env.PORT || 80)
//package.json

{
  "name": "testnode",
  "version": "0.0.1",
  "main": "index.js",
  "script": {
    "start": "node index.js"
  },
  "dependencies": {"express": "^4.14.0"}
}
# dokerfile

# image de base
FROM node:8.11-alpine

# copie de la  liste des dependances
COPY package.json /app/package.json

# installation / compilation des dependances
RUN cd /app && npm install

# copie du code applicatif
COPY . /app/

WORKDIR /app

#port d'ecoute
EXPOSE 80

# commande a lancer (equivalent a node index.js)
CMD ["npm","start"]

Ensuite on construit l’image avec:

$ docker image build -t app:1.0 .

Les instruction du dockerfile sont execute de facon sequentielle. Enfin, nous pouvons lancer un container de notre appli:

$ docker container run -p 8080:80 app:1.0

Exemple: Application Python

Cette cree un QRcode contenant le string "Hello".

# barcode.py

# https://githb.com/mmulqueen/pyStrich
from pystrich.datamatrix import DataMatrixEncoder

encoder = DataMatrixEncoder('Hello')
print(encoder.get_ascii())
#dockerfile

FROM python:3

ADD barcode.py /

RUN pip install pystrich

CMD [ "python", "/barcode.py" ]

On cree ensuite l’image de l’application python avec:

$ docker image build -t barcode .

Enfin nous pouvons utiliser le container:

$ docker container run barcode

Exemple: Application Java

Execute un "Hello World"

// Main.java

public class Main
{
  public static void main(String[] args){
    System.out.println("Hello World")
  }
}
# dockerfile

FROM openjdk:10

COPY . /usr/src/myapp

WORKDIR /usr/src/myapp

RUN javac Main.java

CMD ["java","Main"]

On build l’image puis on lance le container:

$ docker image build -t hellojava:v1.0 .

$ docker container run hellojava:1.0
Hello World

Exemple: Application Php

A chaque fois qu’une requet GET est recu, on envoie les informations du serveur.

# index.php

<?php
  pprint_r($_SERVER)
?>
# dockerfile pour servir les pages php avec apache
# autre possibilite avec Nginx+php-fpm
FROM php:7-appache
COPY . /var/www/html/

on build et on execute le container:

$ docker image build -t phpreq:1.0 .

$ docker run -p 8888:80 phpreq:1.0

Exercices: creation d’image

Creer une image a partir d’un container existant avec commit

Soit un container “c1” ayant Alpine pour OS et curl installe. Pour creer une image “myping” a partir du container “c1”, il faut utiliser la commande suivante:

$ docker container commit c1 myping

Maintenant on peut creer un container avec l’image “myping” avec curl deja installe:

$ docker container run -ti myping

Nous avons donc lancé un container, ajouté un binaire dans ce container et commité le tout en une nouvelle image. Le binaire est donc présent dans cette image. Commiter un container pour créer une image n’est pas l’approche recommandée. La création d’une image se fait à partir d’un Dockerfile, fichier texte contenant l’ensemble des commandes nécessaires.

le fichier .dockerignore

il est possible de creer un fichier .dockerignore et mettre tous fichiers/dossiers qui ne doivent pas etre dans le build.

Entrypoint vs Cmd

Nous allons illustrer sur plusieurs exemples l’utilisation des instructions ENTRYPOINT et CMD. Ces instructions sont utilisées dans un Dockerfile pour définir la commande qui sera lancée dans un container.

Dans un Dockerfile, les instructions ENTRYPOINT et CMD peuvent être spécifiées selon 2 formats:

Reecriture a l’execution du container

ENTRYPOINT et CMD sont 2 instructions du Dockerfile, mais elle peuvent cependant être écrasées au lancement d’un container:

Instruction ENTRYPOINT utilisée seule

L’utilisation de l’instruction ENTRYPOINT seule permet de créer un wrapper autour de l’application. Nous pouvons définir une commande de base et lui donner des paramètres suplémentaires, si nécessaire, au lancement d’un container. Dans ce premier exemple, vous allez créer un fichier Dockerfile-v1 contenant les instructions suivantes:

FROM	alpine
ENTRYPOINT	["ping"]

Créez ensuite une image, nommée ping:1.0, à partir de ce fichier.

$ docker image build -f Dockerfile-v1 -t ping:1.0 .

Lancez maintenant un container basé sur l’image ping:1.0

$ docker container run ping:1.0

La commande ping est lancée dans le container (car elle est spécifiée dans ENTRYPOINT), ce qui produit le message suivant:

BusyBox	v1.26.2	(2017-05-23	16:46:25	GMT)	multi-call	binary.
Usage:	ping	[OPTIONS]	HOST
Send	ICMP	ECHO_REQUEST	packets	to	network	hosts
# en gros il manque @IP a la commande

Par défaut, aucune machine hôte n’est ciblée, et à chaque lancement d’un container il est nécessaire de préciser un FQDN ou une IP. La commande suivante lance un nouveau container en lui donnant l’adresse IP d’un DNS Google (8.8.8.8), nous ajoutons également l’option -c 3 pour limiter le nombre de ping envoyés.

$ docker container run ping:1.0 -c 3 8.8.8.8

Nous obtenons alors le résultat suivant.

PING 8.8.8.8 (8.8.8.8): 56 data bytes
64	bytes	from	8.8.8.8:	seq=0	ttl=37	time=8.731	ms
64	bytes	from	8.8.8.8:	seq=1	ttl=37	time=8.503	ms
64	bytes	from	8.8.8.8:	seq=2	ttl=37	time=8.507	ms

La commande lancée dans le container est donc la concaténation de l’ENTRYPOINT et de la commande spécifiée lors du lancement du container (tout ce qui est situé après le nom de l’image).

Comme nous pouvons le voir dans cet exemple, l’image que nous avons créée est un wrapper autour de l’utilitaire ping et nécessite de spécifier des paramêtres supplémentaires au lancement d’un container.

Instruction CMD utilisée seule

De la même manière, il est possible de n’utiliser que l’instruction CMD dans un Dockerfile, c’est d’ailleurs très souvent l’approche qui est utilisée car il est plus simple de manipuler les instructions CMD que les ENTRYPOINT.

Créez un fichier Dockerfile-v2 contenant les instructions suivantes:

FROM	alpine
CMD	["ping"]

Créez une image, nommée ping:2.0, à partir de ce fichier.

$ docker image build -f Dockerfile-v2 -t ping:2.0 .

Si nous lançons maintenant un nouveau container, il lancera la commande ping comme c’était le cas avec l’exemple précédent dans lequel seul l’ENTRYPOINT était défini.

Nous n’avons cependant pas le même comportement que précédemment, car pour spécifier la machine à cibler, il faut redéfinir la commande complète à la suite du nom de l’image.

Si nous ne spécifions que les paramètres de la commande ping, nous obtenons un message d’erreur car la commande lancée dans le container ne peut pas être interpretée.

Il faut redéfinir la commande dans sa totalité, ce qui est fait en la spécifiant à la suite du nom de l’image

$ docker container run ping:2.0	ping -c	3	8.8.8.8
PING	8.8.8.8	(8.8.8.8):	56	data	bytes
64	bytes	from	8.8.8.8:	seq=0	ttl=37	time=10.223	ms
64	bytes	from	8.8.8.8:	seq=1	ttl=37	time=8.523	ms
64	bytes	from	8.8.8.8:	seq=2	ttl=37	time=8.512	ms

Instruction ENTRYPOINT et CMD utilisée seule

Il est également possible d’utiliser ENTRYPOINT et CMD en même temps dans un Dockerfile, ce qui permet à la fois de créer un wrapper autour d’une application et de spécifier un comportement par défaut.

Nous allons illustrer cela sur un nouvel exemple et créer un fichier Dockerfile-v3 contenant les instructions suivantes:

FROM	alpine
ENTRYPOINT	["ping"]
CMD	["-c3",	"localhost"]

Ici, nous définissons ENTRYPOINT et CMD, la commande lancée dans un container sera la concaténation de ces 2 instructions: ping -c3 localhost.

Créez une image à partir de ce Dockerfile, nommez la ping:3.0, et lançez un nouveau container à partir de celle-ci.

$	docker image build -f Dockerfile-v3 -t ping:3.0 .
$	docker container run ping:3.0

Vous devriez alors obtenir le résultat suivant:

PING	localhost	(127.0.0.1):	56	data	bytes
64	bytes	from	127.0.0.1:	seq=0	ttl=64	time=0.062	ms
64	bytes	from	127.0.0.1:	seq=1	ttl=64	time=0.102	ms
64	bytes	from	127.0.0.1:	seq=2	ttl=64	time=0.048	ms

Nous pouvons écraser la commande par défaut et spécifier une autre adresse IP

$ docker container run ping:3.0	8.8.8.8
PING	8.8.8.8	(8.8.8.8):	56	data	bytes
64	bytes	from	8.8.8.8:	seq=0	ttl=38	time=9.235	ms
64	bytes	from	8.8.8.8:	seq=1	ttl=38	time=8.590	ms
64	bytes	from	8.8.8.8:	seq=2	ttl=38	time=8.585	ms

Il faut alors faire un CTRL-C pour arrêter le container car l’option -c3 limitant le nombre de ping n’a pas été spécifiée. Cela nous permet à la fois d’avoir un comportement par défaut et de pouvoir facilement le modifier en spécifiant une autre commande.

Multi-stages build

Sur l’exemple de l’application Java:

Execute un "Hello World"

// Main.java

public class Main
{
  public static void main(String[] args){
    System.out.println("Hello World")
  }
}
# dockerfile traditionnel

FROM openjdk:10

COPY . /usr/src/myapp

WORKDIR /usr/src/myapp

RUN javac Main.java

CMD ["java","Main"]

Nous allons faire un multi-stages build. on utilise une image de base qui contient un JDK complet, on copie les sources avec l’instruction COPY et on les compilent dans l’instruction RUN pour generer un binaire. une fois le binaire cree, on a plus besoin de l’environnement JDK pour le cree mais juste d’un environnement pour le faire tourner soit le JRE.

# dockerfile avec multi stage build

FROM openjdk:10 as build
COPY . /usr/src/myapp
WORKDIR /usr/src/myapp
RUN javac Main.java

FROM openjdk:10-jre-slim
COPY --form=build /usr/src/myapp/Main.class /usr/src/myapp/
WORKDIR /usr/src/myapp
CMD ["java","Main"]

Exemple: Multi-stages build application Reactjs

# creation d'une application React
$ npm init react-app api
# dockerfile avec multi stage build

FROM node:8.11-alpine as build
WORKDIR /app
COPY package.json ./package.json
RUN npm install
COPY . ./
RUN npm run build

FROM nginx:1.14.0
COPY --form=build /app/build/ /usr/share/nginx/html/
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

puis on build et on lance un container:

$ docker image build -t app:1.0 .
$ docker run -p 9000:80 app:1.0

Exercice: Multistage build

Comme nous l’avons vu, le Dockerfile contient une liste d’instructions qui permet de créer une image. La première instruction est FROM, elle définit l’image de base utilisée. Cette image de base contient souvent beaucoup d’éléments (binaires et librairies) dont l’application finale n’a pas besoin (compilateur, …). Ceci qui peut impacter de façon considérable la taille de l’image et également sa sécurité puisque cela peut considérablement augmenter sa surface d’attaque. C’est la qu’intervint le multistage build.

Le multi-stage build, introduit dans la version 17.05 de Docker permet, au sein d’un seul Dockerfile, d’effectuer le process de build en plusieurs étapes. Chacune des étapes peut réutiliser des artefacts (fichiers résultant de compilation, assets web, …) créés lors des étapes précédentes. Ce Dockerfile aura plusieurs instructions FROM mais seule la dernière sera utilisée pour la construction de l’image finale.

Si nous reprenons l’exemple du serveur http ci dessus, nous pouvons dans un premier temps compiler le code source en utilisant l’image golang contenant le compilateur. Une fois le binaire créé, nous pouvons utiliser une image de base vide, nommée scratch, et copier le binaire généré précédemment.

# appli web en Go 300Mo->6Mo grace au multistage build
FROM	golang:1.9.0-alpine	as	build
WORKDIR	/go/src/github.com/lucj/who
COPY	http.go	.
RUN	CGO_ENABLED=0	GOOS=linux	go	build	-a	-installsuffix	cgo	-o	http	.
FROM	scratch
COPY	--from=build	/go/src/github.com/lucj/who/http	.
CMD	["./http"]

L’exemple que nous avons utilisé ici se base sur une application écrite en Go. Ce langage a la particularité de pouvoir être compilé en un binaire static, c’est à dire ne nécessitant pas d’être “linké” à des librairies externes. C’est la raison pour laquelle nous pouvons partir de l’image scratch. Pour d’autres langages, l’image de base utilisée lors de la dernière étape du build pourra être différente (alpine, …)

Pour cette simple application, le multistage build a permit de supprimer 270M de binaires et librairies dont la présence est inutile dans l’image finale. L’exemple d’une application écrite en go est extrème, mais le multistage build fait partie des bonnes pratiques à adopter quel que soit le language de développement de l’application.

Prise en compte du cache

Chaque instruction utilise le cache cree lors du build precedent, pour chaque step lors du build nous aurons des Using cache donc creation de l’image quasi immediate.

Il faut que les element qui sont susceptible d’etre le plus souvent modifie soient le plus bas possible dans le dockerfile de tel sorte qu’on puisse faire appel au cache un maximum.

Exercice: Cache

Nous observons que chaque étape du build à été effectuée une nouvelle fois. Il serait intéressant de faire en sorte qu’une simple modification du code source ne déclenche pas le build des dépendances.

une bonne pratique, à suivre dans l’écriture d’un Dockerfile, est de faire en sorte que les éléments qui sont modifiés le plus souvent (code de l’application par exemple) soient positionnés plus bas que les éléments qui sont modifiés moins fréquemment (liste des dépendances).

Dockerfile de base:

FROM node:10.15-alpine
COPY . /app/
RUN cd /app && npm install
WORKDIR /app
EXPOSE 80
CMD ["npm", "start"]

On peut alors modifier le Dockerfile de la façon suivante:

FROM node:10.15-alpine
COPY package.json	/app/package.json
RUN cd /app && npm install
COPY . /app/
WORKDIR /app
EXPOSE 80
CMD ["npm", "start"]

L’approche suivie ici est la suivante:

On rebuild alors une nouvelle fois l’image en lui donnant le tag pong:1.2. Un cache est créé pour chaque étape du build. On fait la modification suivante dans le fichier pong.js. puis l’on build une nouvelle fois l’image, en la nommant cette fois pong:1.3.

Nous observons ici que le cache est utilisé jusqu’au step 3 (—> Using cache). Il est ensuite invalidé lors du step 4 car le daemon Docker a détecter le changement de code que nous avons effectué. La prise en compte du cache permet souvent de gagner beaucoup de temps lors de la phase d build.

Le contexte de build

$ docker image build -t app:v2.0 .
Sending build context to Docker daemon 4.096kB
# fichier .dockerignore
.git
node_modules

Donc faire attention a ce que contient dossier ou on se trouve car si on lance un build, tout le contenu du dossier sera present dans l’image.

Les commandes de base

Les differentes commandes pour la primitive image. Pour avoir plus d’info, voir docker image --help.

les commandes de base pull:

les commandes de base push:

les commandes de base inspect:

Permet d’avoir une vue detaille d’une image.

$ docker image inspect alpine

$ docker image inspect -f '' alpine

les commandes de base history:

Permet de donner l’historique des layers qui sont creer lors de la creation de l’image.

$ docker history alpine

les commandes de base ls:

Permet d’enumerer les images presentes localement. Soit les images telecharge depuis un registry et les images creees avec la commande build.

les commandes de base save / load:

La commande save permet d’exporter une image dans un fichier .tar et la commande load permet de creer une image depuis un fichier .tar.

Exemple:

# liste les images presents en local
$ docker images
alpine ...

# on save l'image dans un fichier tar
$ docker save -o alpine.tar alpine

# on verifie que le tar existe dans notre dossier
$ ls
alpine.tar

# on supprime l'image localement
$ docker image rm alpine

# liste les images presents en local
$ docker images
"vide"

# on load l'image present dans le tar
$ docker load < alpine.tar
Loaded image: alpine:latest

# liste les images presents localement
$ docker image ls
alpine ...
$ docker image ls
$ docker image rm ubuntu
$ docker image ls -q
$ docker image rm $(docker image ls -q)

Registry

Il faut se logguer pour pouvoir push une image dans un repository docker hub grace a la commande docker login.

$ docker image pull alpine:3.8
$ docker image push username/www:2.0

# autre exemple avec l'appli pong en nodejs
# on ajoute un tag
$ docker image tag pong:1.3 username/pong:1.3
# se log a docker hub
$ docker login -u username
# push l'image
$ docker image push username/pong:1.3

Differents providers: les registries Docker

Differents providers: autres registries de l’ecosysteme

Docker Hub: overview

Differentes categories:

Les images officielles sont scannees regulierement au niveau binaire (CVE) pour detecter les vulnerabilitees.

Un utilisateur peut creer son propre repository:

Docker Hub: CI/CD

Exercice: Docker Hub

Docker Open Source Registry

docker fournit un registry open-source que l’on peut monter dans un container

Un daemon docker ne peut communiquer avec un registry que de facon securise:

# lancement du registry dans un container
$ docker container run -d -p 5000:5000 registry:2.6.2

# liste des images presentes dans le registry
$ curl localhost:5000/v2/_catalog
{"repositories":[]}

# download de l'image nginx depuis le Docker Hub
$ docker image pull nginx:1.14

# tag de l'image nginx selon le format URL:PORT/NAME:VERSION
$ docker image tag nginx:1.14 localhost:5000/nginx:1.14

# upload de l'image dans le registry
$ docker image push localhost:5000/nginx:1.14

# liste des images presentes dans le registry
$ curl localhost:5000/v2/_catalog
{"repositories":["nginx"]}

Si le registry ne tourne pas sur la meme machine ex: docker image push 198.168.0.1:5000/nginx:1.14, alors il faut un certificat ou ajouter l’option --insecure-registry , on peut faire un systemctl status docker et recuperer la valeur dans Loaded le service docker soit /lib/systemd/system/docker.service puis ajouter a la ligne ExecStart=..... ceci --insecure-registry [@IP:PORT du registry] puis relancer le docker daemon systemctl daemon-reload puis systemctl restart docker.

Une autre facon de faire est de cree un fichier dans /etc/docker/daemon.json puis mettre la liste de registry ou il peut se connecter:

{
  "insecure-registries":["192.168.0.1:5000"]
}

Exercice: Registry open source

Exercice: Configuration du registry open source


Stockage

Container et persistance de donnees

# creation du fichier /tmp/test_file dans un container test
$ docker container run -ti --name test alpine sh
touch /tmp/test_file
exit

# le fichier /tmp/test_file est visible depuis la machine hote
$ find /var/lib/docker -name 'test_file'
/var/lib/docker/overlay2/3aw3d3sd4ds51da...as85das1da/diff/tmp/test_file

# suppression du container
$ docker container rm test
test

# le fichier a ete supprime avec le container
$ find /var/lib/docker -name 'test_file'
'vide'

Volume

Definition a l’execution:

Les commandes de base avec la primitive volume

Exemple:

# creation du volume db-data en utilisant le driver par defaut (stockage sur la machine hote)
$ docker volume create --name db-data
db-data

$ docker volume ls
DRIVER    VOLUME NAME
local     db-data

$ docker volume inspect db-data
[
  {
    "CreateAt": "2019-09-01T01:12:11Z"
    "Driver": "local",
    "Label": {},
    "Mountpoint": "/var/lib/docker/volumes/db-data/_data",
    "Name": "db-data",
    "Option": {},
    "Scope": "local"
  }
]

Utilisation/reutilisation d’un volume existant:

# le volume db-data est monte dans le container
$ docker container run -d --name db -v db-data:/data/db mongo:4.0

# le contenu du repertoire du container est visible dans le volume via cette commande
$ ls /var/lib/docker/volumes/db-data/_data

Exercice: Volumes

voir l’exo il y a plein d’exemple de montage, partage de volume host->container.

Drivers de volumes

Exemple: plugin sshfs

# installation du plugin
$ docker plugin install vieux/sshfs
Plugin "vieux/sshfs" is requesting the following privileges:
- network: [host]
- mount: [/var/lib/docker/plugins/]
- mount: []
- device: [/dev/fuse]
- capabilities: [CAP_SYS_ADMIN]
...
#telechargement depuis le docker hub

On peut lister les plugins avec:

docker plugin ls
# creation du repertoire dur le serveur ssh
$ ssh USER@HOST mkdir /tmp/data

# creation du volume avec le driver vieux/sshfs
## ici le flag -d permet de specifier le driver soit vieux/sshfs
## puis les flgs -o pour les options dont le driver a besoin pour se connecter
## donc ce volume est dans un serveur distant
$ docker volume create -d vieux/sshfs -o sshcmd=USER@HOST:/tmp/data -o password=PASSWORD data

# Liste des volumes
## on verifie que le volume est bien monte
$ docker volume ls
vieux/sshfs:lasest data

# utilisation du volume dans un container
$ docker run -it -v data:/data alpine
touch /data/test

# verification sur le serveur ssh
$ ssh USER@HOST ls /tmp/data
test

Dans le Docker Hub on peut voir les plugin, il y a differentes categories tel que volume, authorisation login, network…


Docker Machine

Utililitaire pour provisioner des hotes docker.

Utilisation:

different types de drivers:

Utilisation de Docker Machine pour creer des hotes Docker en local ou sur un Cloud provider:

docker machine 1

Configuration du client local pour qu’il communique avec un daemon Docker distant:

docker machine 2

Possibilite de configurer le client docker de tel facon qu’il s’adresse au daemon distant et non au daemon local is suffit de faire eval $(docker machine env [NOM_MACHINE_DISTANTE]) puis a partir de ce moment la toute les commandes lance avec le client docker s’adresseront au daemon distant.

Installation:

les commandes docker-machine

$ docker-machine --help
Usage: docker-machine [OPTIONS] COMMAND [arg...]
Options:
...
Commands:
active            Print which machine is active
config            Print the connection config for machine
Create            Create a machine
env               Display the commands to set up the environment for Docker client
inspect           Inspect information about a machine
ip                Get the IP address of a machine
kill              Kill a machine
ls                List machine
regenerate-certs  Regenerate TLS Certificates for machine
restart           Restart a machine
rm                Remove a machine
ssh               Log into or run a command on a machine with SSH
scp               Copy files between machines
....

Creation

exemple: creation d’un hote sur VirtualBox

option par defaut:

$ docker-machine create --driver virtualbox node1

option additionnelles:

$ docker-machine create --driver virtualbox --virtualbox-memory=2048 --virtualbox-disk-size=5000 node3

exemple: creation d’un hote sur DigitalOcean

# un token d'identification doit etre specifie
## ici elle est dan la var env TOKEN
$ docker-machine create --driver digitalocean --digitalocean-access-tocken=$TOKEN node1

option additionnelles:

$ docker-machine create --driver digitalocean --digitalocean-access-token=$TOKEN --digitalocean-region=lon1 --digitalocean-size=1gb node2

exemple: creation d’un hote sur Amazon EC2

# des cles d'identification doivent etre specifiees
$ docker-machine create --driver amazonec2 --amazonec2-access-key=${ACCESS_KEY_ID} --amazonec2-secret-key=${SECRET_ACCESS_KEY} node1

option additionnelles:

$ docker-machine create --driver amazonec2 --amazonec2-access-key=${ACCESS_KEY_ID} --amazonec2-secret-key=${SECRET_ACCESS_KEY} --amazonec2-region=eu-west-1 --amazonec2-ami=ami-ed82e39e --amazonec2-instance-type=t2.large node2

Communication avec un hote distant

$ docker-machine env node1
export DOCKER_TLS_VERIFY="1"
export DOCKER_HOST="tcp://192.168.99.102:2376"
export DOCKER_CERT_PATH="/Users/luc/.docker/machine/machines/node1"
export DOCKER_MACHINE_NAME="node1"
$ eval $(docker-machine env node1)
$ env | grep DOCKER
DOCKER_HOST=tcp://192.168.99.102:2376
DOCKER_MACHINE_NAME=node1
DOCKER_TLS_VERIFY=1
DOCKER_CERT_PATH=/Users/luc/.docker/machine/mochines/node1
$ docker image ls
$ eval $(docker-machine env -u)
$ docker image ls

en gros par defaut le docker local communique avec le daemon local. si on lance docker-machine env node1 on voit le parametrage du node1 soit la VM lance dans virtualbox. si on fait eval $(docker-machine env node1) on a sette ces variables d’environnement. Si on fait un docker ps on se trouve dans un autre contexte soit le daemon de node1. pour unset les variables d’enironnement on fait eval $(docker-machine env -u).

puis une fois fini on peut faire un docker-machine rm node1 pour supprimer la vm de “l’hyperviser” virtualbox.

Exercice: Creation en local

PDF : creation local

Exercice: Creation sur DigitalOcean

PDF : creation sur digital ocean


Docker Compose

Outil tres utile pour gerer les applications multi-container.

Presentation

docker-compose.yml: structure

Exemple d’application web: docker-compose.yml:

version: '3.7'
volume:
  data:
networks:
  frontend:
  backend:
services:
  web:
    image: org/web:2.3
    networks:
      - frontend
    ports:
      - 80:80
  api:
    image: org/api:1.2
    networks:
      - backend
      - frontend
  db:
    image: mongo:4.0
    volumes:
      - data:/data/db
    networks:
      - backend

docker-compose.yml: definition d’un service

Ces instructions peuvent etre definies pour chaque service:

docker-compose.yml: developpement / production

Le binaire docker-compose

Les commandes docker-compose les plus utilisees

Service discovery

Extrait d’un fichier docker-compose.yml (en ROUGE db:):

version: '3'
services:
  db:
    image: mongo:3.4
    volumes:
      - data:/data/db
    restart: always
  api:
    image: org/api:1.2
    restart: always
  volumes:
    data:

Extrait d’un code Node.js: connexion a la base de donnees

// Mongodb connection string
url = 'mongodb://db/todos';  //ici le db en ROUGE

// Connection to database
MongoClient.connect(url, (err,conn) =>{
  if(err){
    return callback(err);
  }else{
    return callback(null,conn);
  }
});

en rouge: LE SERVICE API UTILISE LE SERVICE DE BASE DE DONNEES PAR SON NOM SOIT db

Voting App

Schema de l’application:

voting-app (Python)             result-app (Node.js)
      |                               |
      v                               v
redis (redis)                    db (PostgreSQL)
        \                          /
         \====> worker (.NET) <===/

fonctionnement:

Un utilisateur vote depuis voting-app, il choisit CAT ou DOG, le vote est stocke dans la bdd redis, le service worker va recuperer le vote depuis redis et va l’enregistrer dans la base de resultat db, puis les utilisateurs ont acces aux resultat fournit par result-app.

L’instruction build: dans le docker-compose.yml:

Permet de preciser ou se trouve le contexte de build cad le dockerfile et l’ensembles des fichiers necessaire pour construire l’image.

Au lieu d’executer l’app nodejs avec node server.js il faut plutot utiliser nodemon server.js ce dernier sera charge de lancer et superviser le process du service result-app de plus il peut detecter les fichiers qui changent et redemarrer automatiquement l’appli qui tourne dans le container pour prendre en compte ces changements. avec cette approche on peut faire des modifications de code depuis l’IDE sur la machine de dev et avoir l’appli qui se MAJ automatiquement dans le container.

en production on deploie les images des services et on monte pas le code applicatif dans les container.

Exercice: Voting App

PDF : voting app

pour preciser un fichier en particulier avec le flag -f:

$ docker-compose -f docker-compose-simple.yml up -d

augmenter le nombre de worker à 2:

$	docker-compose	scale	worker=2

Exercice: la stack Elastic

PDF : elastic

Voir l’exo, pas mal d’explication sur ELK.

ex: envoyer par http des log a eleastic:

while read -r line; do curl -s -XPUT -d "$line" http://localhost:8080 > /dev/null; done < ./nginx.log

Dcocker Swarm

Solution d’orchestration dev par docker.

Historique: le cluster Swarm avant Docker 1.12

Historique: le cluster Swarm a partir de Docker 1.12

Swarm mode

Le swarm mode utilise la notion de VIP soit Virtual IP, @IP virtuelle pour chaque service. Quand un service est appele par son nom c’est le DNS du daemon qui renvoie la VIP. Quand une requete arrive sur cette VIP, il y a un loadbalancing qui va rediriger le trafique vers l’un des containers du service en question.

Swarm mode: les primitives

vue d'ensemble Swarm mode

Les commandes sur l’image sont adresse aux nodes Managers. Les nodes Workers font tourner les applications

Swarm mode: les commandes de base

$ docker swarm --help

Usage: docker swarm COMMAND
Manage Swarm
Options:
    --help Print usage
Commands:
  init        initialize a swarm
  join        join a swarm as a node and/or manager
  join-token  manage join tokens
  leave       leave the swarm
  unlock      unlock swarm
  unlock-key  manage the unlock key
  update      update the swarm

Raft - Algo de consensus distribue

voir: http://thesecretlivesofdata.com/raft/

Node

Node: plusieurs etats possibles (availability)

Node: les commandes de base

Toutes les commandes docker executes sur un swarm doivent etre adresse a un node de type manager et pas un worker.

$ docker node --help

Usage:  docker node COMMAND

Manage Swarm nodes

Commands:
  demote      Demote one or more nodes from manager in the swarm
  inspect     Display detailed information on one or more nodes
  ls          List nodes in the swarm
  promote     Promote one or more nodes to manager in the swarm
  ps          List tasks running on one or more nodes, defaults to current node
  rm          Remove one or more nodes from the swarm
  update      Update a node

Node: initialisation du Swarm

# option obligatoire si plusieurs interfaces reseau --advertise-addr
## elle renvoie la commande qu'il faut executer pour ajouter d'autres machine en tant que membre du swarm qui sera de TYPE WORKER
docker@node1:~$ docker swarm init [--advertise-addr 192.168.99.100]
to add worker:
docker swarm joint --token SWMTKN-1-07990ineggmfdldkdkdosm 192.168.99.100:2377
# cmd pr ajouter un manager
to add manager: docker swarm joint-token manager

docker@node1:~$ docker node ls
...

Node: ajout d’un worker

# on ajoute node2 au swarm
docker@node2:~$ docker swarm join --token SWMTKN-1-07990ineggmfdldkdkdosm 192.168.99.100:2377
This node joined a swarm as a worker

docker@node1:~$ docker node ls
ID HOSTNAME ...
val name  ...

docker@node2:~$ docker node ls
Error response from daemon: This node is not a swarm manager...

Node: ajout d’un manager

# pour recuperer la commande a executer pour ajouter une machine au swarm aui sera de TYPE MANAGER

docker@node1:~$ docker swarm joint-token manager
...
docker swarm join --token SWMTKN-1-07990kfkffhgndmd1152 192.168.99.100:2377

docker@node3:~$ docker swarm join --token SWMTKN-1-07990kfkffhgndmd1152 192.168.99.100:2377
This node joined a swarm as a manager

Node: promotion / destitution

# destitution d'un node manager en workerd
docker@node1:~$ docker node demote node1
Manager node1 demoted in the swarm.

# promotion d'un node worker en manager
docker@node3:~$ docker node promote node2

# etat du swarm
docker@node2:~$ docker node ls
node 3 leader
node 1
node 2 rechable

Node: availability

# le node ne pourra plus recevoir de nouvelles taches
docker@node1:~$ docker node update --availability pause node2

# les taches du node2 seront schedulees sur d'autres node du cluster
docker@node1:~$ docker node update --availability drain node2

# le node2 repasse en mode actif et pourra recevoir de nouvelles taches
docker@node1:~$ docker node update --availability acive node2

Node: labels

# list des labels du node1
$ docker node inspect -f '' node1
{}

# ajout du label Memcached avec la valeur true
$ docker node update --label-add Memcached=true node1
node1

# Liste des labels du node1
$ docker node inspect -f '' node1 | jq .
{
  "Memcached": "true"
}

Exercice: Creation en local

Exercice: Creation sur DigitalOcean

Service (visible dans le docker-compose)

permet de lancer un container replique si besoin dans un cluster swarm

Service: configuration

Service: tasks

task

Service: les commandes de base

$ docker service --help

Usage: docker service COMMAND

Manage services

Options:
  --help Print usage

Commands:
  create  create a new service
  inspect display detailed information on one or more services
  ls      list services
  ps      list the tasks of a service
  rm      remove one or more services
  scale   scale one or multiple replicated services
  update update a service

Service: Exemple d’un serveur HTTP

# a faire sur un manager
$ docker service create --name www -p 8080:80 --replicas 3 nginx
ad855fgigp

$ docker service ls
ad855fgigp

$ docker service ps www
node1
node2
node3

$ docker service scale www=1
www scaled to 1

$ docker service ps www
"il ne reste plus que 1"

Service: Exemple ed base de donnees

$ docker service create --mount type=volume,src=data,dst=/data/db --name db --publish 27017:27017 mongo:4.0

$ docker service ls
ID NAME ...
id  db...

$ docker volume ls
DRIVER    VOLUME NAME
local     onfa1684asd13 #volune defini dans le dockerfile de mongo: VOLUME /data/configdb
local     data #volume cree par l'instruction mount

Exercice: Creation d’un service

Service: rolling upgrade

creation d’un service en specifiant les parametres de MAJ, ici les taches seront MAJ 2 par 2 toutes les 10 secondes:

# creation du service de 4 taches
$ docker service create --update-parallelism 2 --update-delay 10s --publish 8080:80 --replicas 4 --name vote instavote/vote

# les taches sont MAJ 2 par 2 ttes les 10sec
$ docker service update --image instavote/vote:indent vote

# supprimer le service
$ doceer service rm vote

Service: Rollback

$ docker service create --publish 8080:80 --name vote instavote/vote

# la cle SPEC affiche par inspect contient la conf du service
$ docker service inspect vote

# si on met le service a jour cela rajoute une nouvelle cle PREVIOUSSPEC
$ docker service update --image instavote/vote:indent vote

# on peut voir PreviousSpec via
$ docker service inspect vote

# pour faire un rollback
$ docker service rollback vote

Exercice: Rollong update & Rollback

voir pdf, pas mal d’explications.

Secret

# creation d'un secret
node1 $ echo "akdond56" | docker secret create password -
fgfgd1g68g7rg4g687

# creation d'un node avec le secret
node1 $ docker create --name=api --secret=password username/api
sekkf0ff55f

node1 $ docker service ps api
node2

# voir le secret dans node2 dans /run/secrets/password
node2 $ docker exec -ti $(docker ps --filter name=api -q) sh
akdond56

# si on maj le service et qu'on lui enleve le secret, il est plus dispo
node1 $ docker service update --secret rm="password" api

node2 $ docker exec -ti$(docker ps --filter name=api -q) sh
# cat /run/secrets/password
no such file or directory

Config

en gros on injecte un fichier nginx.conf dans le service via un objet CONFIGS(qui contient la source server_config), ca permet de gerer la config a l’exterieur de l’application au lieu de maintenir une image:

version: '3.3'
services:
  proxy:
    image: nginx:1.14
    configs: #ICI
      -source: server_config
      target: /etc/nginx/nginx.conf
      mode: 0444
      uid: '33'
      gid: '33'
      ...

Exercice: Utilisation des secrets et configs

Stack

$ docker stack --help

deploy  deploy a new stack or update an existing stack
ls      list stacks
ps      list the tasks in the stack
rm      remove the stack
services  list the services in the stack

Stack: exemple avec la Voting App

$ git clone https://github.com/docker/example-voting-app/ && cd example-voting-app

$ docker stack deploy -c docker-stack.yml vote
creating network vote_backend
creating network vote_default
creating network vote_frontend
creating service vote_visualizer
creating service vote_redis
creating service vote_db
creating service vote_vote
creating service vote_result

$ docker stack ls
NAME    SERVICES
vote    6

$ docker service ls
ID          NAME        MODE    REPLICAS    IMAGE
asdadofof   vote_vote...
(liste de tt les services)

Exercice: Deploiement de la stack TICK

Stack: swarmprom

Solution open source pour le monitoring d’un swarm (https://github.com/stefanprodan/swarmprom)

la cle deploy n’est pas prise en compte par le binaire docker-compose. cette cle permet de definir les option de deploiement du service sur un swarm.

Routing Mesh

# le service app est sur 2 replicas, le port 80 est publie sur le port 8000
# le routing mesh expose le port 8000 sur chaque node du cluster swarm
# le trafic destine a l'appli peut arriver sur n'importe quel node
$ docker service create --name app --replicas 2 --network appnet -p 8000:80 nginx

Ici on voit que le trafic arrive sur le host-C qui n’a pas de service, la requete est redirige vers un replicat du service qui tourne sur un autre node:

Routing mesh

Routing mesh details

Interface web de gestion: Portainer

# recuperation du fichier Compose definissant l'application
$ curl -L https://portainer.io/download/portainer-agent-stack.yml -o portainer-agent-stack.yml

# lancement de l'application en tant que Stack
$ docker stack deploy -c portainer-agent-stack.yml portainer

Interface web de gestion: Swarmpit

Contrairement a Portainer, Swarmpit est essentielement dedie a Swarm et propose beaucoup moins de fonctionnalites.


Les logs de l’algorithme RAFT

RAFT logs

Dans cette mise en pratique, nous allons démistifier les logs qui sont utilisés par l’algorithme Raft afin de gérer l’état du cluster.

Ces logs sont crées sur le Leader des managers et répliqués sur chaque manager.

Swarm architecture

Swarm architecture

Creation d’un swarm

Une solution simple est d’utiliser Docker Machine pour créer 2 hôtes Docker. Nous initialiserons le swarm sur l’un des 2 hôtes et ajouterons le second.

Dans cet exemple, j’utilise le driver virtualbox afin de créer les machines virtuelles en local.

Creation des VMs

$ docker-machine create --driver virtualbox node01
$ docker-machine create --driver virtualbox node02

Vous pouvez utiliser un autre driver si vous le souhaitez afin de créer les VMs sur un autre hyperviseur (Hyper-V) ou sur un cloud provider (AWS, DigitalOcean, Microsoft Azure, …)

Sur ma machine de développement, les VMs sont créées avec les IPs suivantes, les votres pourront être différentes.

$ docker-machine ls
NAME     ACTIVE   DRIVER       STATE     URL                         SWARM   DOCKER        ERRORS
node01   -        virtualbox   Running   tcp://192.168.99.100:2376           v17.10.0-ce
node02   -        virtualbox   Running   tcp://192.168.99.101:2376           v17.10.0-ce

Initialisation du swarm

Lorsque nous sommes sur un node qui n’est pas en mode Swarm, le répertoire swarm du dossier d’installation de Docker est vide.

$ ls /var/lib/docker/swarm

Depuis node01, lancez la commande suivante en utilisant l’adresse que vous avez obtenue pour ce node.

```bash$ docker swarm init –advertise-addr 192.168.99.100 Swarm initialized: current node (311ep1n2wcgrje263l45sjrix) is now a manager.

To add a worker to this swarm, run the following command:

docker swarm join --token SWMTKN-1-081l3uznies41wgvgtjnnyc1c2tczcdnopqiyoo1z249xq043b-3haunf75clx1dfabv5gvgt9fv 192.168.99.100:2377

To add a manager to this swarm, run ‘docker swarm join-token manager’ and follow the instructions.


Depuis node02 lancez la commande de join telle qu'elle est précisée par la commande précédente.

```bash
$ docker swarm join --token SWMTKN-1-081l3uznies41wgvgtjnnyc1c2tczcdnopqiyoo1z249xq043b-3haunf75clx1dfabv5gvgt9fv 192.168.99.100:2377
This node joined a swarm as a worker.

Vous pouvez alors lister les nodes de votre swarm avec la commande suivante:

$ docker node ls
ID                            HOSTNAME            STATUS              AVAILABILITY        MANAGER STATUS
311ep1n2wcgrje263l45sjrix *   node01              Ready               Active              Leader
t0u519i3rusd7442d8nfypf4e     node02              Ready               Active

A partir du moment ou le daemon Docker est en mode Swarm (suite à la commande d’initialisation précédente), plusieurs éléments sont présents dans le répertoire swarm.

$ find /var/lib/docker/swarm
/var/lib/docker/swarm
/var/lib/docker/swarm/docker-state.json
/var/lib/docker/swarm/certificates
/var/lib/docker/swarm/certificates/swarm-root-ca.crt
/var/lib/docker/swarm/certificates/swarm-node.key
/var/lib/docker/swarm/certificates/swarm-node.crt
/var/lib/docker/swarm/worker
/var/lib/docker/swarm/worker/tasks.db
/var/lib/docker/swarm/state.json
/var/lib/docker/swarm/raft
/var/lib/docker/swarm/raft/snap-v3-encrypted
/var/lib/docker/swarm/raft/wal-v3-encrypted
/var/lib/docker/swarm/raft/wal-v3-encrypted/0.tmp
/var/lib/docker/swarm/raft/wal-v3-encrypted/0000000000000000-0000000000000000.wal

Les logs sont situés dans le répertoire raft, les clés d’encryption dans le répertoire certificates.

Creation d’un secret

La commande suivante permet de créer un secret nommé passwd et contenant une chaine de caractère.

$ echo 'A2e5bc21' | docker secret create passwd -

Si nous listons les secrets existants sur notre swarm, seul le secret précédemment créé apparait, son contenu n’est plus visible.

$ docker secret ls
ID                          NAME     CREATED        UPDATED
4mbzd3pt9jk9z2lqehm7e77bb   passwd   8 seconds ago  8 seconds ago

Nous verrons par la suite que ce secret est en clair dans les logs de Raft.

A propos des logs de Raft

La version 1.13 de la plateforme Docker a introduit la gestion des secrets dans le contexte d’un swarm. Ce sont des information sensibles, par exemple des identifiants de connexion à des services tiers. Les secrets sont stockées en clair dans les logs utilisés par l’implémentation de l’algorithme Raft et c’est notamment pour cette raison que logs sont cryptés, afin d’assurer la confidentialité de ces informations.

Les secrets sont généralement créés par les Ops lors du lancement de l’application puis fournis au service qui en ont besoin. Ils seront alors accessibles, dans les containers du service, depuis un système de fichiers temporaire sous /run/secrets/NOM_DU_SECRET.

Decryptage des logs

Nous allons utiliser ici l’utilitaire swarm-rafttool, un binaire qui se trouve dans la librairie SwarmKit utilisée par Docker pour la gestion des clusters swarm.

Swarm Rafttool

Afin d’éviter l’installation de cet utilitaire, nous le lançons directement depuis un container.

$ docker run --rm -ti -v /var/lib/docker/swarm/:/var/lib/docker/swarm:ro -v $(pwd):/tmp/ zaggash/docker-rafttool

Nous pouvons alors voir les différentes options possibles:

/ # swarm-rafttool
Tool to translate and decrypt the raft logs of a swarm manager

Usage:
 /root/go/bin/swarm-rafttool [command]

Available Commands:
  decrypt Decrypt a swarm manager’s raft logs to an optional directory
  dump-wal Display entries from the Raft log
  dump-snapshot Display entries from the latest Raft snapshot
  dump-object Display an object from the Raft snapshot/WAL

Flags:
  -h, --help help for /root/go/bin/swarm-rafttool
  -d, --state-dir string State directory (default “/var/lib/swarmd”)
  --unlock-key string Unlock key, if raft logs are encrypted

Use "/root/go/bin/swarm-rafttool [command] — help" for more information about a command.

Dans la suite, nous utiliserons la command dump-wal afin de décrypter et visualiser les entrées du fichier de logs.

Decryptage

Afin de décrypter le fichier de log, nous créons un script shell qui va tout d’abord copier le fichier puis lancer le binaire swarm-rafttool sur cette copie. L’étape de copie est nécessaire car swarm-rafttool ne permet pas de décrypter les logs en cours d’usage.

Avec un petit coup de vi, copiez dans un fichier dump.sh (dans le container lancé précédemment) le contenu suivant:

d=$(date "+%Y%m%dT%H%M%S")
SWARM_DIR=/var/lib/docker/swarm
WORK_DIR=/tmp
DUMP_FILE=$WORK_DIR/dump-$d
STATE_DIR=$WORK_DIR/swarm-$d
cp -r $SWARM_DIR $STATE_DIR
$GOPATH/bin/swarm-rafttool dump-wal --state-dir $STATE_DIR > $DUMP_FILE
echo $DUMP_FILE

Nous pouvons alors lancer ce script et observer le contenu des logs.

/ # chmod +x ./dump.sh
/ # ./dump.sh | xargs cat

La sortie est relativement verbeuse, et peux être décomposée en plusieurs Entry. Je vous invite à examiner les premières qui sont relative à la mise en place du Swarm..

La dernière Entry (ci-dessous) concerne la création du secret passwd.

Entry Index=12, Term=2, Type=EntryNormal:
id: 102286946670602
action: <
  action: STORE_ACTION_CREATE
  secret: <
    id: "4mbzd3pt9jk9z2lqehm7e77bb"
    meta: <
      version: <
        index: 11
      >
      created_at: <
        seconds: 1499070018
        nanos: 989531240
      >
      updated_at: <
        seconds: 1499070018
        nanos: 989531240
      >
    >
    spec: <
      annotations: <
        name: "passwd"
      >
      data: "A2e5bc21\n"
    >
  >

Comme nous pouvons le voir, le contenu du secret est en clair dans le log. L’encryption des logs est donc obligatoire pour préserver la sécurité de ces informations sensibles.

Sortons du container

/ # exit

Autolock

Si un manager est compromis, les logs cryptés et les clés d’encryption sont récupérables. Il est alors facile pour un hacker de décrypter les logs et d’avoir ainsi accès aux données sensibles, comme nous venons de le faire. Pour empêcher cela, un Swarm peut être locké. Une clé d’encryption est alors générée et utilisée pour encrypter les clés publique / privée (celles servant à encrypter / décrypter les logs).

Cette nouvelle clé, appelée Unlock key, doit être sauvegardée offline et fournie manuellement au daemon Docker après un restart.

Nous visualisons le contenu des clés situées dans le sous répertoire certificates.

$ cat /var/lib/docker/swarm/certificates/swarm-node.crt
$ cat /var/lib/docker/swarm/certificates/swarm-node.key

La commande suivante met à jour le Swarm et active la fonctionnalité d’Autolock.

Note: il est également possible d’activer l’Autolock lors de la création du Swarm.

$ docker swarm update --autolock=true
Swarm updated.

To unlock a swarm manager after it restarts, run the `docker swarm unlock`
command and provide the following key:

    SWMKEY-1-y4plj3mAYXoS4OiHHU9TC23vjKM6dgmcjdFju/2YTX0

Please remember to store this key in a password manager, since without it you will not be able to restart the manager.

Si nous observons une nouvelle fois le contenu des clés, nous pouvons voir qu’elles ont été encryptées.

$ cat /var/lib/docker/swarm/certificates/swarm-node.crt
$ cat /var/lib/docker/swarm/certificates/swarm-node.key

Redemarage du daemon

Reperez le PID du processus dockerd et envoyez lui un signal KILL.

$ ps aux | grep dockerd
root      1232  0.4  6.8 442076 70220 ?        Sl   07:35   0:13 /usr/local/bin/dockerd -D -g /var/lib/docker -H unix:// -H tcp://0.0.0.0:2376 --label provider=virtualbox --tlsverify --tlscacert=/var/lib/boot2docker/ca.pem --tlscert=/var/lib/boot2docker/server.pem --tlskey=/var/lib/boot2docker/server-key.pem -s aufs

$ kill 1232

le PID du daemon Docker tournant sur votre VM sera différent de celui ci-dessus

Afin de conserver les mêmes options que précédemment, redémarrez le daemon avec la commande listée lors du ps. Nous la lancerons en tâche de fond en l’encadrant avec nohup … & comme dans l’exemple ci-dessous.

$ nohup /usr/local/bin/dockerd -D -g /var/lib/docker -H unix:// -H tcp://0.0.0.0:2376 --label provider=virtualbox --tlsverify --tlscacert=/var/lib/boot2docker/ca.pem --tlscert=/var/lib/boot2docker/server.pem --tlskey=/var/lib/boot2docker/server-key.pem -s aufs &

Il n’est alors pas possible de lancer de commande sur le swarm tant que le manager n’a pas été unlocké.

$ docker node ls
Error response from daemon: Swarm is encrypted and needs to be unlocked bef
ore it can be used. Please use "docker swarm unlock" to unlock it.

Nous délockons alors le manager en lui précisant le token récupéré lors de l’opétation de lock.

$ docker swarm unlock
Please enter unlock key:
$ docker node ls
ID                            HOSTNAME  STATUS  AVAILABILITY        MANAGER STATUS
311ep1n2wcgrje263l45sjrix *   node01              Ready               Active              Leader
t0u519i3rusd7442d8nfypf4e     node02              Ready               Active

Conclusion

Cet exercice donne un rapide aperçu des logs générés par l’algorithme de consensus Raft. Nous l’avons illustré en nous basant sur un Swarm simplement composé d’un seul manager. Vous trouverez des détails supplémentaires dans cet article: https://medium.com/lucjuggery/raft-logs-on-swarm-mode-1351eff1e690


Backup d’un Swarm

Comme nous l’avons vu, les nodes de type manager sont les garants de l’état du swarm. Chaque action effectuée dans le swarm (création d’un service, mise à jour d’un service, création d’un network, ….) est consignée dans un fichier de log sur le leader des managers. Ce fichier étant répliqué en temps réel sur les autres managers.

Sur chaque manager, ces logs, les clés servant à les chiffrer et d’autres fichiers relatifs à l’état du swarm sont stockés dans le répertoire /var/lib/docker/swarm. Ce répertoire est créé lors de l’initialisation du swarm (docker swarm init).

Backup

Afin de réaliser le backup d’un swarm, il suffit de réaliser les opérations suivantes:

Restauration du backup

Afin de réaliser la restauration d’un backup, les opérations suivantes sont nécessaires:


Tolerances aux pannes

PDF : Tolerance aux pannes


Network

Container Network Model

Container network model

Network CLI

docker network -h
Flag shorthand -h has been deprecated, please use --help

Usage:  docker network COMMAND

Manage networks

Commands:
  connect     Connect a container to a network
  create      Create a network
  disconnect  Disconnect a container from a network
  inspect     Display detailed information on one or more networks
  ls          List networks
  prune       Remove all unused networks
  rm          Remove one or more networks

Run 'docker network COMMAND --help' for more information on a command.

Les differents drivers

$ docker network create --driver DRIVER [OPTION] NAME

Les networks d’un hote Docker

$ docker network ls
bidge
host
none...

Les networks d’un hote Docker: bridge

node1 $ ip a show docker0
@IP/mask

Network CLI bridge

# the other end of the veth pair is attached to the docker0 bridge network
$ brctl show

# creation of an alpine container
$ docker container run -ti alpine:3.8 sh

$ the other end of the vert pair is attached to the docker0 bridge network
$ brctl show
(ajout de `vetha5412f12` dans la colone interfaces)

Les containers attaches au bridge0 ne peuvent pas communiquer entre eux via leur nom:

# recuperation du network id du bridge
$ docker network ls

$ docker run -d --name+nginx nginx:1.14

$ docker inspect -f '' 0eee
172.17.0.2

$ docker network inspect [ID du bridge]
(json contenant les ip de alpine et nginx)

Les networks d’un hote Docker: host

$ docker container run -ti --network=host alpine sh

Les networks d’un hote Docker: none

$ docker container run -ti --network=none alpine sh

Les networks d’un hote Docker: user defined bridge

user defined bridge

Les containers attaches au nouveau bridge peuvent communiquer entre eux via leur nom:

exemple user definded bridge

Les networks d’un hote Docker: Macvlan

macvlan

Exercice: Networks sur un hote

Les networks dans un Swarm

Quand on fait un docker swarm init nous avons en plus de bridge, host et none:

Les networks dans un Swarm: overlay

overlay

node1 $ docker network create --driver overlay ovnet

node1 $ docker network create --driver overlay ovnet alpine sleep 1000

Exercice: Networks dans un Swarm

Networks dans un Swarm


Securite

------Namespaces-------
Mount   (permet de tourner sur son propre FS)
PID     (permet a un container d'avoir son propre arbre de process isole du systeme)
Net     (fournit un stack reseaux independant de la machine hote)
IPC     (permet d'avoir un espace de memoire partage  utilise pour la communication inter process dans le container)
UTS     (permet d'avoir son propre hostname)
User    (permet de mapper un user non root de la machine hote avec un user root dans le container)

------Control Groups-------
RAM
CPU
I/O

------Securite-------
SELinux
AppArmor
Seccomp
Capabilites

Hardening

Le but du hardening est de mettre en place un maximum d’elements de securite pour minimisier le plus possible la surface d’un systeme.

Exemples de menaces

Hardening: Center for Internet Security benchmark

liste les best practices de securite du CIS voir la doc:

PDF : CIS Docker Benchmark

Hardening: Security Bench

voici la commande permettant d’executer la batterie de test Security Bench, ln voit le resultat des test point par point en fonction de chaque chapitre du document:

# --net host permet de se ratacher a la stack reseaux de la machine hote il aura donc acces a l'ensemble des interfaces reseaux
# --pid host permet de se ratacher au namespace pid de la machine hote et donc avoir acces a l'ensemble des processus sur le systeme dont le docker daemon
# --cap-add audit_control qui va ajouter la capabilie qui permet l'audit du kernel
$ docker run -it --net host --pid host --cap-add audit_control -e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST -v /var/lib:/var/lib -v /var/run/docker.sock:/var/run/docker.sock -v /etc:/etc --label docker_bench_security docker/docker-bench-security

...
[INFO] 1 - Host Configuration
[WARN] 1.1 - Ensure a separate partition for containers has been created
[NOTE] 1.2 - Ensure the container host has been Hardened
...

Linux Capabilities

C’est des primitifs present dans le kernel linux et elles permettent de decouper les droits roots en plusieurs categories.

Exemple: Capabilities

La capabilite SYS_ADMIN est necessaire pour modifier le hostname (appel a la fonction sethostname), on utilise le flag --cap-add pour ajouter une capability:

$ docker run -ti alpine sh
hostname foo
hostname  sethostname: Operation not permitted

$ docker run -ti --cap-add=SYS_ADMIN alpine sh
hostname foo
hostname
foo

La capabilite NET_RAW est necessaire pour utiliser la commande ping, on utilise le flag --cap-drop pour supprimer une capability:

$ docker run alpine ping 8.8.8.8
PING 8.8.8.8 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: seq=0 ttl=37 time=0.394 ms
...

$ docker run --cap-drop=NET_RAW alpine ping 8.8.8.8
ping: permission denied (are you root?)

Linux Security Modules

    |
    v
__________
-rw-r--r--   1  root  root  3106  Sept 23 2019  .bashrc

Linux Security Modules: AppArmor

AppArmor est utilise par defaut par docker, si on souhaite donner plus de privilege a un container, il suffit de desactiver le profile AppArmor avec cette option --security-opt soit --security-opt apparmor:unconfinded:

$ docker run -ti --security-opt apparmor:unconfined alpine sh

Linux Security Modules: SELinux

ici on peut voir les attributs etendus avec ls -Z, ci-dessous on a par exemple docker_exec_t. Quand on essaie d’acceder a index.html nous avons une erreur car ce fichier n’a pas le meme contexte de securite. en desactivant SELinux avec l’option --security-opt label:disable nous pouvons acceder au fichier index.html:

selinux

PDF : explication fonctionnement SELinux

Seccomp (Secure Computing Mode)

Exemple, ici nous allons interdire la commande mkdir:

# l'appel a mkdir est autorise avec le profil par defaut
$ docker container run -it alpine sh
mkdir test
exit

# fichier json definissant un profil custom qui interdit l'appel a mkdir
$ cat policy.json
{
  "defaultAction": "SCMP_ACT_ALLOW", #ici on autorise tout a la base
  "syscalls": [
      {
          "name": "mkdir",
          "action": "SCMP_ACT_ERRNO" #puis on interdit mkdir
      }
  ]
}

# un container utilisant ce profil n'est pas autorise a faire un appel a mkdir
$ docker run -it --security-opt seccomp:policy.json alpine sh
mkdir test
mkdir: can't create directory 'test': Operation not permitted

Scan de vulnerabilites

Content Trust

# build et push d'une image avec le tag 1.0
$ docker image build -t username/app:1.0 .
$ docker image push username/app:1.0

# activation de docker content trust
$ export DOCKER_CONTENT_TRUST=1

# build et push d'une image avec le tag 2.0
# creation de 2 cles (root key, taging key) qui sont utilisees pour signer le tag de l'image
$ docker image build -t username/app:2.0 .
$ docker image push username/app:2.0
...
# activation de Docker Content Trust sur une autre machine
$ export DOCKER_CONTENT_TRUST=1

# download de la version 1.0 de l'image (tag non signe)
$ docker image pull username/app:1.0
No trust data for 1.0

# download de la version 2.0 de l'image (tag signe)
$ docker image pull username/app:2.0
...

De nombreuses solutions commerciales