Docker
Les notes de cettes pages sont prises à partir de la documentation de docker : https://docs.docker.com/get-started/
Sur Ubuntu 18.10 et dans mon cas, il faut pas utiliser le snap de docker mais : sudo apt install docker
Puis aller sur https://github.com/docker/machine/releases/, télécharger docker-machine, puis:
- faire un chmod +x sur le fichier
- Le copier dans /snap/bin :
sudo cp docker-machine-Linux-x86_64 /snap/bin/docker-machine
Présentation de Docker
Docker est une plateforme qui permet de développer, déployer et faire tourner des applications en conteneurs. L'usage de conteneur s'appelle la conteneurisation. Les conteneurs sont:
- Flexibles : Tout peut être conteneurisé
- Légers
- Interchangeables
- Portables
- Scalables
- Empilables
Un conteneur est lancé en faisant tourner une image : une image est un exécutable qui inclut tout ce dont a besoin le programme, et le conteneur est l'environnement d'exécution qui permet à cette image de fonctionner. Une fois lancé le conteneur est un processus utilisateur, que l'on peut lister avec docker ps. Les conteneurs partagent entre eux les ressources système et le noyau Linux.
À la différence d'une VM, un conteneur peut tourner nativement sur n'importe quelle machine Linux; une même machine Linux peut utiliser plusieurs conteneurs, cette technologie est plus économe que les VM.
Préparer l'environnement Docker
On va commencer par installer Docker.
https://docs.docker.com/install/
Le versioning fonctionne sur le modèle : Année.Mois.Patch
Une fois installé, on peut tester avec docker --version
.
On a plus d'informations avec docker info:
justine@Justine-pc:~$ docker info
Containers: 0
Running: 0
Paused: 0
Stopped: 0
Images: 1
Server Version: 18.09.0
Storage Driver: overlay2
Backing Filesystem: extfs
Supports d_type: true
Native Overlay Diff: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins:
Volume: local
Network: bridge host macvlan null overlay
Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog
Swarm: inactive
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: d6de12e2f362cb9dc49ad957911996d3de59b338
runc version: 4fc53a81fb7c994640722ac585fa9ca548971871
init version: fec3683
Security Options:
apparmor
seccomp
Profile: default
Kernel Version: 4.18.0-10-generic
Operating System: Ubuntu 18.10
OSType: linux
Architecture: x86_64
CPUs: 8
Total Memory: 15.59GiB
Name: ***
ID: EN3D:SYGK:2WYA:TZM5:MHRP:FBOH:GQVD:VFKW:SDG6:V3FW:XSKK:5MAI
Docker Root Dir: /var/lib/docker
Debug Mode (client): false
Debug Mode (server): false
Registry: https://index.docker.io/v1/
Labels:
Experimental: false
Insecure Registries:
127.0.0.0/8
Live Restore Enabled: false
Product License: Community Engine
WARNING: No swap limit support
On peut ensuite ajouter son utilisateur au groupe docker:
sudo adduser user docker
Tester l'installation de Docker
Le test le plus simple consiste à faire tourner l'image hello-world :
justine@Justine-pc:~$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
d1725b59e92d: Pull complete
Digest: sha256:0add3ace90ecb4adbf7777e9aacf18357296e799f81cabc9fde470971e499788
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
To generate this message, Docker took the following steps:
1. The Docker client contacted the Docker daemon.
2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
(amd64)
3. The Docker daemon created a new container from that image which runs the
executable that produces the output you are currently reading.
4. The Docker daemon streamed that output to the Docker client, which sent it
to your terminal.
To try something more ambitious, you can run an Ubuntu container with:
$ docker run -it ubuntu bash
Share images, automate workflows, and more with a free Docker ID:
https://hub.docker.com/
For more examples and ideas, visit:
https://docs.docker.com/get-started/
Hello-world est une image simple qui permet de valider l'installation.
On peut lister les images télechargées avec docker image ls
...et lister les conteneurs avec docker container ls --all
Récapitulatif des commandes issu de la documentation Docker :
List Docker CLI commands
docker
docker container --help
Display Docker version and info
docker --version
docker version
docker info
Execute Docker image
docker run hello-world
List Docker images
docker image ls
List Docker containers (running, all, all in quiet mode)
docker container ls
docker container ls --all
docker container ls -aq
Construire une app
Nous allons commencer en bas de la hiérarchie de Docker pour remonter. La hiérarchie Docker est constituée comme suit :
-Stack : Définit les interactions entre les services
-Services : Définit comment les containers se comportent en production
-Conteneur : On y est.
Classiquement, lorsque l'on construit une application en python par exemple, on commence par télécharger un environnement Python pour ensuite coder par dessus. Avec Docker, on peut se contenter de récupérer une image contenant une instance de Python sans installation. Ensuite notre build contiendra l'image de Python avec toutes ses dépendances, ainsi que notre application; tout sera "packagé" ensemble, et on sera sûrs que tout fonctionne. Cela évite d'avoir à prendre en compte des différences entre l'environnement de développement et celui de production.
Ces images portables sont définies par un fichier de configuration appellé Dockerfile.
Définir un container avec Dockerfile
Le Dockerfile définit tout ce qui va aller dans l'environnement à l'intérieur du conteneur; les ressources comme les interfaces réseau sont virtualisées dans cet environnement isolé, alors il va falloir mapper les ports vers le monde extérieur, et être spécifique quant aux fichiers que l'on veut copier dans l'environnement. Après ça, le conteneur devrait fonctionner partout.
On va commencer avec Python. Il faut créer un nouveau dossier, s'y rendre, créer un fichier appellé "Dockerfile" et y coller ce texte :
- Use an official Python runtime as a parent image
FROM python:2.7-slim
- Set the working directory to /app
WORKDIR /app
- Copy the current directory contents into the container at /app
COPY . /app
- Install any needed packages specified in requirements.txt
RUN pip install --trusted-host pypi.python.org -r requirements.txt
- Make port 80 available to the world outside this container
EXPOSE 80
- Define environment variable
ENV NAME World
- Run app.py when the container launches
CMD ["python", "app.py"]
Ce fichier fait référence à deux fichiers encore inexistants, app.py et requirements.txt. On va les créer.
L'application proprement dite
Dans le dossier lui-même, créer les deux fichiers.
Notre app sera alors complète: puisque que le Dockerfile contient la commande COPY, app.py et requirements.txt seront pris en compte à la création de l'image. La sortie de l'application sera dispo via http grâce au EXPOSE, qui permet d'accéder à la sortie depuis le "monde extérieur" sur le port 80.
requirements.txt:
Redis
app.py:
from flask import Flask
from redis import Redis, RedisError
import os
import socket
Connect to Redis
redis = Redis(host="redis", db=0, socket_connect_timeout=2, socket_timeout=2)
app = Flask(__name__)
@app.route("/")
def hello():
try:
visits = redis.incr("counter")
except RedisError:
visits = "cannot connect to Redis, counter disabled" html = "
Hello {name}!
" \
"Hostname: {hostname}
" \
"Visits: {visits}"
return html.format(name=os.getenv("NAME", "world"), hostname=socket.gethostname(), visits=visits)if __name__ == "__main__":
app.run(host='0.0.0.0', port=80)
Ici le pip install -r requirements.txt
installe les bibliothèques Flask et Redis pour Python. L'application affiche aussi la variable d'environnement NAME et la sortie d'un appel à socket.gethostname(). Ici, Redis ne devrait pas fonctionner puisqu'on a fait qu'installer sa bibliothèque, et pas Redis lui-même.
NB : Redis est un système de BDD "anti-SQL".
NB2 : demander le hostname depuis un conteneur renvoie le container ID, qui est comme le PID d'un exécutable.
Construire l'application
On est prêts à construire notre application ! Il faut s'assurer que l'on est dans notre dossier et qu'il contient bien nos trois fichiers (Dockerfile, requirements.txt et app.py). On va lancer la construction :
L'argument -t permet de taguer mon image pour qu'elle ait un nom reconnaissable (ici "montest"); ne pas oublier le . qui renvoie au dossier actuel.
Docker va alors exécuter le Dockerfile; installer requirements.txt dans l'image en partant de l'image Python, etc...
Une fois le processus terminé, mon dossier n'as pas changé; mais l'image est bien apparu dans ma liste d'images, ce que je peux voir avec un docker image ls.
Problèmes de proxy/dns:
Un Proxy peut être indiqué dans le dockerfile avec:
- Set proxy server, replace host:port with values for your servers
ENV http_proxy host:port
ENV https_proxy host:port
Un DNS peut être indiqué à Docker en modifiant/créant le fichier /etc/docker/daemon.json et en y mettant:
{
"dns": ["your_dns_address", "8.8.8.8"]
}
Il faut ensuite sauvegarder le fichier et redémarrer le service docker.
Tester l'application
Notre image est créée, il est temps de créer ce conteneur et de tester !
il suffit d'un :
L'argument -p 4000:80 mappe le port 4000 de ma machine au port 80 dans le conteneur : j'ai alors accès à mon application en visitant l'url suivante : 127.0.0.1:4000 .
J'y vois la sortie de mon application. Ca marche aussi avec un curl http://localhost:4000
On peut aussi lancer l'application en arrière-plan (detached) :
J'ai alors l'identifiant de conteneur et je suis renvoyée au terminal : le conteneur est en arrière-plan. Je le vois avec docker container ls : je vois que le container ID (en abrégé) correspond à ce que renvoie l'app.
Je peux stopper le conteneur avec :
Partager l'image
On peut uploader cette image et la faire tourner ailleurs.
En effet il va être nécessaire de savoir comment push des registres lors de la mise en prod. Un registre est une collection de dépôts, et un dépôt est une collection d'images. Un peu comme Github, mais le code est déjà construit. On peut créer plusieurs dépôts sur un registre avec un seul compte; la ligne de commande docker utilise le registre public de docker par défaut. Il en existe d'autres.
On peut même créer son propre registre !
Se logger
Il va falloir se logger avec son compte docker (qu'on peut créer sur hub.docker.com) et noter son identifiant.
Une fois que c'est fait, je peux me connecter sur le site et créer un repo.
Je peux ensuite logger via le terminal avec docker login
.
Tagger l'image
La notation pour associer une image a un dépôt est nomutilisateur/repo:tag.
Le tag est optionnel mais recommandé, car c'est c'est le mécanisme utilisé par DOcker pour donner des numéros de versions. Il faut donc que les noms aient du sens.
Je vais donc faire ici :
docker tag montest justinep/monrepotest:letest #montest est l'image, justinep mon identifiant Docker, letest est le tag que je donne.
Un docker image ls
me permet de voir mon image tagguée.
Publier l'image
Je peux uploader l'image avec :
Récupérer l'image depuis le dépôt et la faire tourner (Pull and Run)
Désormais, je peux faire un :
pour faire tourner l'image : si elle n'est pas sur ma machine locale, elle est téléchargée et lancée.
Peu importe où je suis, Docker récupère l'image avec Python et tout le reste, et fait tourner le code; tout ça dans un paquet bien ficelé, sans qu'il n'y ai rien de plus à installer.
Les Services
Nous allons scaler nos applications et active le load balancing. Pour cela, On va monter d'un niveau dans la hiérarchie pour voir les services.
Dans une application distribuée, les différentes parties de l'application sont appellées "services". Par exemple, si l'on prend un site web de streaming vidéo, l'un des services servirait à à stocker les données dans une BDD; un autre à encoder les vidéos; etc.
Les services ne sont finalement que des conteneurs en prod. Un service fait tourner une image, mais il codify la façon dont elle tourne : quels ports utiliser, combien de répliques du conteneur, etc. Le scaling d'un service change le nombre d'instances d'un conteneur seront lancées pour un même logiciel, afin d'assigner plus de ressources matérielles au processus. Les services peuvent être définis, lancés et scalés grâce au fichier docker-compose.yaml.
Notre premier docker-compose.yml
Ce fichier définit la façon dont les conteneurs doivent se comporter en production.
Une foit que l'on s'est assuré que notre image a bien été push, on peut créer le fichier n'importe où et y coller le contenu suivant :
version: "3"
services:
web:
# replace username/repo:tag with your name and image details
image: username/repo:tag
deploy:
replicas: 5
resources:
limits:
cpus: "0.1"
memory: 50M
restart_policy:
condition: on-failure
ports:
- "4000:80"
networks:
- webnet
networks:
webnet:
Ce fichier accomplit les choses suivantes :
- Faire un pull de l'image
- Lancer 5 instances de l'image en tant qu'un service appellé "web", chacun pouvant utiliser maximum 10% du CPU (distribué sur tous les coeurs) et 50 Mo de RAM
- Relancer immédiatement un conteneur qui plante
- Mapper le port 4000 de l'hôte au port 80 de web
- Dire aux conteneur de web de se partager le port 80 via un réseau avec load-balancing appellé webnet
- Définir le réseau webnet avec les paramètres par défaut (un réseau avec load-balancing)
Lancer notre nouvelle application
On va d'abord utiliser la commande :
docker swarm init
Le fonctionnement de cette commande sera expliqué plus loin.
NB: Un swarm est un outil de clustering et de scheduling servant à administrer un cluster de noeuds Docker comme si c'était un seul système virtuel.
Si j'ai plusieurs adresses sur mon interface réseau (IPv6...) je peux en préciser une en ajoutant --advertise-addr @IP
Je peux ensuite créer l'application :
docker stack deploy docker-compose.yml monservicetest
Je peux remplacer "monservicetest" par n'importe quel nom.
Désormais, notre service fait tourner 5 réplique de notre image sur le même hôte. On peut faire un docker service ls et constater que celui-ci est bien présent :
justine@Justine-pc:~/Dockertest$ docker service ls
ID NAME MODE REPLICAS IMAGE PORTS
c3jwhxzgvjj6 monservicetest_web replicated 5/5 justinep/monrepotest:letest *:4000->80/tcp
On a un certain nombre d'infos : un ID de service, le nom avec le suffixe _web, les répliques, etc.
Un seul conteneur tournant au sein d'un service est appellé une tâche (task). Les tâches ont un ID unique qui s'incrémente avec le nombre de répliques. Je peux les lister :
n4kztr59mt8o monservicetest_web.1 justinep/monrepotest:letest Justine-pc Running Running 4 minutes ago
by570g9v7nzn monservicetest_web.2 justinep/monrepotest:letest Justine-pc Running Running 4 minutes ago
k8mdckf6m8fg monservicetest_web.3 justinep/monrepotest:letest Justine-pc Running Running 4 minutes ago
84vxmgbbe1s0 monservicetest_web.4 justinep/monrepotest:letest Justine-pc Running Running 4 minutes ago
jigcz7c22hdh monservicetest_web.5 justinep/monrepotest:letest Justine-pc Running Running 4 minutes ago
On peut aussi les voir avec docker container ls, mais sans indications sur le service.
Si je vais voir mon 127.0.0.1:4000 et que je rafraichis la page plusieurs fois, je vois que le hostname renvoyé change, grâce au load balancing.
Faire du scaling de notre application
Il suffit de changer de changer le nombre de répliques dans le fichier docker-compose.yml, et de relancer le déploiement. Le service sera mis à jour, pas besoin de le couper avant : on peut rajouter des répliques.
Arrêter le swarm et l'application
On peut arrêter l'app avec:
docker stack rm monservicetest
Couper le swarm:
docker swarm leave --force
Note : les fichiers composés ainsi servent à définir des applications Docker, et peuvent être uploadés sur le cloud quand on utiliser Docker édition entreprise.
Résumé des commandes :
docker stack deploy -c <composefile> <appname> # Run the specified Compose file
docker service ls # List running services associated with an app
docker service ps <service> # List tasks associated with an app
docker inspect <task or container> # Inspect task or container
docker container ls -q # List container IDs
docker stack rm <appname> # Tear down an application
docker swarm leave --force # Take down a single node swarm from the manager
Les swarms (essaims)
Un swarm est un groupe de machines qui font tourner docker et qui ont rejoint un cluster. Une fois que ce cluster est en place, on peut continuer à utiliser les commandes docker que l'on connait, mais elles sont exécutées sur le cluster par un swarm manager. Les machines d'un swarm peuvent être physiques ou virtuelles. Après avoir rejoint un swam, elles sont appellées noeuds.
Les swarm managers peuvent utiliser différentes stratégies pour faire tourner des conteneurs, telles que "le noeud le plus vide" (emptiest node) qui remplit les machines les moins sollicitées, ou "global" qui s'assure que chaque machine a exactement une instance du conteneur spécifié. Ces instructions sont données dans le fichier de composition, comme celui utilisé précédemment.
Les swarm managers sont les seules machines du swarm qui peuvent exécuter des commandes, ou autoriser d'autres machines à rejoindre le swarm comme travailleurs. Les travailleurs ne font que fournir de la capacité de travail sans avoir aucune autorité sur les autres machines.
Jusqu'à présent nous avons utilisé Docker en mode single-host, cependant Docker peut être basculé en mode swarm. Passer dans le mode swarm fait automatiquement de la machine actuelle un swarm manager. À partir de là, Docker exécute les commandes sur tout le swarm et non plus sur notre seule machine.
Préparer le swarm
Un swarm est composé de noeuds, qu'ils soient physiques ou virtuels. Le concept de base est simplement d'utiliser la commande docker swarm init
pour activer le mode swarm faire de notre machine un manager, et de lancer docker swarm join
sur les autres machines pour qu'elles rejoignent le swarm comme travailleurs.
Nous allons utiliser des VMs pour la suite; à partir d'ici, j'utiliserais des VMs locales sur Linux à l'aide de VirtualBox.
Nous allons créer les deux vm avec docker-machine:
docker-machine create --driver virtualbox myvm1
docker-machine create --driver virtualbox myvm2
(En cas de soucis avec docker-machine, cf en haut de la page !)
Lister les VMs et avoir leurs IP
Tout simplement :
docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS
myvm1 - virtualbox Running tcp://192.168.99.100:2376 v18.09.0
myvm2 - virtualbox Running tcp://192.168.99.101:2376 v18.09.0
Initialiser le swarm et ajouter des noeuds
La première machine agit en tant que manager et exécute les commandes; elle authentifie les travailleurs qui rejoignent le swarm. La seconde est un travailleur.
On peut envoyer des commandes aux machines avec la commande docker-machine ssh. On va dire à myvm1 de devenir swarm manager et regarder la sortie:
docker-machine ssh myvm1 "docker swarm init --advertise-addr 192.168.99.100"
- La sortie :
Swarm initialized: current node (qcdqotcpeohwk9yi7dp4fvpcq) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-3ngct9c9o6a0j78qm49aivrjgo9i5hnk84ba823e2lcp2tv7bj-073c8yp54wekct3xzz1ory8om 192.168.99.100:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
Il faut toujours lancer docker swarm init et docker swarm join avec le port 2377 (port de gestion de swarm) ou ne rien mettre pour avoir le port par défaut.
docker-machine ls renvoie le port 2376, qui est le port du daemon Docker; il ne faut pas l'utiliser.
Si on veut utiliser le client ssh de notre propre machine, on peut ajouter l'argument --native-ssh.
On peut voir que le swarm init renvoie une commande de swarm join à donner aux autres machines pour qu'elles rejoignent le swarm comme travailleurs. On va l'envoyer à myvm2.
This node joined a swarm as a worker.
Et voilà ! Notre premier swarm est actif. On peut voir les noeuds depuis myvm1 qui est manager :
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS ENGINE VERSION
qcdqotcpeohwk9yi7dp4fvpcq * myvm1 Ready Active Leader 18.09.0
1mtumwa9zqmk09xj9mih5nt8c myvm2 Ready Active
Si l'on désire quitter le swarm, il suffit d'utiliser la commande docker swarm leave
sur un des noeuds.
Déployer notre application sur le cluster
Jusque ici, nous avons utilisé docker-machine ssh pour envoyer des commandes vers nos machines. Une autre option est d'utiliser la commande docker-machine env <machine> pour lancer une commande qui configure l'interpréteur de commandes actuel pour qu'il parle au daemon Docker sur la VM. C'est la meilleure méthode pour ce qui va suivre car elle permet d'utiliser un fichier docker-compose.yml local pour déployer les applications "à distance" sans avoir à le copier où que ce soit.
On va utiliser la commande docker-machine env myvm1 pour faire en sorte que notre shell parle à la VM1, ce sera utile pour la suite.
- $ docker-machine env myvm1
export DOCKER_TLS_VERIFY="1"
export DOCKER_HOST="tcp://192.168.99.100:2376"
export DOCKER_CERT_PATH="/home/justine/.docker/machine/machines/myvm1"
export DOCKER_MACHINE_NAME="myvm1"
# Run this command to configure your shell:
# eval $(docker-machine env myvm1)
- On nous invite à lancer une commande, ce que l'on fait :
- $ eval $(docker-machine env myvm1)
- Je vais vérifier que myvm1 est bien la machine active (regarder l'astérisque)
- ~$ docker-machine ls
NAME ACTIVE DRIVER STATE URL SWARM DOCKER ERRORS
myvm1 * virtualbox Running tcp://192.168.99.100:2376 v18.09.0
myvm2 - virtualbox Running tcp://192.168.99.101:2376 v18.09.0
Il est à noter que:
- Pour utiliser le shell sur une autre machine, il suffit d'utiliser à nouveau la commande docker-machine env dans le même ou un autre shell, puis d'utiliser la commande suivante sur la nouvelle machine à contrôler. Ces commandes sont toujours spécifiques au shell en cours.
- On peut aussi utiliser docker-machine ssh, mais on a alors pas d'accès immédiat au fichier sur la machine locale.
- On peut aussi utiliser docker-machine scp <fichier> <machine>:~ pour copier des fichiers depuis la machine locale.
Déployer l'application sur le swarm manager
Maintenant que l'on contrôle myvm1, on peut utiliser ses pouvoirs de swarm manager pour déployer notre app avec la même commande docker stack deploy
que l'on a utilisé auparavant, ainsi que notre copie locale de docker-compose.yml. La commande prend quelques secondes à s'accomplir, et le déploiement prend du temps avant d'être disponible. On peut ensuite utiliser la commande docker service ps <nom_du_service> sur le swarm manager pour vérifier que tout fonctionne:
justine@Justine-pc:~/Dockertest$ docker swarm init --advertise-addr 192.168.99.100
Swarm initialized: current node (jno762f6q1jnzfpn9evw3md9r) is now a manager.
To add a worker to this swarm, run the following command:
docker swarm join --token SWMTKN-1-5etyayjtyceujmmsakct446qf6lshz1tjvh41fj2hrrclla8xr-1tbbbgxsodbu4cj508lq9erm7 192.168.99.100:2377
To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.
justine@Justine-pc:~/Dockertest$ docker-machine ssh myvm2 "docker swarm join --token SWMTKN-1-5etyayjtyceujmmsakct446qf6lshz1tjvh41fj2hrrclla8xr-1tbbbgxsodbu4cj508lq9erm7 192.168.99.100:2377"
This node joined a swarm as a worker.
- Je lance le service
justine@Justine-pc:~/Dockertest$ docker stack deploy -c docker-compose.yml letest
Creating network letest_webnet
Creating service letest_web
- Je vérifie
justine@Justine-pc:~/Dockertest$ docker service ps letest_web
ID NAME IMAGE NODE DESIRED STATE CURRENT STATE ERROR PORTS
ie2uwhz2ci0q letest_web.1 justinep/monrepotest:letest myvm1 Running Running 31 seconds ago
o59l1s79q7fh letest_web.2 justinep/monrepotest:letest myvm2 Running Running 42 seconds ago
m0o5ejudaqku letest_web.3 justinep/monrepotest:letest myvm1 Running Running 31 seconds ago
lxfbcszozwxs letest_web.4 justinep/monrepotest:letest myvm2 Running Running 42 seconds ago
À noter : si mon docker-compose.yml prévoit de faire 4 répliques du service, ces 4 répliques sont réparties sur les deux vms.
Accéder à mon application
Il suffit d'utiliser à l'ip de l'une des deux machines via un navigateur. Les machines sont reliées entre elles via un réseau IP :