Docker, créer et administrer
vos conteneurs d'applications
Allez-y doucement avec le WiFi!
N'utilisez pas votre hotspot.
Ne chargez pas de vidéos, ne téléchargez pas de gros fichiers pendant la formation.
Bonjour, je suis:
Cet atelier se déroulera de 9h à 17h.
La pause déjeuner se fera entre 12h et 13h30.
(avec 2 pauses café à 10h30 et 15h!)
N'hésitez pas à m'interrompre pour vos questions, à n'importe quel moment.
Surtout quand vous verrez des photos de conteneurs en plein écran!
Ce cours a été écrit à l'origine comme support à des ateliers en présentiel
Ce contenu est maintenu par Jérôme Petazzoni et de nombreux contributeurs
La traduction intégrale depuis l'anglais est réalisée et maintenue par @djalal
Vous pouvez aussi le parcourir à votre propre rythme.
Nous avons inclus autant d'information que possible dans les diapositives.
Nous vous recommandons d'avoir un instructeur pour vous aider...
... sauf si vous êtes à l'aise avec la lecture minutieuse de la documentation Docker...
... et avec la recherche de réponses dans les forums Docker, StackOverflow, et autres boutiques.
Tout le contenu est disponible dans un dépôt public Github:
Vous pouvez obtenir une version à jour de ces diapos ici:
https://container.training/ (anglais) ou https://docker.djal.al/ (français)
Tout le contenu est disponible dans un dépôt public Github:
Vous pouvez obtenir une version à jour de ces diapos ici:
https://container.training/ (anglais) ou https://docker.djal.al/ (français)
👇 Essayez! Le code source sera affiché et vous pourrez l'ouvrir dans Github pour le consulter et le corriger.
Cette diapo a une petite loupe dans le coin en haut à gauche.
Cette loupe signifie que ces diapos apportent des détails supplémentaires.
Vous pouvez les zapper si:
vous êtes pressé(e);
vous êtes tout nouveau et vous craignez la surcharge cognitive;
vous ne souhaitez que l'essentiel des informations.
Vous pourrez toujours y revenir une autre fois, ils vous attendront ici ☺
(auto-generated TOC)
(auto-generated TOC)
(auto-generated TOC)
(auto-generated TOC)
(auto-generated TOC)
Docker vu d'hélicoptère
(automatically generated title slide)
Dans cette leçon, nous apprendrons:
Pourquoi les conteneurs (pitch d'ascenseur non-technique)
Pourquoi les conteneurs (version technique du pitch d'ascenseur)
Comment Docker nous aide à les construire, transporter et lancer
L'Histoire des conteneurs
On ne va pas (encore!) lancer Docker ou des conteneurs dans ce chapitre.
Pas de souci, nous y arriverons assez tôt!
L'industrie logicielle a changé
Avant:
Maintenant:
Nombreuses technos différentes:
Nombreux environnements différents:
Ecrire les instructions d'installation dans un fichier INSTALL.txt
Avec ce fichier, écrire un script install.sh
qui va marcher pour vous
Traduire ce fichier en Dockerfile
, le tester sur votre machine
Si le Dockerfile passe sur votre machine, il passera n'importe où
Réjouissez-vous, car vous êtes sauvé de l'enfer des dépendances et du "ça marche sur ma machine"
Plus jamais de "ça marchait en dev - c'est le problème des admins maintenant!"
Ecrire les Dockerfiles pour les composants applicatifs
Utiliser des images pré-générées du Docker Hub (mysql, redis, etc.)
Décrire votre suite logicielle avec un fichier Compose
Intégrer quelqu'un avec deux commandes:
git clone ...docker-compose up
Avec ça, vous pouvez monter des environnements de développement, intégration ou QA en quelques minutes!
Montez un environnement de test avec un Dockerfile ou un fichier Compose
Pour chaque lancement de test, montez un nouveau conteneur (ou une suite complète)
Chaque test est lancé dans un environnement propre.
Aucune pollution des précédents tests
Bien plus rapide et économique que de monter des VMs à chaque fois!
Générez votre appli à partir de Dockerfiles
Stockez les images résultantes dans un dépôt
Stockez-les pour toujours (ou aussi longtemps que nécessaire)
Testez ces images en QA, CI ou intégration...
Lancez les mêmes images en production
Quelque chose est cassé? Repassez à l'image précédente.
Diagnostic d'une ancienne régression? Une ancienne image est toujours là pour vous!
Les images contiennent toutes les bibliothèques, dépendances, etc. nécessaires au lancement de l'appli.
Ecrivez votre code pour qu'il se connecte à des services nommés ("db", "api", etc.)
Utilisez Compose pour démarrer votre suite
Docker va fournir un DNS pour conteneur pour résoudre ces noms de services
Vous pouvez maintenant monter en charge, ajouter des répartiteurs de charge, de la réplication... sans changer votre code.
Note: ce n'est pas couvert dans cet atelier d'introduction!
Aucun format d'échange standard.
(Non, un fichier tarball n'est pas un format!)
Difficile d'utiliser des conteneurs pour les développeurs.
(Quel est l'équivalent d'un docker run debian
?)
En conséquence, ils restent cachés des utilisateurs finaux.
Aucun composant réutilisable, APIs ou outils.
(Au mieux, abstractions de VMs, e.g libvirt)
Analogie:
Standardiser le format de conteneur, parce que les conteneurs n'étaient pas portables.
Rendre les conteneurs faciles à utiliser pour les développeurs.
Focus sur les composants réutilisables, APIs et l'écosystème d'outils standard.
Amélioration par rapport aux outils ad-hoc, interne et spécifique.
Déploiement par paquets: deb, rpm, gem, jar, homebrew...
Enfer des dépendances
"Ça marche chez moi."
Base de livraison souvent réalisée de zéro (debootstrap...) et fragile.
Livrez des images de conteneurs avec toutes leurs dépendances.
De plus grosses images, mais sous-découpées en couches.
Ne livre que les couches qui ont changé.
Économise du disque, du réseau et de la mémoire.
Couches (Layers):
On va livrer un fichier tarball (ou un hash de commit) avec ses instructions.
Avec un environnement de dev très différent de la production.
Sauf que les admin. système n'ont pas toujours un environnement de test eux-mêmes...
... et quand ils l'ont, il peut différer de celui des devs.
Donc les admin. système doivent déterminer les différences, et faire en sorte que ça marche...
... ou le renvoyer aux développeurs.
Déployer du code est cause de friction et de délais.
On va livrer une image de conteneur ou un fichier Compose.
Un admin. système pourra toujours lancer cette image de conteneur.
Un admin. système pourra toujours lancer ce fichier Compose.
Les admin. doivent toujours adapter la configuration à l'environnement de prod, mais ils ont au moins un point de référence.
L'outillage des admin. système permet d'utiliser la même image en dev et prod.
Les développeurs pourront plus facilement être mis en position de lancer les déploiements eux-mêmes.
Histoire des conteneurs ... et de Docker
(automatically generated title slide)
Cela fait très longtemps que les conteneurs existent.
(Voir cet excellent billet par Serge Hallyn pour plus de détails historiques.)
Utilisateurs: fournisseurs d'hébergement.
Audience hautement spécialisée avec une forte culture d'admin. système.
Je ne peux pas parler pour Heroku, mais les conteneurs étaient l'arme secrète de dotCloud (parmi d'autres).
dotCloud maintenait un PaaS, via un moteur de conteneur personnalisé.
Ce moteur était basé sur OpenVZ (et plus tard, LXC) et AUFS.
Tout a commencé (vers 2008) par un simple script Python.
En 2012, le moteur comptait plusieurs composants Python (env. 10)
(et env. 100 micro-services!)
Fin 2012, dotCloud reconstruit le moteur de conteneur.
Le nom de code de ce projet est "Docker".
Mars 2013, Pycon, Santa Clara:
"Docker" est montré en public pour la première fois.
Il est publié avec une licence open source.
Réactions et retours très positifs!
L'équipe dotCloud est progressivement réaffectée au développement de Docker.
La même année, dotCloud change de nom pour s'appeler Docker.
En 2014, l'activité PaaS est revendue.
Gestionnaires de PAAS (Flynn, Dokku, Tsuru, Deis...)
Utilisateurs de PAAS (ceux assez gros pour justifier la construction de leur propre PAAS)
Services d'intégration continue
développeurs, développeurs, développeurs
En 2013, la technologie sous-tenant les conteneurs (cgroups, namespaces, stockage copy-on-write, etc.)
La popularité croissante de Docker et des conteneurs a mis en lumière de nombreux bugs.
En conséquence, ces bugs sont corrigés, résultant dans une meilleure stabilité des conteneurs.
Aujourd'hui, tout fournisseur d'hébergement/cloud un peu sérieux peut lancer des conteneurs.
Les conteneurs sont devenus un super outil pour déployer/transporter des applis vers/depuis les environnements on-prem/cloud.
Docker atteint le jalon symbolique du 1.0
Docker est maintenant supporté par les systèmes existants comme Mesos ou Cloud Foundry.
Standardisation autour de l'OCI (Open Containers Initiative).
De nouveaux moteurs de conteneurs sont développés.
Création de la CNCF (Cloud Native Computing Foundation).
Le moteur de conteneur initial est maintenant nommé "Docker Engine".
D'autres outils y sont ajoutés:
Docker Inc. lance ses offres commerciales.
Notre environnement de formation
(automatically generated title slide)
Si vous assistez à un atelier ou un tutoriel:
Si vous suivez ou révisez ce cours tout seul, vous pouvez:
installer Docker en local (comme expliqué dans le chapitre "Installer Docker")
installer Docker sur une VM dans le cloud (par ex.)
utiliser https://www.play-with-docker.com/ pour lancer en un instant un environnement de formation
containers/Training_Environment.md
Cette section suppose que vous suivez ce cours dans le cadre d'un tutoriel, une atelier ou une formation, où chaque apprenant reçoit une VM Docker individuelle.
La VM est créée juste avant la formation.
Elle restera allumée pendant toute la durée de la formation.
Elle sera détruite peu de temps après la formation.
Elle est fournie avec Docker et quelques autres outils utiles.
containers/Training_Environment.md
"Installer Docker" signifie en vrai "Installer le Docker Engine et le client en ligne de commande".
Le Docker Engine est un daemon (un service tournant en tâche de fond).
Ce daemon gère les conteneurs, à la manière d'un hyperviseur qui gère ses VMs.
Nous dialoguons avec le Docker Engine par la Docker CLI (ligne de commande).
Docker CLI et Docker Engine communiquent via une API.
Il existe de nombreux autres programmes, et de composants client, pour exploiter cette API.
containers/Training_Environment.md
On va télécharger des images de conteneur et des paquets de distribution.
Cela pourrait quelque peu stresser la connexion WiFi locale, et nous ralentir.
Au lieu de ça, on préfère passer par une VM distante qui a une meilleure connectivité.
Dans de rares cas, installer Docker en local peut s'avérer tortueux:
pas d'accès au compte admin/root (poste géré par une DSI stricte)
CPU ou système d'exploitation 32 bits.
vieille version de l'OS (par ex. CentOS 6, OSX pré-Yosemite, Windows 7)
Il est meilleur de passer du temps à apprendre les conteneurs qu'à trifouiller l'installateur!
containers/Training_Environment.md
Vous avez besoin d'un client SSH.
ssh
suffit:$ ssh <login>@<ip-address>
Sur Windows, si vous n'avez pas de client SSH, vous pouvez télécharger:
Putty (www.putty.org)
Git BASH (https://git-for-windows.github.io/)
MobaXterm (https://mobaxterm.mobatek.net/)
containers/Training_Environment.md
Une fois connecté(e), assurez-vous que la commande Docker de base fonctionne:
$ docker versionClient: Version: 18.03.0-ce API version: 1.37 Go version: go1.9.4 Git commit: 0520e24 Built: Wed Mar 21 23:10:06 2018 OS/Arch: linux/amd64 Experimental: false Orchestrator: swarmServer: Engine: Version: 18.03.0-ce API version: 1.37 (minimum version 1.12) Go version: go1.9.4 Git commit: 0520e24 Built: Wed Mar 21 23:08:35 2018 OS/Arch: linux/amd64 Experimental: false
Si ça ne marche pas, levez la main et on viendra vous aider!
containers/Training_Environment.md
Nos premiers conteneurs
(automatically generated title slide)
À la fin de cette leçon, vous aurez:
vu Docker en action;
démarré vos premiers conteneurs.
containers/First_Containers.md
Depuis votre environnement Docker, lancez juste la commande suivante:
$ docker run busybox echo hello worldhello world
(Si votre installation Docker est vierge, quelques lignes en plus s'afficheront,
correspondant au téléchargement de l'image busybox
.)
containers/First_Containers.md
Nous avons utilisé l'une des images les plus petites et simples: busybox
.
busybox
est typiquement utilisée dans les systèmes embarqués (téléphones, routeurs, etc.)
Nous avons lancé un seul processus pour afficher hello world
.
containers/First_Containers.md
Lançons un conteneur un peu plus excitant:
$ docker run -it ubunturoot@04c0bb0a6c07:/#
C'est un conteneur tout neuf.
Il exécute un système ubuntu
basique et sans fioritures.
-it
est le raccourci pour -i -t
.
-i
dit à Docker de nous connecter à l'entrée du conteneur.
-t
dit à Docker que nous voulons un pseudo-terminal.
containers/First_Containers.md
Essayez de lancer figlet
dans notre conteneur.
root@04c0bb0a6c07:/# figlet hellobash: figlet: command not found
D'accord, donc nous allons devoir l'installer.
containers/First_Containers.md
Nous voulons figlet
, alors installons-le:
root@04c0bb0a6c07:/# apt-get update...Fetched 1514 kB in 14s (103 kB/s)Reading package lists... Doneroot@04c0bb0a6c07:/# apt-get install figletReading package lists... Done...
Une minute plus tard, figlet
est installé!
containers/First_Containers.md
Le programme figlet
prend un message en paramètre.
root@04c0bb0a6c07:/# figlet hello _ _ _ | |__ ___| | | ___ | '_ \ / _ \ | |/ _ \ | | | | __/ | | (_) ||_| |_|\___|_|_|\___/
Magnifique! 😍
containers/First_Containers.md
Vérifions maintenant combien de paquets y sont installés.
root@04c0bb0a6c07:/# dpkg -l | wc -l190
dpkg -l
liste les paquets installés dans notre conteneur
wc -l
va les compter
Combien de paquets avons-nous sur notre hôte?
containers/First_Containers.md
Quittez le conteneur en vous déconnectant du shell, comme d'habitude.
(i.e. avec ^D
ou exit
)
root@04c0bb0a6c07:/# exit
Maintenant, essayons de:
lancer dpkg -l | wc -l
. Combien de paquets sont installés?
lancer figlet
. Est-ce que ça marche?
containers/First_Containers.md
Nous avons lancé un conteneur ubuntu
sur un hôte Linux/Windows/macOS.
Ils possèdent des paquets indépendants et différents.
Installer quelque chose sur l'hôte ne l'expose pas dans le conteneur.
Et vice-versa.
Même si l'hôte et le conteneur ont tous deux la même distribution Linux.
Nous pouvons lancer n'importe quel conteneur sur n'importe quel hôte.
(Une exception: les conteneurs Windows ne peuvent tourner sur les machines Linux; par encore en tout cas.)
containers/First_Containers.md
Notre conteneur est maintenant en état stopped.
Il existe encore sur le disque, mais toutes ses ressources ont été libérées.
Nous verrons plus tard comment récupérer ce conteneur.
containers/First_Containers.md
Et si nous démarrions un nouveau conteneur, pour y lancer à nouveau figlet
?
$ docker run -it ubunturoot@b13c164401fb:/# figletbash: figlet: command not found
Nous avons lancé un tout nouveau conteneur.
Dans l'image de base Ubuntu utilisée ci-dessus, figlet
est absent.
containers/First_Containers.md
Pouvons nous réutiliser ce conteneur que nous avons pris le soin de personnaliser?
On pourrait, mais ce n'est pas le mode de production par défaut avec Docker.
Quel est le processus général, alos?
Toujours démarrer avec un conteneur tout nouveau.
Si on a besoin d'installer quoique ce soit dans notre conteneur, générer une nouvelle image.
Ça a l'air compliqué!
Nous allons voir que c'est en fait assez simple!
Et tout ça pour quoi?
Tout ça pour appuyer sur l'automatisation et la répétabilité. Voyons voir pourquoi ...
containers/First_Containers.md
Dans la métaphore "pets vs cattle", il existe deux genres de serveurs.
Les animaux de compagnie (Pets):
ont un petit nom et une configuration unique
quand ils défaillent, on fait tout ce qu'on peut pour les soigner.
Le bétail (Cattle):
ont des noms génériques (par ex. contenant des numéros) et une configuration générique
leur configuration est générée par une couche de gestion de configuration, et des templates ...
quand une panne advient, on peut les remplacer immédiatement par un nouveau serveur
Quelle est la relation avec Docker et les conteneurs?
containers/First_Containers.md
Avec l'usage de VMs locales (comme par ex. Virtualbox ou VMware), notre flux de travail ressemble à ce qui suit:
créer une VM à partir d'un gabarit de base (Ubuntu, CentOS...)
installer les paquets, configurer l'environnement
travail sur le projet en tant que tel
finalement, éteindre la VM
la prochaine fois qu'on aborde ce projet, redémarrer la VM dans l'état où on l'a laissée
si on a besoin de retoucher l'environnement, on le fait en direct.
Au fil du temps, la configuration de la VM évolue et diverge.
Il nous manque une procédure propre, fiable et déterministe de provisionner cet environnement.
containers/First_Containers.md
Avec Docker, la démarche est la suivante:
créer une image de conteneur représentant notre environnement de dév.
lancer un conteneur basé sur cette image
travailler sur notre projet proprement dit
finalement, stopper le conteneur
la prochaine fois qu'on aborde le projet, démarrer un nouveau contneeur
en cas de changement de l'environnement, on créé une nouvelle image.
Nous avons une définition claire de notre environnement, qu'on peut partager de manière fiable avec les autres.
Nous verrons dans les chapitres suivants comment préparer une image personnalisée avec figlet
.
containers/First_Containers.md
Conteneurs en tâche de fond
(automatically generated title slide)
Nos premiers conteneurs étaient interactifs.
Nous allons maintenant voir comment:
containers/Background_Containers.md
Nous allons lancer un petit conteneur spécial.
Ce conteneur ne fait qu'afficher l'heure à chaque seconde.
$ docker run jpetazzo/clockFri Feb 20 00:28:53 UTC 2015Fri Feb 20 00:28:54 UTC 2015Fri Feb 20 00:28:55 UTC 2015...
^C
.jpetazzo/clock
.jpetazzo
.containers/Background_Containers.md
Les conteneurs peuvent être démarrés en tâche de fond, avec l'option -d
(mode daemon)
$ docker run -d jpetazzo/clock47d677dcfba4277c6cc68fcaa51f932b544cab1a187c853b7d0caf4e8debe5ad
containers/Background_Containers.md
Comment vérifier que notre conteneur est encore lancé?
Avec docker ps
, tout comme la commande ps
d'UNIX, qui liste les processus qui tournent.
$ docker psCONTAINER ID IMAGE ... CREATED STATUS ...47d677dcfba4 jpetazzo/clock ... 2 minutes ago Up 2 minutes ...
Docker nous indique:
Up
) depuis quelques minutes;containers/Background_Containers.md
Démarrons deux autres conteneurs.
$ docker run -d jpetazzo/clock57ad9bdfc06bb4407c47220cf59ce21585dce9a1298d7a67488359aeaea8ae2a
$ docker run -d jpetazzo/clock068cc994ffd0190bbe025ba74e4c0771a5d8f14734af772ddee8dc1aaf20567d
Vérifiez que docker ps
mentionne correctement tous les 3 conteneurs.
containers/Background_Containers.md
Quand de nombreux conteneurs tournent déjà, il peut être utile de limiter l'affichage au dernier conteneur démarré.
C'est à ça que sert l'option -l
("Last"):
$ docker ps -lCONTAINER ID IMAGE ... CREATED STATUS ...068cc994ffd0 jpetazzo/clock ... 2 minutes ago Up 2 minutes ...
containers/Background_Containers.md
Plusieurs commandes Docker sont basées sur des IDs de conteneurs: docker stop
, docker rm
, etc.
Si nous voulons lister uniquement les IDs de nos conteneurs (sans les autres colonnes ni en-tête),
nous pouvons utiliser l'option -q
("Quiet", "Quick"):
$ docker ps -q068cc994ffd057ad9bdfc06b47d677dcfba4
containers/Background_Containers.md
Nous pouvons combiner -l
et -q
pour uniquement voir l'ID du dernier conteneur démarré:
$ docker ps -lq068cc994ffd0
A première vue, cela parait vraiment utile dans le cadre de scripts.
Toutefois, si nous voulons démarrer un conteneur et récupérer son ID de manière sécurisée,
il est plus conseillé d'utiliser docker run -d
, ce que nous aborderons dans un instant.
(Using docker ps -lq
is prone to race conditions: what happens if someone
else, or another program or script, starts another container just before
we run docker ps -lq
?)
containers/Background_Containers.md
On vous a dit que Docker enregistrait l'affichage d'un conteneur.
C'est le moment d'en parler.
$ docker logs 068Fri Feb 20 00:39:52 UTC 2015Fri Feb 20 00:39:53 UTC 2015...
logs
va afficher les logs complets du conteneur.
containers/Background_Containers.md
Pour éviter de se faire spammer avec des dizaines de pages d'infos,
on peut utiliser l'option --tail
:
$ docker logs --tail 3 068Fri Feb 20 00:55:35 UTC 2015Fri Feb 20 00:55:36 UTC 2015Fri Feb 20 00:55:37 UTC 2015
containers/Background_Containers.md
Tout comme la commande UNIX standard tail -f
, on peut
suivre les logs de notre conteneur:
$ docker logs --tail 1 --follow 068Fri Feb 20 00:57:12 UTC 2015Fri Feb 20 00:57:13 UTC 2015^C
^C
.containers/Background_Containers.md
Il y a deux façons de stopper notre conteneur détaché;
docker kill
.docker stop
.La première arrête le conteneur immédiatement, en utilisant
le signal KILL
.
La seconde est plus douce. Elle envoie un signal TERM
, et
après 10 secondes, si le conteneur n'est pas arrêté,
il envoie KILL
.
Rappel: le signal KILL
ne peut être intercepté, et terminera
le conteneur de force.
containers/Background_Containers.md
Essayons d'arrêter un de ces conteneurs:
$ docker stop 47d647d6
Cela va prendre 10 secondes:
containers/Background_Containers.md
Soyons moins patient avec les deux autres conteneurs:
$ docker kill 068 57ad06857ad
Les commandes stop
et kill
acceptent plusieurs IDs de conteneurs.
Ces conteneurs seront supprimés immédiatement (sans le délai de 10 secondes).
Vérifions que nos conteneurs ne s'affichent plus:
$ docker ps
containers/Background_Containers.md
Nous pouvons aussi afficher les conteneurs stoppés, avec l'option -a
(--all
).
$ docker ps -aCONTAINER ID IMAGE ... CREATED STATUS068cc994ffd0 jpetazzo/clock ... 21 min. ago Exited (137) 3 min. ago57ad9bdfc06b jpetazzo/clock ... 21 min. ago Exited (137) 3 min. ago47d677dcfba4 jpetazzo/clock ... 23 min. ago Exited (137) 3 min. ago5c1dfd4d81f1 jpetazzo/clock ... 40 min. ago Exited (0) 40 min. agob13c164401fb ubuntu ... 55 min. ago Exited (130) 53 min. ago
containers/Background_Containers.md
Comprendre les images Docker
(automatically generated title slide)
Dans cette section, nous expliquerons:
Ce qu'est une image;
Ce qu'est un layer;
Les différents nommages d'image;
Comment chercher et télécharger des images;
Les tags d'image et quand les utiliser.
Image = fichiers + méta-données
Ces fichiers forment le système de fichier racine de notre conteneur.
Les méta-données indiquent un nombre de choses, comme:
Des couches composent les images, appelées layers, empilées les unes au-dessus des autres.
Chaque couche peut ajouter, modifier, supprimer des fichiers ou des méta-données.
Les layers sont partagés entre images pour optimiser l'usage du disque, les temps de transfert et la consommation mémoire.
Chaque des points suivants se traduira par un layer:
Une image est un système de fichiers en lecture seule.
Un conteneur est un ensemble de processus encapsulé dans une copie en lecture/écriture de ce système de fichiers.
Pour optimiser le temps de démarrage du conteneur, on fait du copy-on-write au lieu d'une copie traditionnelle.
docker run
démarre un conteneur depuis une image donnée.
Conceptuellement, les images sont proches des classes.
Conceptuellement, les layers sont proches de l'héritage.
Conceptuellement, les conteneurs sont proches des instances.
Si une image est en lecture-seule, comment on la change?
On ne la change pas.
On lance un nouveau conteneur à partir de cette image.
Puis on apporte des modifications à ce conteneur.
Quand on a fini, nous les figeons dans un nouveau layer.
Une nouvelle image est créée en empilant la nouvelle couche au-dessus de l'ancienne image.
La seule façon de créer une image est de geler un conteneur.
La seule façon de créer un conteneur est d'instancier une image.
A l'aide!
Il existe une image spéciale vide, appelée scratch
.
La commande docker import
charge un fichier tarball dans Docker.
Note: vous n'aurez sans doute jamais à faire cela vous-même.
docker commit
docker build
(utilisé 99% du temps)
Nous expliquerons les deux méthodes dans un moment.
Il existe trois espaces de nommage (namespaces):
Images officielles:
par ex. ubuntu
, busybox
, etc.
Images d'utilisateurs (et organisations):
par ex. jpetazzo/clock
Images auto hébergées
par ex. registry.example.com:5000/mon-image/privee
Examinons chacun d'entre eux.
L'espace de nom racine est pour les images officielles. Elles y sont placées par Docker Inc., mais sont généralement écrites et maintenues par des tierces parties.
Ces images incluent:
De petites images "couteau suisse", telles busybox.
Des images de distributions Linux servant de base aux builds, comme ubuntu, fedora, etc.
Des services et composants prêts à l'emploi, comme redis, postgresql, etc.
Plus de 150 à ce jour!
L'espace de nommage pour utilisateur contient les images dans Docker Hub fournies par les utilisateurs et organisations.
Par exemple:
jpetazzo/clock
L'utilisateur Docker Hub est:
jpetazzo
Le nom de l'image est:
clock
Cet espace de nommage contient les images qui ne sont pas hébergées sur Docker Hub, mais sur des registres de tierce partie.
Ils contiennent le nom de serveur (ou adresse IP), et le port (en option), du serveur de registre.
Par exemple:
localhost:5000/wordpress
localhost:5000
est l'hôte et le port du registrewordpress
est le nom de cette imageOther examples:
quay.io/coreos/etcdgcr.io/google-containers/hugo
On stocke les images:
Vous pouvez utiliser le client Docker pour télécharger (pull) ou téléverser (push) des images.
Pour être plus précis: vous pouvez utiliser le client Docker pour intimer au Docker Engine de push et pull des images vers/depuis un registre.
Voyons quelles sont les images disponibles sur notre serveur.
$ docker imagesREPOSITORY TAG IMAGE ID CREATED SIZEfedora latest ddd5c9c1d0f2 3 days ago 204.7 MBcentos latest d0e7f81ca65c 3 days ago 196.6 MBubuntu latest 07c86167cdc4 4 days ago 188 MBredis latest 4f5f397d4b7c 5 days ago 177.6 MBpostgres latest afe2b5e1859b 5 days ago 264.5 MBalpine latest 70c557e50ed6 5 days ago 4.798 MBdebian latest f50f9524513f 6 days ago 125.1 MBbusybox latest 3240943c9ea3 2 weeks ago 1.114 MBtraining/namer latest 902673acc741 9 months ago 289.3 MBjpetazzo/clock latest 12068b93616f 12 months ago 2.433 MB
Nous ne pouvons lister toutes les images sur un registre distant, mais nous pouvons chercher un mot-clé spécifique:
$ docker search marathonNAME DESCRIPTION STARS OFFICIAL AUTOMATEDmesosphere/marathon A cluster-wide init and co... 105 [OK]mesoscloud/marathon Marathon 31 [OK]mesosphere/marathon-lb Script to update haproxy b... 22 [OK]tobilg/mongodb-marathon A Docker image to start a ... 4 [OK]
"Stars" mesure la popularité de l'image.
"Official" concerne les images qui sont dans le namespace racine.
"Automated" indique que l'image est générée automatiquement par le Docker Hub.
(Cela signifie que leur recette de construction est toujours disponible.)
Il y a deux façons de récupérer des images.
Explicite, avec docker pull
.
Implicite, en lançant docker run
et l'image n'est pas disponible en local.
$ docker pull debian:jessiePulling repository debianb164861940b8: Download completeb164861940b8: Pulling image (jessie) from debiand1881793a057: Download complete
Comme vu précédemment, les images sont faites de layers.
Docker a téléchargé tous les layers nécessaires.
Dans notre exemple, :jessie
indique quelle version exacte de Debian nous voulons.
C'est un tag (étiquette) de version.
On peut associer des tags aux images.
C'est utile pour préciser les versions ou variantes d'une image.
docker pull ubuntu
va se référer à ubuntu:latest
.
Le tag latest
est par tradition mis à jour fréquemment.
Pas besoin de spécifier d'étiquette (tag) pour:
Utiliser des tags pour:
This is similar to what we would do with pip install
, npm install
, etc.
Nous avons appris comment:
Construire des images Docker avec un Dockerfile
(automatically generated title slide)
containers/Building_Images_With_Dockerfiles.md
Nous allons construire une image de conteneur automatiquement, grâce au Dockerfile
.
A la fin de cette leçon, vous saurez comment:
Ecrire un Dockerfile
.
Générer une image (build) via un Dockerfile
.
containers/Building_Images_With_Dockerfiles.md
Dockerfile
Un Dockerfile
est une recette de construction pour une image Docker.
Il contient une série d'instructions indiquant à Docker comment l'image est construite.
La commande docker build
génère une image à partir d'un Dockerfile
.
containers/Building_Images_With_Dockerfiles.md
Dockerfile
Notre Dockerfile doit être dans un dossier nouveau et vide.
Dockerfile
.$ mkdir myimage
Dockerfile
à l'intérieur de ce nouveau dossier.$ cd myimage$ vim Dockerfile
Bien sûr, vous pouvez utiliser n'importe quel éditeur de votre choix.
containers/Building_Images_With_Dockerfiles.md
FROM ubuntuRUN apt-get updateRUN apt-get install figlet
FROM
indique notre image de base pour notre build.
Chaque ligne RUN
sera exécutée par Docker pendant le build.
Nos commandes RUN
doivent être non-interactive.
(Aucune entrée ne peut être fournie à Docker pendant le build).
Dans bien des cas, nous ajouterons l'option -y
à apt-get
.
containers/Building_Images_With_Dockerfiles.md
Enregistrez notre fichier, et lancez:
$ docker build -t figlet .
-t
indique le tag à appliquer à l'image.
.
indique la localisation du build context.
Nous parlerons en détails du build context plus tard.
Pour garder les choses simples: c'est le dossier où se trouve notre Dockerfile
.
containers/Building_Images_With_Dockerfiles.md
L'affichage de docker build
ressemble à ceci:
docker build -t figlet .Sending build context to Docker daemon 2.048kBStep 1/3 : FROM ubuntu ---> f975c5035748Step 2/3 : RUN apt-get update ---> Running in e01b294dbffd(...output of the RUN command...)Removing intermediate container e01b294dbffd ---> eb8d9b561b37Step 3/3 : RUN apt-get install figlet ---> Running in c29230d70f9b(...output of the RUN command...)Removing intermediate container c29230d70f9b ---> 0dfd7a253f21Successfully built 0dfd7a253f21Successfully tagged figlet:latest
RUN
a été omis.containers/Building_Images_With_Dockerfiles.md
Sending build context to Docker daemon 2.048 kB
Le build context est le dossier .
donné à docker build
.
Il est envoyé (sous forme d'archive) par le client Docker au daemon Docker.
Cela permet d'utiliser un serveur distant pour le build utilisant des fichiers locaux.
Soyez attentifs (ou patient) si ce dossier est lourd et votre connexion est lente.
You can speed up the process with a .dockerignore
file
It tells docker to ignore specific files in the directory
Only ignore files that you won't need in the build context!
containers/Building_Images_With_Dockerfiles.md
Step 2/3 : RUN apt-get update ---> Running in e01b294dbffd(...output of the RUN command...)Removing intermediate container e01b294dbffd ---> eb8d9b561b37
Un container (e01b294dbffd
) est créé à partir de l'image de base.
La commande RUN
se lance dans ce container.
Le conteneur est sauvé dans une nouvelle image (eb8d9b561b37
)
Le conteneur de build (e01b294dbffd
) est supprimé.
Le résultat de cette étape sera l'image de base pour la prochaine commande.
containers/Building_Images_With_Dockerfiles.md
Si vous lancez le même build de nouveau, ce sera instantané. Pourquoi?
Après chaque étape de build, Docker prend un snapshot de l'image résultante.
Avant chaque nouvelle étape, Docker vérifie si la même séquence a été générée.
Docker utilise les chaines de caractères exactes définies dans votre Dockerfile
, donc:
RUN apt-get install figlet cowsay
RUN apt-get install cowsay figlet
RUN apt-get update
n'est pas exécuté, quand les miroirs sont mis à jour.Vous pouvez forcer un nouveau build avec docker build --no-cache...
.
containers/Building_Images_With_Dockerfiles.md
L'image résultante n'est pas différente de celle définie manuellement.
$ docker run -ti figletroot@91f3c974c9a1:/# figlet hello _ _ _ | |__ ___| | | ___ | '_ \ / _ \ | |/ _ \ | | | | __/ | | (_) ||_| |_|\___|_|_|\___/
Youpi! 🎉
containers/Building_Images_With_Dockerfiles.md
La commande history
liste toutes les couches composant une image.
Pour chaque couche (layer), on voit la date de création, sa taille et la commande utilisée.
Quand une image est générée via un Dockerfile
, chaque layer correspond à une ligne du Dockerfile
.
$ docker history figletIMAGE CREATED CREATED BY SIZEf9e8f1642759 About an hour ago /bin/sh -c apt-get install fi 1.627 MB7257c37726a1 About an hour ago /bin/sh -c apt-get update 21.58 MB07c86167cdc4 4 days ago /bin/sh -c #(nop) CMD ["/bin 0 B<missing> 4 days ago /bin/sh -c sed -i 's/^#\s*\( 1.895 kB<missing> 4 days ago /bin/sh -c echo '#!/bin/sh' 194.5 kB<missing> 4 days ago /bin/sh -c #(nop) ADD file:b 187.8 MB
containers/Building_Images_With_Dockerfiles.md
La plupart des arguments de Dockerfile
peuvent être passés sous deux formes:
chaine simple:
RUN apt-get install figlet
liste JSON:
RUN ["apt-get", "install", "figlet"]
Nous allons changer notre Dockerfile et voir comment cela affecte l'image résultante.
containers/Building_Images_With_Dockerfiles.md
Changeons notre Dockerfile comme suit:
FROM ubuntuRUN apt-get updateRUN ["apt-get", "install", "figlet"]
Puis relançons un build du nouveau Dockerfile.
$ docker build -t figlet .
containers/Building_Images_With_Dockerfiles.md
Comparons le nouvel historique:
$ docker history figletIMAGE CREATED CREATED BY SIZE27954bb5faaf 10 seconds ago apt-get install figlet 1.627 MB7257c37726a1 About an hour ago /bin/sh -c apt-get update 21.58 MB07c86167cdc4 4 days ago /bin/sh -c #(nop) CMD ["/bin 0 B<missing> 4 days ago /bin/sh -c sed -i 's/^#\s*\( 1.895 kB<missing> 4 days ago /bin/sh -c echo '#!/bin/sh' 194.5 kB<missing> 4 days ago /bin/sh -c #(nop) ADD file:b 187.8 MB
La syntaxe JSON spécifie une commande exacte à exécuter.
La syntaxe simple spécifie une commande à être encapsulée dans /bin/sh -c "..."
.
containers/Building_Images_With_Dockerfiles.md
La syntaxe simple:
/bin/sh -c ...
) pour interpréter la commande/bin/sh
dans le conteneurLa syntaxe JSON:
/bin/sh
dans le conteneurcontainers/Building_Images_With_Dockerfiles.md
CMD
et ENTRYPOINT
(automatically generated title slide)
Dans cette leçon, nous verrons deux instructions
importantes du Dockerfile
:
CMD
et ENTRYPOINT
.
Ces instructions nous permettent de déclarer la commande par défaut à lancer dans un conteneur.
containers/Cmd_And_Entrypoint.md
Quand quelqu'un lancera notre conteneur, nous voulons le saluer avec un sympathique bonjour, et une fonte spéciale.
Pour ça, nous allons lancer:
figlet -f script hello
-f script
dit à figlet d'utiliser une fonte spécifique.
hello
est le message que nous voulons afficher.
containers/Cmd_And_Entrypoint.md
CMD
à notre DockerfileNotre nouveau Dockerfile
aura cet aspect:
FROM ubuntuRUN apt-get updateRUN ["apt-get", "install", "figlet"]CMD figlet -f script hello
CMD
définit une commande par défaut à lancer quand aucune n'est précisée.
Elle peut apparaître n'importe où dans le fichier.
Chaque CMD
annulera et remplacera la précédente.
Par conséquent, même si vous pouvez utiliser plusieurs fois CMD
, c'est inutile au final.
containers/Cmd_And_Entrypoint.md
Essayons de lancer un build:
$ docker build -t figlet ....Successfully built 042dff3b4a8dSuccessfully tagged figlet:latest
Et de le lancer:
$ docker run figlet _ _ _ | | | | | | | | _ | | | | __ |/ \ |/ |/ |/ / \_| |_/|__/|__/|__/\__/
containers/Cmd_And_Entrypoint.md
CMD
Si nous voulons ouvrir un shell dans notre conteneur (au lieu de
lancer figlet
), il suffit de spécifier un autre programme à lancer:
$ docker run -it figlet bashroot@7ac86a641116:/#
On a indiqué bash
Il a remplacé la valeur de CMD
containers/Cmd_And_Entrypoint.md
ENTRYPOINT
Nous voulons être capable de spécifier un message différent en ligne de commande,
tout en gardant figlet
et quelques paramètres par défaut.
Autrement dit, on aimerait taper quelque chose comme:
$ docker run figlet salut _ | | , __, | | _|_ / \_/ | |/ | | | \/ \_/|_/|__/ \_/|_/|_/
Nous utiliserons pour ça l'instruction ENTRYPOINT
du Dockerfile.
containers/Cmd_And_Entrypoint.md
ENTRYPOINT
à notre DockerfileNotre nouveau Dockerfile aura cet aspect:
FROM ubuntuRUN apt-get updateRUN ["apt-get", "install", "figlet"]ENTRYPOINT ["figlet", "-f", "script"]
ENTRYPOINT
définit une commande de base (et ses paramètres) pour le conteneur.
Les arguments en ligne de commande sont ajoutés aux paramètres ci-dessus.
Tout comme CMD
, ENTRYPOINT
peut apparaître n'importe où, et remplacera la valeur précédente.
Pourquoi avoir utilisé la syntaxe JSON pour notre ENTRYPOINT
?
containers/Cmd_And_Entrypoint.md
Quand CMD ou ENTRYPOINT utilisent la syntaxe simple, ils sont encapsulés dans sh -c
Pour éviter ce comportement, on peut utiliser la syntaxe JSON.
Et qu'est-ce qu'il se passerait avec une syntaxe simple dans ENTRYPOINT
?
$ docker run figlet salut
On obtiendrait la commande suivante dans l'image figlet
:
sh -c "figlet -f script" salut
containers/Cmd_And_Entrypoint.md
Lançons un build:
$ docker build -t figlet ....Successfully built 36f588918d73Successfully tagged figlet:latest
Exécutons là:
$ docker run figlet salut _ | | , __, | | _|_ / \_/ | |/ | | | \/ \_/|_/|__/ \_/|_/|_/
containers/Cmd_And_Entrypoint.md
CMD
et ENTRYPOINT
Et si nous voulions définir un message par défaut pour notre conteneur?
Alors nous utiliserions ENTRYPOINT
et CMD
ensemble.
ENTRYPOINT
va définir la commande de base pour notre conteneur.
CMD
définira les paramètres par défaut pour cette commande.
Ils doivent tous les deux utiliser la syntaxe JSON.
containers/Cmd_And_Entrypoint.md
CMD
et ENTRYPOINT
ensembleNotre nouveau Dockerfile a cette tête:
FROM ubuntuRUN apt-get updateRUN ["apt-get", "install", "figlet"]ENTRYPOINT ["figlet", "-f", "script"]CMD ["hello world"]
ENTRYPOINT
définit une commande de base (et ses paramètres) pour le conteneur.
Si nous ne spécifions aucun argument supplémentaire au lancement du conteneur,
la valeur de CMD
y est ajoutée.
Autrement, nos arguments supplémentaires de ligne de commande remplacent CMD
.
containers/Cmd_And_Entrypoint.md
Lançons un build:
$ docker build -t myfiglet ....Successfully built 6e0b6a048a07Successfully tagged myfiglet:latest
Exécutons-là sans paramètres:
$ docker run myfiglet _ _ _ _ | | | | | | | | | | | _ | | | | __ __ ,_ | | __| |/ \ |/ |/ |/ / \_ | | |_/ \_/ | |/ / | | |_/|__/|__/|__/\__/ \/ \/ \__/ |_/|__/\_/|_/
containers/Cmd_And_Entrypoint.md
Maintenant, passons des arguments supplémentaires à l'image.
$ docker run myfiglet hola mundo _ _ | | | | | | | __ | | __, _ _ _ _ _ __| __ |/ \ / \_|/ / | / |/ |/ | | | / |/ | / | / \_| |_/\__/ |__/\_/|_/ | | |_/ \_/|_/ | |_/\_/|_/\__/
Nous avons surchargé CMD
tout en gardant ENTRYPOINT
.
containers/Cmd_And_Entrypoint.md
ENTRYPOINT
Et si nous voulons lancer un shell dans notre conteneur?
On ne peut pas juste taper docker run figlet bash
car
ça dirait juste à figlet d'afficher le mot "bash".
On utilise donc le paramètre --entrypoint
:
$ docker run -it --entrypoint bash myfigletroot@6027e44e2955:/#
containers/Cmd_And_Entrypoint.md
Copier des fichiers pendant le build
(automatically generated title slide)
Jusqu'ici, nous avons installé des choses dans nos images de conteneurs en téléchargeant des paquets.
Nous pouvons aussi copier des fichiers depuis le build context vers le conteneur que nous générons.
Rappel: le build context est le dossier qui contient le Dockerfile.
Dans ce chapitre, nous apprendrons une nouvelle instruction du Dockerfile: COPY
.
containers/Copying_Files_During_Build.md
Nous voulons construire un conteneur qui compile un simple programme "Hello world" écrit en C.
Voici le programme, hello.c
:
int main () { puts("Hello, world!"); return 0;}
Ouvrons un nouveau dossier, et plaçons ce fichier à l'intérieur.
Nous écrirons ensuite le Dockerfile.
containers/Copying_Files_During_Build.md
Sur Debian et Ubuntu, le paquet build-essential
nous donnera un compilateur.
En l'installant, n'oubliez pas de spécifier l'option -y
, ou sinon le build échouera
(puisque cette phase ne peut pas être intéractive).
Puis nous allons utiliser COPY
pour placer le fichier source dans le conteneur.
FROM ubuntuRUN apt-get updateRUN apt-get install -y build-essentialCOPY hello.c /RUN make helloCMD /hello
Ecrivez ce Dockerfile.
containers/Copying_Files_During_Build.md
Créez les fichiers hello.c
et Dockerfile
dans le même dossier.
Lancer docker build -t hello .
dans ce dossier.
Lancer docker run hello
, vous devriez voir Hello, world!
.
Victoire!
containers/Copying_Files_During_Build.md
COPY
et le cache de buildLancez le build encore.
Maintenant, modifiez hello.c
et lancer le build encore.
Docker peut mettre en cache les étapes impliquant COPY
.
Ces étapes ne seront pas exécutées si les fichiers n'ont pas changé.
containers/Copying_Files_During_Build.md
On peut COPY
des dossiers complets en récursif.
D'anciens Dockerfiles peuvent aussi comporter l'instruction ADD
.
C'est similaire sauf qu'il peut aussi extraire des archives automatiquement.
Si nous voulions vraiment compiler le code C dans le conteneur, nous aurions:
copié le source dans un dossier différent, via l'instruction WORKDIR
.
ou mieux encore, utilisé l'image officielle gcc
.
containers/Copying_Files_During_Build.md
Réduire la taille de l'image
(automatically generated title slide)
Dans notre précédent exemple, notre image finale contenait:
notre programme hello
son code source
le compilateur
Seul le premier élément est strictement nécessaire.
Nous allons voir comment obtenir une image sans les composants superflus.
containers/Multi_Stage_Builds.md
RUN
?Que se passe-t-il si nous utilisons une des commandes suivantes?
RUN rm -rf ...
RUN apt-get remove ...
RUN make clean ...
RUN
?Que se passe-t-il si nous utilisons une des commandes suivantes?
RUN rm -rf ...
RUN apt-get remove ...
RUN make clean ...
Cela ajoute une couche qui supprime un tas de fichiers.
Mais les couches précédentes (qui ont ajouté les fichiers) existent toujours.
containers/Multi_Stage_Builds.md
En téléchargeant une image, tous les layers doivent être récupérés.
Instruction Dockerfile | Taille du layer | Taille de l'image |
---|---|---|
FROM ubuntu |
Taille de l'image de base | Taille de l'image de base |
... |
... | Somme de cette couche + toutes les précédentes |
RUN apt-get install somepackage |
Taille des fichiers ajoutés (e.g. qqes Mo) |
Somme de cette couche + toutes les couches précédentes |
... |
... | idem |
RUN apt-get remove somepackage |
Env. zéro (méta-données seules) |
Identique à la précédente |
En conséquence, RUN rm
ne réduit pas la taille de l'image, ni ne libère d'espace disque.
containers/Multi_Stage_Builds.md
Des techniques variées sont disponibles pour obtenir des images plus petites:
dégonflement de layers,
ajouter des binaires qui sont générés hors du Dockerfile,
aplatir l'image finale,
les builds multi-stage, à multiples étapes.
Passons-les en revue rapidement.
containers/Multi_Stage_Builds.md
Vous verrez souvent des Dockerfiles comme suit:
FROM ubuntuRUN apt-get update && apt-get install xxx && ... && apt-get remove xxx && ...
Ou la variante plus lisible:
FROM ubuntuRUN apt-get update \ && apt-get install xxx \ && ... \ && apt-get remove xxx \ && ...
Cette commande RUN
nous retourne une seule couche.
Les fichiers qui y sont ajoutés, puis supprimés dans le même layer, n'augmentent pas sa taille.
containers/Multi_Stage_Builds.md
Pour:
fonctionne sur toutes les versions de Docker
n'exige pas d'outils spéciaux
Contre:
pas très lisible
quelques fichiers inutiles pourraient subsister si le nettoyage n'était pas assez poussé
ce layer est coûteux (lent à générer)
containers/Multi_Stage_Builds.md
Cela résulte dans un Dockerfile qui ressemble à ça:
FROM ubuntuCOPY xxx /usr/local/bin
Bien sûr, cela suppose que le fichier xxx
existe déjà dans le build context.
Ce fichier doit exister avant de lancer docker build
.
Par exemple, il peut:
Voir par exemple l'image officielle busybox ou cette image busybox plus ancienne.
containers/Multi_Stage_Builds.md
Pour:
Contre:
exige un outil de build supplémentaire
nous retombons dans l'enfer des dépendances et le "ça-marche-sur-mon-poste"
Contre, si le binaire est ajouté au dépôt de code:
brise la portabilité entre différentes plate-formes
augmente largement la taille du dépôt si le binaire est souvent mis à jour
containers/Multi_Stage_Builds.md
L'idée est de transformer l'image finale en une image à un seul layer.
Cela peut être réalisé de deux manières (au moins).
docker image build --squash ...
docker build -t temp-image .docker run --entrypoint true --name temp-container temp-imagedocker export temp-container | docker import - final-imagedocker rm temp-containerdocker rmi temp-image
containers/Multi_Stage_Builds.md
Pour:
les images à layer unique sont plus légères et rapides à télécharger
les fichiers supprimés ne prennent plus de place ni de ressources réseau.
Contre:
nous devons quand même activement supprimer les fichiers inutiles;
aplatir une image peut prendre beaucoup de temps (pour les plus grosses images)
aplatir est une opération qui annule le cache
(ne changer ne serait-ce qu'un petit fichier, et toute l'image doit être aplatie de nouveau)
containers/Multi_Stage_Builds.md
Un build multi-stage nous permet d'indiquer plusieurs étapes d'images.
Chaque étape constitue une image séparée, et peut copier les fichiers des images précédentes.
Nous allons voir comment ça marche plus en détail.
containers/Multi_Stage_Builds.md
Builds multi-stage
(automatically generated title slide)
A tout moment dans notre Dockerfile
, nous pouvons ajouter une ligne FROM
.
Cette ligne démarre une nouvelle étape dans notre build.
Chaque étape peut accéder aux fichiers des étapes précédentes avec COPY --from=...
.
Quand un build est étiqueté (avec docker build -t ...
), c'est la dernière étape qui récupère le tag.
Les étapes précédentes ne sont pas supprimées, elle seront utilisées par le cache, et peuvent être référencées.
containers/Multi_Stage_Builds.md
Chaque étape est numérotée, en débutant à 0
Nous pouvons copier un fichier d'une étape précédente en indiquant son numéro, par ex.:
COPY --from=0 /fichier/depuis/etape/une /chemin/dans/etape/courante
Nous pouvons aussi nommer les étapes, et y faire référence:
FROM golang AS builderRUN ...FROM alpineCOPY --from=builder /go/bin/mylittlebinary /usr/local/bin/
containers/Multi_Stage_Builds.md
Nous allons changer notre Dockerfile pour:
donner un surnom à notre première étape: compiler
ajouter une seconde étape utilisant la même image de base ubuntu
ajouter le binaire hello
à la seconde étape
vérifier que CMD
est dans la seconde étape
Le Dockerfile résultant est dans la prochaine diapo.
containers/Multi_Stage_Builds.md
Dockerfile
du build multi-stageVoici le Dockerfile final:
FROM ubuntu AS compilerRUN apt-get updateRUN apt-get install -y build-essentialCOPY hello.c /RUN make helloFROM ubuntuCOPY --from=compiler /hello /helloCMD /hello
Essayons de le générer, et vérifions que cela fonctionne bien:
docker build -t hellomultistage .docker run hellomultistage
containers/Multi_Stage_Builds.md
Listez nos images avec docker images
, et vérifiez la taille de:
l'image de base ubuntu
,
l'image hello
à étape unique,
l'image hellomultistage
à plusieurs étapes.
Nous pouvons arriver à des tailles d'images encore plus petites avec des images de base plus petites.
Toutefois, si nous utilisons une image de base commune (par ex. en prenant comme standard ubuntu
), ces images en commun ne seront téléchargées qu'une fois par hôte, les rendant
virtuellement "gratuites".
containers/Multi_Stage_Builds.md
On peut aussi étiqueter une étape intermédiaire avec docker build --target STAGE --tag NAME
Cela va créer une image (appelée NAME
) correspondant à l'étape STAGE
Elle peut être utilisée pour accéder facilement à une étape intermédiaire pour inspection.
(Au lieu de fouiller l'affichage de docker build
pour trouver l'ID d'image)
Elle peut aussi être utilisée pour générer de multiples images avec un seul Dockerfile
(Au lieu d'utiliser plusieurs Dockerfiles, qu'il faudrait perpétuellement synchroniser)
containers/Multi_Stage_Builds.md
Publier des images sur le Docker Hub
(automatically generated title slide)
Nous avons généré nos premières images.
Nous pouvons maintenant les publier vers le Docker Hub!
Vous n'avez pas à faire les exercices de cette section, parce qu'ils exigent un compte sur le Docker Hub, et nous ne voulons forcer personne à en créer un.
Veuillez noter, toutefois, que la création d'un compte sur le Docker Hub est gratuite (sans carte bancaire), et que l'hébergement d'images publiques est aussi gratuit.
containers/Publishing_To_Docker_Hub.md
docker login
Depuis Docker sur Mac/Windows, ou Docker sur un
poste de travail Linux, on peut (et on préfére si possible)
s'intégrer avec le trousseau de clés du système pour stocker
les accès en sécurité. Toutefois, sur la plupart des serveurs
Linux, ce sera stocké par défaut dans ~/.docker/config
.
containers/Publishing_To_Docker_Hub.md
Docker et ses tags d'images sont comme Git et ses tags/branches.
Ce sont des pointeurs vers un ID d'image spécifique.
Marquer une image ne renomme pas cette image: elle ne fait qu'ajouter une étiquette.
En poussant une image vers un registre distant, l'adresse de registre est dans le tag.
Example: registry.example.net:5000/image
Qu'en est-il des images du Docker Hub?
Docker et ses tags d'images sont comme Git et ses tags/branches.
Ce sont des pointeurs vers un ID d'image spécifique.
Marquer une image ne renomme pas cette image: elle ne fait qu'ajouter une étiquette.
En poussant une image vers un registre distant, l'adresse de registre est dans le tag.
Example: registry.example.net:5000/image
Qu'en est-il des images du Docker Hub?
jpetazzo/clock
est, en fait, index.docker.io/jpetazzo/clock
ubuntu
est, en fait, library/ubuntu
, i.e. index.docker.io/library/ubuntu
containers/Publishing_To_Docker_Hub.md
Ajoutons une étiquette à notre image figlet
(ou une autre):
docker tag figlet jpetazzo/figlet
Et poussons-là sur le Hub:
docker push jpetazzo/figlet
C'est tout!
Ajoutons une étiquette à notre image figlet
(ou une autre):
docker tag figlet jpetazzo/figlet
Et poussons-là sur le Hub:
docker push jpetazzo/figlet
C'est tout!
N'importe qui peut maintenant docker run jpetazzo/figlet
de partout.
containers/Publishing_To_Docker_Hub.md
Vous pouvez lier un dépôt du Docker Hub avec un dépôt Github ou BitBucket.
Chaque push dans Github/Bitbucket va déclencher un build sur Docker Hub.
Si l'image est générée avec succès, elle sera disponible sur Docker Hub.
Vous pouvez associer tags et branches entre le code source et images de conteneurs.
Si vous maintenez des dépôts publics, tout ça est gratuit.
containers/Publishing_To_Docker_Hub.md
/www
dans la colonne "Build context" (ou le dossier qui contient le Dockerfile).containers/Publishing_To_Docker_Hub.md
Astuces pour Dockerfiles efficaces
(automatically generated title slide)
Nous allons voir comment:
réduire le nombre de layers.
Exploiter le cache de build pour accélérer la construction.
Injecter les tests unitaires dans le processus de génération.
Chaque ligne du Dockerfile ajoute une nouvelle couche.
Ecrivez votre Dockerfile pour exploiter le système de cache de Docker.
Combinez les commandes avec &&
pour chainer les commandes et \
pour empiler les lignes.
Note: il est fréquent d'écrire un Dockerfile ligne par ligne:
RUN apt-get install thisthingRUN apt-get install andthatthing andthatotheroneRUN apt-get install somemorestuff
Puis le corriger très facilement avant déploiement:
RUN apt-get install thisthing andthatthing andthatotherone somemorestuff
Problème classique de Dockerfile:
"chaque fois que je change une ligne de code, toutes mes dépendances sont ré-installées!"
Solution: COPY
-er les listes de dépendances (packages.json
, requirements.txt
, etc.)
à part pour éviter de ré-installer des dépendances inchangées chaque fois.
Dockerfile
Les dépendances sont ré-installées chaque fois, car le système de build ne sait pas si requirements.txt
a été mis à jour.
FROM pythonWORKDIR /srcCOPY . .RUN pip install -qr requirements.txtEXPOSE 5000CMD ["python", "app.py"]
Dockerfile
Ajouter les dépendances dans une étape à part permet à Docker un cache plus efficace, car il ne les installera que si requirements.txt
change.
FROM pythonCOPY requirements.txt /tmp/requirements.txtRUN pip install -qr /tmp/requirements.txtWORKDIR /srcCOPY . .EXPOSE 5000CMD ["python", "app.py"]
FROM <baseimage>RUN <install dependencies>COPY <code>RUN <build code>RUN <install test dependencies>COPY <test data sets and fixtures>RUN <unit tests>FROM <baseimage>RUN <install dependencies>COPY <code>RUN <build code>CMD, EXPOSE ...
RUN <unit tests>
échoue, le build ne produira aucune imageExemples de Dockerfile
(automatically generated title slide)
Il y a quelques astuces, conseils et techniques qu'on peut appliquer dans nos Dockerfiles.
Mais parfois, on se doit de passer par des formes différentes, voire opposées, selon:
la complexité du projet,
le langage de programmation ou le framework choisi,
l'étape du projet (nouveau MVP vs prod super-stable),
si nous générons une image finale, ou une base pour d'autres images,
etc.
Nous allons montrer quelques exemples de techniques très différentes.
Au moment d'écrire des images officielles, c'est une bonne idée de réduire au maximum:
le nombre de couches,
la taille finale de l'image.
C'est souvent au détriment du temps de génération et du confort pour le mainteneur de l'image; mais quand une image est téléchargée des millions de fois, économiser ne serait-ce qu'une poignée de secondes de délai vaut le coup.
RUN apt-get update && apt-get install -y libpng12-dev libjpeg-dev && rm -rf /var/lib/apt/lists/* \ && docker-php-ext-configure gd --with-png-dir=/usr --with-jpeg-dir=/usr \ && docker-php-ext-install gd...RUN curl -o wordpress.tar.gz -SL https://wordpress.org/wordpress-${WORDPRESS_UPSTREAM_VERSION}.tar.gz \ && echo "$WORDPRESS_SHA1 *wordpress.tar.gz" | sha1sum -c - \ && tar -xzf wordpress.tar.gz -C /usr/src/ \ && rm wordpress.tar.gz \ && chown -R www-data:www-data /usr/src/wordpress
(Source: Image officielle Wordpress)
Parfois, il est préférable de prioriser le confort du mainteneur
En particulier, si:
l'image change beaucoup,
l'image a peu d'utilisateurs (par ex. 1 seul, le mainteneur!),
l'image est générée et lancée sur la même machine,
l'image est générée et lancée sur des machines sur un réseau très rapide...
Dans ces cas, mieux vaut garder les choses simples!
(Prochaine diapo: un Dockerfile qui peut être utilisé pour un aperçu de site Jekyll / github pages)
FROM debian:sidRUN apt-get update -qRUN apt-get install -yq build-essential makeRUN apt-get install -yq zlib1g-devRUN apt-get install -yq ruby ruby-devRUN apt-get install -yq python-pygmentsRUN apt-get install -yq nodejsRUN apt-get install -yq cmakeRUN gem install --no-rdoc --no-ri github-pagesCOPY . /blogWORKDIR /blogVOLUME /blog/_siteEXPOSE 4000CMD ["jekyll", "serve", "--host", "0.0.0.0", "--incremental"]
Un tag d'image peut indiquer une version de l'image.
Mais parfois, plusieurs composants importants co-existent, et nous devons indiquer les versions de chacun.
C'est possible en passant par des variables d'environnement:
ENV PIP=9.0.3 \ ZC_BUILDOUT=2.11.2 \ SETUPTOOLS=38.7.0 \ PLONE_MAJOR=5.1 \ PLONE_VERSION=5.1.0 \ PLONE_MD5=76dc6cfc1c749d763c32fff3a9870d8d
(Source: Image officielle Plone)
Il est très courant de définir un entrypoint spécifique.
Ce point d'entrée est généralement un script, réalisant une série d'opérations telles que:
vérifications avant démarrage (si une dépendance obligatoire n'est pas disponible, afficher un message d'erreur sympa au lieu d'un obscur paquet de lignes dans un fichier log);
génération ou validation de fichier de configuration;
limiter les privilèges (avec par ex. su
ou gosu
, parfois combiné avec chown
);
et plus encore.
#!/bin/sh set -e # first arg is '-f' or '--some-option' # or first arg is 'something.conf' if [ "${1#-}" != "$1" ] || [ "${1%.conf}" != "$1" ]; then set -- redis-server "$@" fi # allow the container to be started with '--user' if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then chown -R redis . exec su-exec redis "$0" "$@" fi exec "$@"
(Source: Image officielle Redis)
Pour faciliter la maintenance (et éviter les erreurs humaines), évitez de répéter des informations comme:
numéros de versions,
URLs de ressources distantes (par ex. fichiers tarballs) ...
Pour ce faire, utilisez des variables d'environnement.
ENV NODE_VERSION 10.2.1...RUN ... && curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION.tar.xz" \ && curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \ && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \ && grep " node-v$NODE_VERSION.tar.xz\$" SHASUMS256.txt | sha256sum -c - \ && tar -xf "node-v$NODE_VERSION.tar.xz" \ && cd "node-v$NODE_VERSION" \...
(Source: Image officielle Nodejs)
En théorie, les images de production et développement devraient être les mêmes.
En pratique, nous avons souvent besoin d'activer des comportements spécifiques en développement (par ex. trace de debogage).
Une façon de concilier les deux besoins est d'utiliser Compose pour activer ces comportements.
Jetons un oeil à l'appli de démo trainingwheels comme exemple.
Le Dockerfile génère une image exploitant gunicorn:
FROM pythonRUN pip install flaskRUN pip install gunicornRUN pip install redisCOPY . /srcWORKDIR /srcCMD gunicorn --bind 0.0.0.0:5000 --workers 10 counter:appEXPOSE 5000
(Source: Dockerfile trainingwheels)
Ce fichier Compose utilise la même image, mais avec quelques valeurs surchargées en développement:
On préfère le serveur Flask de développement (surcharge de CMD
);
On définit la variable d'environnement DEBUG
;
On utilise un volume pour fournir un processus de développement local plus rapide.
services: www: build: www ports: - 8000:5000 user: nobody environment: DEBUG: 1 command: python counter.py volumes: - ./www:/src
(Source: Fichier Compose trainingwheels)
Le but principal des conteneurs est de rendre notre vie meilleure;
Dans ce chapitre, nous avons montré bien des façons d'écrire des Dockerfiles;
Ces Dockerfiles utilisent parfois des techniques diamétralement opposées;
Et pourtant, c'était la "bonne" technique pour cette situation spécifique;
C'est bien (et souvent encouragé) de commencer simple et d'évoluer selon le besoin;
N'hésitez pas à revoir ce chapitre plus tard (après quelques Dockerfiles) pour inspiration!
Dockerfiles avancés
(automatically generated title slide)
Nous avons vu des Dockerfiles simples pour illustrer comment Docker construit des images de conteneurs.
Dans cette section, nous allons voir d'autres commandes propres aux Dockerfiles.
containers/Advanced_Dockerfiles.md
Dockerfile
, l'essentielLes instructions d'un Dockerfile
sont exécutées dans l'ordre.
Chaque instruction ajoute une nouvelle couche à l'image (layer).
Docker gère un cache avec les layers des builds précédents.
Quand rien ne change dans les instructions ou les fichiers qui définissent un layer, le builder récupère la version en cache, sans exécuter l'instruction de ce layer.
L'instruction FROM
DOIT être la première instruction (hormis les commentaires).
Les lignes débutant par #
sont considérées comme des commentaires.
Quelques instructions (comme CMD
et ENTRYPOINT
) concernent les méta-données.
(avec pour conséquence que chaque mention de ces instructions rend les précédentes obsolètes)
containers/Advanced_Dockerfiles.md
RUN
L'instruction RUN
peut être utilisée de deux manières.
Via le shell wrapping, qui exécute la commande spécifiée dans un shell,
avec /bin/sh -c
, exemple:
RUN apt-get update
Ou via la méthode exec
, qui évite l'expansion de chaine du shell, et
permet l'exécution pour les images qui n'embarquent pas /bin/sh
, i.e. :
RUN [ "apt-get", "update" ]
containers/Advanced_Dockerfiles.md
RUN
plus en détailRUN
est utile pour:
RUN
n'est pas fait pour:
Si vous voulez démarrer automatiquement un processus quand le container se lance,
vous devriez passer par CMD
et/ou ENTRYPOINT
.
containers/Advanced_Dockerfiles.md
Il est possible d'exécuter plusieurs commandes d'un seul coup:
RUN apt-get update && apt-get install -y wget && apt-get clean
Il est aussi possible de répartir une même commande sur plusieurs lignes:
RUN apt-get update \ && apt-get install -y wget \ && apt-get clean
containers/Advanced_Dockerfiles.md
EXPOSE
L'instruction EXPOSE
indique à Docker quels ports doivent être publiés
pour cette image.
EXPOSE 8080EXPOSE 80 443EXPOSE 53/tcp 53/udp
Tous les ports sont privés par défaut;
Déclarer un port avec EXPOSE
ne suffit pas à le rendre public;
Le Dockerfile
ne contrôle pas sur quel port un service sera exposé.
containers/Advanced_Dockerfiles.md
Quand vous lancez docker run -p <port> ...
, ce port devient public;
(Même s'il n'a pas été déclaré via EXPOSE
.)
Quand vous lancez docker run -P...
(sans numéro de port), tous les ports
déclarés via EXPOSE
deviennent publics.
Un port public est accessible depuis les autres containers et depuis l'extérieur de la machine hôte.
Un port privé n'est pas accessible depuis l'extérieur.
containers/Advanced_Dockerfiles.md
COPY
L'instruction COPY
ajoute des fichiers et du contenu depuis votre machine hôte
vers l'image.
COPY . /src
Cela va ajouter le contenu du build context (le dossier passé en argument
à la commande docker build
) au dossier /src
dans l'image.
containers/Advanced_Dockerfiles.md
Note: vous pouvez manipuler uniquement les fichiers et dossier contenus dans le build context. Tout chemin absolu est traité comme ayant pour racine le build context, i.e que les 2 lignes suivantes sont équivalentes:
COPY . /srcCOPY / /src
Toute tentative d'utiliser ..
pour sortir du build context sera
détectée et bloquée par Docker, et le build échouera.
Sans cela, un Dockerfile
pourrait être valide sur une machine A, mais échouer sur une machine B.
containers/Advanced_Dockerfiles.md
ADD
ADD
fonctionne presque comme COPY
, mais avec quelques petits plus.
ADD
peut récupérer des fichiers à distance:
ADD http://www.example.com/webapp.jar /opt/
Cette ligne irait télécharger le fichier webapp.jar
pour le placer
dans le dossier /opt
.
ADD
va automatiquement décompresser les fichiers zip et tar:
ADD ./assets.zip /var/www/htdocs/assets/
Cette ligne décompresse assets.zip
pour copier son contenu dans /var/www/htdocs/assets
.
Néanmoins, ADD
ne décompressera pas automatiquement les fichiers téléchargés à distance.
containers/Advanced_Dockerfiles.md
ADD
, COPY
, et le cache de buildAvant d'ajouter un nouveau layer, Docker vérifie son cache de build.
Pour la plupart des instructions Dockerfile
, Docker examine simplement le contenu
du Dockerfile pour la vérification du cache.
Pour les instructions ADD
et COPY
, Docker vérifie aussi si les fichiers
à ajouter à l'image ont été modifiés.
ADD
doit toujours télécharger tout fichier distant avant de vérifier
s'il a changé.
(Il ne sait pas utiliser par ex. les en-têtes ETags ou If-Modified-Since)
containers/Advanced_Dockerfiles.md
VOLUME
L'instruction VOLUME
indique à Docker qu'un dossier spécifique devrait
être un volume.
VOLUME /var/lib/mysql
Les accès filesystem dans les volumes contournent la couche copy-on-write, offrant une performance native dans les I/O de ses dossiers.
Les volumes peuvent être attachés à plusieurs containers, permettant de "transporter" les données d'un container à un autre, dans le cas par ex. d'une mise à jour de base de données vers une version plus récente.
Il est possible de démarrer un container en mode "lecture seule". Le filesystem du container sera restreint en mode "lecture seule", mais tout volume restera en lecture/écriture si nécessaire.
containers/Advanced_Dockerfiles.md
WORKDIR
L'instruction WORKDIR
change le dossier en cours
pour les instructions suivantes.
Cela affecte aussi CMD
et ENTRYPOINT
, puisque cela modifie
le dossier de démarrage quand un container se lance.
WORKDIR /src
Vous pouvez spécifier plusieurs WORKDIR
pour changer de dossier au cours
des différentes opérations.
containers/Advanced_Dockerfiles.md
ENV
L'instruction ENV
déclare des variables d'environnement qui devraient être
affectées dans tout container lancé depuis cette image.
ENV WEBAPP_PORT 8080
Ceci a pour résultat de créer une variable d'environnement dans tout container provenant de cette image.
WEBAPP_PORT=8080
Vous pouvez aussi spécifier des variables d'environnement via docker run.
$ docker run -e WEBAPP_PORT=8000 -e WEBAPP_HOST=www.example.com ...
containers/Advanced_Dockerfiles.md
USER
L'instruction USER
change l'utilisateur ou l'UID à utiliser pour la suite des opérations,
mais aussi l'utilisateur au lancement du container.
Comme WORKDIR
, elle peut être utilisée plusieurs fois, par ex.
pour repasser à root
ou un autre utilisateur.
containers/Advanced_Dockerfiles.md
CMD
L'instruction CMD
est la commande par défaut qui se lance quand un
container est instancié à partir d'une image.
CMD [ "nginx", "-g", "daemon off;" ]
Ceci signifie que nous n'avons pas besoin de spécifier nginx -g "daemon off;"
lors du lancement de notre container.
Au lieu de:
$ docker run <dockerhubUsername>/web_image nginx -g "daemon off;"
Nous pouvons juste écrire:
$ docker run <dockerhubUsername>/web_image
containers/Advanced_Dockerfiles.md
CMD
plus en détailTout comme RUN
, l'instruction CMD
existe sous deux formes.
La première lance un shell:
CMD nginx -g "daemon off;"
La seconde s'exécute directement, sans passer par un shell:
CMD [ "nginx", "-g", "daemon off;" ]
containers/Advanced_Dockerfiles.md
CMD
CMD
peut être forcé au lancement d'un container.
$ docker run -it <dockerhubUsername>/web_image bash
Ceci lancera bash
au lieu de nginx -g "daemon off;"
.
containers/Advanced_Dockerfiles.md
ENTRYPOINT
L'instruction ENTRYPOINT
ressemble à l'instruction CMD
, sauf
que les arguments passés en ligne de commande sont ajoutés au point d'entrée.
Note: vous devez utiliser pour cela la syntaxe "exec" (["..."]
).
ENTRYPOINT [ "/bin/ls" ]
Avec ceci, si nous lançons la commande:
$ docker run training/ls -l
Au lieu d'essayer de lancer -l
, le container va exécuter /bin/ls -l
containers/Advanced_Dockerfiles.md
ENTRYPOINT
Le point d'entrée peut aussi être redéfini.
$ docker run -it training/lsbin dev home lib64 mnt proc run srv tmp varboot etc lib media opt root sbin sys usr$ docker run -it --entrypoint bash training/lsroot@d902fb7b1fc7:/#
containers/Advanced_Dockerfiles.md
CMD
et ENTRYPOINT
interagissentLes instructions CMD
et ENTRYPOINT
fonctionnent mieux
quand elles sont définies ensemble.
ENTRYPOINT [ "nginx" ]CMD [ "-g", "daemon off;" ]
La ligne ENTRYPOINT
spécifie la command à lancer et la ligne CMD
spécifie ses options. En ligne de commande, nous pourrons donc
potentiellement surcharger les options le cas échéant.
$ docker run -d <dockerhubUsername>/web_image -t
Cela surchargera les options CMD
avec de nouvelles valeurs.
containers/Advanced_Dockerfiles.md
ONBUILD
vous permet de cacher des commandes qui ne seront
executées que quand cette image servira de base à une autre.LABEL
ajoute des meta-datas libres à l'image.ARG
déclare des variables de build (optionelles ou obligatoires).STOPSIGNAL
indique le signal à envoyer lors d'un docker stop
(TERM
par défault).HEALTHCHECK
définit une commande de test vérifiant le statut d'un container.SHELL
choisit le programme par défaut pour la forme string de RUN, CMD, etc.containers/Advanced_Dockerfiles.md
ONBUILD
L'instruction ONBUILD
est un déclencheur. Elle indique les commandes à
exécuter quand une image se base sur l'image en cours de build.
Ceci est utile pour construire des images qui seront une base pour d'autres images.
ONBUILD COPY . /src
ONBUILD
.ONBUILD
ne peut être utilisé pour déclencher des instructions FROM
containers/Advanced_Dockerfiles.md
Bases du réseau pour conteneur
(automatically generated title slide)
Nous allons maintenant lancer des services connectés (acceptant des requêtes) dans des conteneurs.
À la fin de cette section, vous serez capable de:
Lancer un service connecté dans un conteneur;
Manipuler les bases du réseau pour conteneur;
Trouver l'adresse IP d'un conteneur.
Nous expliquerons aussi les différents modèles de réseau usités par Docker.
containers/Container_Networking_Basics.md
Lancer l'image nginx
du Docker Hub, qui contient un serveur web basique:
$ docker run -d -P nginx66b1ce719198711292c8f34f84a7b68c3876cf9f67015e752b94e189d35a204e
Docker va télécharger l'image depuis le Docker Hub.
-d
dit à Docker de lancer une image en tâche de fond.
-P
dit à Docker de rendre ce service disponible depuis d'autres serveurs.
(-P
est la version courte de --publish-all
)
Mais, comment on se connecte à notre serveur web maintenant?
containers/Container_Networking_Basics.md
Nous allons utiliser docker ps
:
$ docker psCONTAINER ID IMAGE ... PORTS ...e40ffb406c9e nginx ... 0.0.0.0:32768->80/tcp ...
Le serveur web tourne sur le port 80 à l'intérieur du conteneur.
Ce port correspond au port 32768 sur notre hôte Docker.
Nous expliquerons les pourquoi et comment de ce mappage.
Mais d'abord, assurons-nous que tout fonctionne correctement.
containers/Container_Networking_Basics.md
Pointer votre navigateur à l'adresse IP de votre hôte Docker, sur le port
affiché par docker ps
, correspondant au port 80 du conteneur.
containers/Container_Networking_Basics.md
Vous pouvez aussi utiliser curl
directement depuis le hôte Docker.
Assurez-vous d'utiliser le bon numéro de port s'il est différent de notre exemple ci-dessous:
$ curl localhost:32768<!DOCTYPE html><html><head><title>Welcome to nginx!</title>...
containers/Container_Networking_Basics.md
Il y a des meta-données dans l'image indiquant "cette image fait tourner quelque chose sur le port 80"
On peut examiner ces meta-donnéees avec docker inspect
:
$ docker inspect --format '{{.Config.ExposedPorts}}' nginxmap[80/tcp:{}]
Cette méta-donnée a pour origine le Dockerfile, via le mot-clé EXPOSE
.
On peut le constater avec docker history
:
$ docker history nginxIMAGE CREATED CREATED BY7f70b30f2cc6 11 days ago /bin/sh -c #(nop) CMD ["nginx" "-g" "…<missing> 11 days ago /bin/sh -c #(nop) STOPSIGNAL [SIGTERM]<missing> 11 days ago /bin/sh -c #(nop) EXPOSE 80/tcp
containers/Container_Networking_Basics.md
Nous n'avons plus d'adresses IPv4.
Les conteneurs ne peuvent pas avoir d'adresse IPv4 publiques.
Ils possèdent des adresses privées.
Les services doivent être exposés port par port.
Le mappage de ports est obligatoire pour éviter les conflits.
containers/Container_Networking_Basics.md
Manipuler la sortie de docker ps
serait fastidieux.
Il y a une commande pour nous aider:
$ docker port <containerID> 8032768
containers/Container_Networking_Basics.md
Si vous voulez allouer vous-même les numéros de port, aucun souci:
$ docker run -d -p 80:80 nginx$ docker run -d -p 8000:80 nginx$ docker run -d -p 8080:80 -p 8888:80 nginx
Note: la convention est port-du-hôte:port-du-conteneur
.
containers/Container_Networking_Basics.md
On peut intégrer les conteneurs au réseau de bien des manières.
Démarrer le conteneur, pour laisser Docker lui allouer un port public.
Puis lire le port affecté et l'injecter dans votre configuration.
Choisir un numéro de port à l'avance, au moment de générer votre configuration.
Puis démarrer votre conteneur en forçant les ports à la main.
Utiliser un plugin de réseau, pour brancher vos conteneurs sur des VLANs, tunnels, etc.
Activer le Mode Swarm pour un déploiement à travers un cluster.
Le conteneur sera accessible depuis n'importe quel noeud du cluster.
En utilisant Docker à travers une couche de gestion supplémentaire comme Mesos ou Kubernetes, ils fournissent en général leurs propres mécanismes d'exposition de conteneurs.
containers/Container_Networking_Basics.md
Nous pouvons utiliser la commande docker inspect
pour trouver l'adresse IP
de notre conteneur.
$ docker inspect --format '{{ .NetworkSettings.IPAddress }}' <yourContainerID>172.17.0.3
docker inspect
est une commande avancée, qui peut retourner une tonne
d'informations à propos des conteneurs.
Ici, nous lui fournissons une chaîne pour extraire exactement l'adresse IP du conteneur.
containers/Container_Networking_Basics.md
Nous pouvons tester la connectivité du conteneur via l'adresse IP
déterminée précédemment. Voyons ceci avec l'outil ping
.
$ ping <ipAddress>64 bytes from <ipAddress>: icmp_req=1 ttl=64 time=0.085 ms64 bytes from <ipAddress>: icmp_req=2 ttl=64 time=0.085 ms64 bytes from <ipAddress>: icmp_req=3 ttl=64 time=0.085 ms
containers/Container_Networking_Basics.md
Nous avons appris comment:
Exposer un port sur le réseau;
Manipuler les bases du réseau pour conteneur;
Trouver une adresse IP de conteneur.
Dans le chapitre suivant, nous verrons comment connecter les conteneurs entre eux, sans publier leurs ports.
containers/Container_Networking_Basics.md
Pilote réseau pour conteneur
(automatically generated title slide)
Le Docker Engine prend en charge de nombreux pilotes réseau.
Certains pilotes sont inclus à l'installation:
bridge
(par défaut)
none
host
container
Le pilote est indiqué avec docker run --net ...
.
Les différents pilotes sont expliqués en détail dans les diapos suivantes.
Par défaut, le conteneur dispose d'une interface eth0
virtuelle.
(En supplément de lo
, sa propre interface de boucle interne).
Cette interface est fournie par une paire veth
.
Elle est connectée au Docker bridge.
(Appelé docker0
par défaut; configurable avec --bridge
.)
L'allocation d'adresses IP se fait sur un sous-réseau privé interne.
(Docker utilise 172.17.0.0/16 par défaut; configurable avec --bip
.)
Le trafic sortant passe à travers une règle iptables MASQUERADE.
Le trafic entrant passe à travers une règle iptables DNAT.
Le conteneur peut avoir ses propres routes, règles iptables, etc.
On démarre le conteneur avec docker run --net none ...
Il n'aura que l'interface de bouclage lo
. Pas de eth0
.
Il ne peut ni recevoir ni envoyer de trafic réseau.
Utile pour les logiciels isolés/suspects.
On démarre le conteneur avec docker run --net host ...
Il voit (et peut accéder) aux interfaces réseau de l'hôte.
Il peut ouvrir n'importe quelle interface et port (pour le meilleur et pour le pire).
Le trafic réseau se passe des couches NAT, bridge ou veth.
Performance = native!
Cas d'usage:
Applications sensibles à la performance (VOIP, jeu-vidéo, streaming...)
découvertes d'homologue (par ex. mappage de port Erlang, Raft, Serf ...)
On démarre le conteneur avec docker run --net container:id ...
Il recycle la pile réseau d'un autre conteneur.
Il partage avec l'autre conteneur les mêmes interfaces, adresses IP, routes, règles iptables, etc.
Ces conteneurs peuvent communiquer à travers leur interface lo
.
(i.e. l'un peut s'attacher à 127.0.0.1 et les autres peuvent s'y connecter.)
Le Container Network Model
(automatically generated title slide)
Nous aborderons le CNM (Modèle de Réseau pour Container)
A la fin de la leçon, vous serez capable de:
Créer un réseau privé pour un groupe de containers;
Utiliser le nommage de container pour connecter les services ensemble;
Connecter et déconnecter dynamiquement des containers à des réseaux;
Affecter l'adresse IP à un container.
Nous expliquerons aussi le principe des réseaux overlay et des plugins de réseau.
containers/Container_Network_Model.md
Le CNM a été introduit dans Engine 1.9.0 (Novembre 2015).
Le CNM ajoute la notion de network, et une commande principale pour manipuler et inspecter ces réseaux: docker network
.
$ docker network lsNETWORK ID NAME DRIVER6bde79dfcf70 bridge bridge8d9c78725538 none nulleb0eeab782f4 host host4c1ff84d6d3f blog-dev overlay228a4355d548 blog-prod overlay
containers/Container_Network_Model.md
Dans le concept, un réseau est un switch virtuel;
Il peut être local (dans un Engine simple) ou global (transversal à plusieurs hôtes);
Un réseau possède un sous-réseau IP associé;
Docker va affecter de nouvelles adresses IP aux containers connectés à ce réseau;
Des containers peuvent être connectés à plusieurs réseaux;
Des containers peuvent se voir affecté des noms et alias par réseau;
Les noms et alias sont résolus via un serveur DNS embarqué.
containers/Container_Network_Model.md
Un réseau est géré par un driver.
Les drivers inclus par défaut:
bridge
(par défaut)none
host
macvlan
Un driver multi-hôte, overlay, est inclus sans installation supplémentaire (pour les clusters Swarm).
Des drivers supplémentaires sont disponibles sous forme de plugins (OVS, VLAN, etc)
Un réseau peut avoir son propre IPAM (allocation d'IP)
containers/Container_Network_Model.md
CNI = Container Network Interface
CNI est utilisé en particulier par Kubernetes
Dans CNI, toutes les nodes et containers sont sur un seul et même réseau IP
CNI et CNM offrent les mêmes fonctions, mais via des méthodes très différentes
containers/Container_Network_Model.md
Essayons de déclarer un nouveau réseau appelé dev
.
$ docker network create dev4c1ff84d6d3f1733d3e233ee039cac276f425a9d5228a4355d54878293a889ba
Le réseau est maintenant visible avec la commande network ls
;
$ docker network lsNETWORK ID NAME DRIVER6bde79dfcf70 bridge bridge8d9c78725538 none nulleb0eeab782f4 host host4c1ff84d6d3f dev bridge
containers/Container_Network_Model.md
Nous allons créer un container nommé sur ce réseau.
Il sera disponible via son nom, es
.
$ docker run -d --name es --net dev elasticsearch:28abb80e229ce8926c7223beb69699f5f34d6f1d438bfc5682db893e798046863
containers/Container_Network_Model.md
Et maintenant, ajoutons un autre container sur ce réseau.
$ docker run -ti --net dev alpine shroot@0ecccdfa45ef:/#
Depuis ce nouveau container, nous pouvons résoudre et ping l'autre, en utilisant son nom:
/ # ping esPING es (172.18.0.2) 56(84) bytes of data.64 bytes from es.dev (172.18.0.2): icmp_seq=1 ttl=64 time=0.221 ms64 bytes from es.dev (172.18.0.2): icmp_seq=2 ttl=64 time=0.114 ms64 bytes from es.dev (172.18.0.2): icmp_seq=3 ttl=64 time=0.114 ms^C--- es ping statistics ---3 packets transmitted, 3 received, 0% packet loss, time 2000msrtt min/avg/max/mdev = 0.114/0.149/0.221/0.052 msroot@0ecccdfa45ef:/#
containers/Container_Network_Model.md
Dans le Docker Engine 1.9, la résolution de nom est implémentée avec /etc/hosts
, et mise à jour chaque fois que les containers sont ajoutés/supprimés.
[root@0ecccdfa45ef /]# cat /etc/hosts172.18.0.3 0ecccdfa45ef127.0.0.1 localhost::1 localhost ip6-localhost ip6-loopbackfe00::0 ip6-localnetff00::0 ip6-mcastprefixff02::1 ip6-allnodesff02::2 ip6-allrouters172.18.0.2 es172.18.0.2 es.dev
Dans le Docker Engine 1.10, ceci a été remplacé par une résolution dynamique.
(Cela résoud les race conditions lors de la mise à jour de /etc/hosts
)
containers/Container_Network_Model.md
Service discovery avec les containers
(automatically generated title slide)
Essayons de lancer une application reposant sur deux containers;
Le premier container est un serveur web;
L'autre est une base de données redis;
Nous les placerons tous deux sur le réseau dev
créé auparavant;
containers/Container_Network_Model.md
L'application est fournie par l'image jpetazzo/trainingwheels
.
Nous en savons peu sur elle, donc nous la lançons et on verra ce qui arrivera!
Démarrer le container, en publiant tous ses ports:
$ docker run --net dev -d -P jpetazzo/trainingwheels
Vérifier quel port lui a été alloué:
$ docker ps -l
containers/Container_Network_Model.md
C'est parce que le service Redis n'est pas lancé.
Ce container essaie de résoudre le nom redis
.
Note: nous n'utilisons pas de FQDN ou une adresse IP ici; juste redis
.
containers/Container_Network_Model.md
Nous devons démarrer un container Redis.
Ce container doit être sur le même réseau que le serveur web.
Il doit porter le nom correct (redis
) pour que l'application le trouve.
Démarrer le container;
$ docker run --net dev --name redis -d redis
containers/Container_Network_Model.md
redis
, au lieu d'avoir une erreur DNS, on récupère l'adresse IP de notre container Redis.containers/Container_Network_Model.md
Et si nous voulions lancer plusieurs clones de notre application?
Puisque les noms sont uniques, il ne peut y avoir qu'un seul container nommé redis
.
Toutefois, nous pouvons forcer un nom de réseau de notre container avec --net-alias
.
--net-alias
a une portée par réseau, et indépendant du nom de container d'origine.
containers/Container_Network_Model.md
Supprimons le container redis
:
$ docker rm -f redis
Et ajoutons un nouveau qui ne bloque pas le nom redis
:
$ docker run --net dev --net-alias redis -d redis
Vérifier que l'appli fonctionne toujours (mais le compteur est revenu à 1, car on a dégagé l'ancien container Redis).
containers/Container_Network_Model.md
Essayons de ping notre container es
depuis un autre container, dans le cas où l'autre container n'est pas sur le réseau dev
$ docker run --rm alpine ping esping: bad address 'es'
Un nom est résolu uniquement quand les containers sont sur le même réseau.
Les containers peuvent se contacter les uns les autres seulement quand ils sont sur le même réseau (vous pouvez essayer de ping avec l'adresse IP pour vérifier).
containers/Container_Network_Model.md
Nous aimerions avoir un autre réseau, prod
avec son propre container es
. Mais il ne peut y avoir qu'un seul container nommé es
!
Nous utiliserons les alias de réseau.
Un container peut avoir plusieurs alias de réseau.
Les alias de réseau sont locaux à un réseau donné (qui existent juste sur ce réseau).
Plusieurs containers peuvent avoir le même alias de réseau (y compris sur le même réseau). Dans Docker Engine 1.11, la résolution d'un alias de réseau renvoie l'adresse IP de tous les containers disposant de cet alias.
containers/Container_Network_Model.md
Créez un réseau prod
.
$ docker network create prod5a41562fecf2d8f115bedc16865f7336232a04268bdf2bd816aecca01b68d50c
Nous pouvons maintenant créer plusieurs containers avec un alias es
sur le nouveau réseau prod
.
$ docker run -d --name prod-es-1 --net-alias es --net prod elasticsearch:238079d21caf0c5533a391700d9e9e920724e89200083df73211081c8a356d771$ docker run -d --name prod-es-2 --net-alias es --net prod elasticsearch:21820087a9c600f43159688050dcc164c298183e1d2e62d5694fd46b10ac3bc3d
containers/Container_Network_Model.md
Essayons la résolution DNS, en utilisant l'outil nslookup
livré dans l'image alpine
.
$ docker run --net prod --rm alpine nslookup esName: esAddress 1: 172.23.0.3 prod-es-2.prodAddress 2: 172.23.0.2 prod-es-1.prod
(On peut ignorer les erreurs can't resolve '(null)'
)
containers/Container_Network_Model.md
Chaque instance ElasticSearch a un nom (généré au démarrage). Ce nom est visible quand on lance une simple requête HTTP sur le point d'accès de l'API ElasticSearch.
Essayons de lancer la commande suivante plusieurs fois:
$ docker run --rm --net dev centos curl -s es:9200{ "name" : "Tarot",...}
Puis essayons-la à nouveau plusieurs fois en remplaçant --net dev
par --net prod
:
$ docker run --rm --net prod centos curl -s es:9200{ "name" : "The Symbiote",...}
containers/Container_Network_Model.md
Docker ne peut créer des noms de réseau et alias sur le réseau par défaut bridge
.
Sachant ceci, pour utiliser ces fonctions, vous devez créer un réseau spécifique d'abord.
Les alias de réseau ne sont pas uniques au sein d'un réseau donné.
i.e plusieurs containers peuvent porter le même alias sur le même réseau.
Dans ce scénario, le serveur DNS Docker retournera plusieurs enregistrements.
(i.e, vous aurez un "DNS round robin" prêt à l'emploi)
Activer le Mode Swarm donne accès au traitement distribué (clustering) et la répartition de charge (load balancing) via IPVS.
Créer les réseaux et les alias de réseau est en général automatisé par des outils comme Compose.
containers/Container_Network_Model.md
Ne comptez pas exclusivement sur le DNS round robin pour de la répartition de charge.
Plusieurs facteurs peuvent affecter la réolution DNS, et vous pourriez avoir:
Aucun problème à utiliser le DNS pour explorer les points d'accès disponibles, mais prenez bien soin de les re-résoudre de temps à autre pour trouver les nouveaux points d'accès.
containers/Container_Network_Model.md
Lors de la création de réseaux, plusieurs options peuvent être fournies:
When creating a network, extra options can be provided.
--internal
désactive tout trafic sortant (le réseau n'aura pas de passerelle par défaut).
--gateway
indique quelle adresse utiliser pour la passerelle (quand le trafic sortant est autorisé).
--subnet
(en notation CIDR) indique le sous-réseau à utiliser.
--ip-range
(en notation CIDR) indique le sous-réseau pour l'allocation.
--aux-address
permet de spécifier une liste d'adresse réservées (qui ne seront jamais affectées aux containers).
containers/Container_Network_Model.md
--ip
.Voici ci-dessous un exemple complet.
$ docker network create --subnet 10.66.0.0/16 pubnet42fb16ec412383db6289a3e39c3c0224f395d7f85bcb1859b279e7a564d4e135$ docker run --net pubnet --ip 10.66.66.66 -d nginxb2887adeb5578a01fd9c55c435cad56bbbe802350711d2743691f95743680b09
Note: ne forcez pas d'adresse IP explicite de container dans votre code!
Je répète: ne forcez pas d'adresse IP de container dans votre code!
containers/Container_Network_Model.md
Les caractéristiques vues jusqu'ici fonctionnent uniquement quand les containers sont sur un seul hôte.
Si les containers sont répartis sur plusieurs hôtes, nous aurons besoin d'un réseau overlay pour les connecter ensemble.
Docker est livré avec un plugin de réseau par défaut, overlay
, qui implémente un réseau superposé exploitant le concept de VXLAN, qui s'active via le Mode Swarm.
D'autres plugins (Weave, Calico...) peuvent aussi fournir des réseaux superposés.
Une fois que vous avez un réseau superposé, toutes les fonctions utilisées dans ce chapitre fonctionnent de la même manière à travers plusieurs hôtes.
containers/Container_Network_Model.md
Hors-sujet pour cet atelier d'introduction!
Instructions très rapides:
docker swarm init
puis docker swarm join
sur les autres noeuds)docker network create mynet --driver overlay
docker service create --network mynet myimage
Pour en savoir plus sur le mode Swarm, jetez un oeil à cette vidéo ou ces diapos.
containers/Container_Network_Model.md
Hors-sujet pour cet atelier d'introduction!
Idée générale:
installer le plugin (souvent livré dans des containers)
lancer le plugin ( si c'est dans un container, il y a souvent besoin de paramètres supplémentaires; n'allez pas docker run
à l'aveugle!)
certains plugins exigent une configuration ou une activation (en créant un fichier spécial qui dit à Docker "utilise le plugin dont la socket est au chemin suivant)
vous pouvez ensuite docker network create --driver pluginname
containers/Container_Network_Model.md
Jusqu'ici, nous avons choisi quel réseau utiliser au démarrage du container.
Le Docker Engine permet aussi la connexion/déconnexion pendant que le container tourne.
Cette fonction est exposée via l'API Docker, et à travers deux commandes:
docker network connect <network> <container>
docker network disconnect <network> <container>
containers/Container_Network_Model.md
Nous avons un container nommé es
connecté à un réseau nommé dev
.
Démarrons un simple container alpine sur le réseau par défaut:
$ docker run -ti alpine sh/ #
Dans ce container, essayons de ping le container es
:
/ # ping esping: bad address 'es'
Cela ne fonctionne pas, mais nous allons corriger cela en connectant le container.
containers/Container_Network_Model.md
Extraire l'ID de notre container alpine; voici deux méthodes:
jeter un oeil à /etc/hostname
dans le container,
exécuter sur le hôte docker ps -lq
.
Lancer la commande suivant sur l'hôte:
$ docker network connect dev <container_id>
containers/Container_Network_Model.md
Essayez encore ping es
depuis le container.
Cela devrait fonctionner correctement normalement:
/ # ping esPING es (172.20.0.3): 56 data bytes64 bytes from 172.20.0.3: seq=0 ttl=64 time=0.376 ms64 bytes from 172.20.0.3: seq=1 ttl=64 time=0.130 ms^C
Stoppez-le avec Ctrl-C.
containers/Container_Network_Model.md
Nous pouvons lister les interfaces réseau avec ifconfig
, ip a
, ou ip l
:
/ # ip a1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever18: eth0@if19: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0 valid_lft forever preferred_lft forever20: eth1@if21: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP link/ether 02:42:ac:14:00:04 brd ff:ff:ff:ff:ff:ff inet 172.20.0.4/16 brd 172.20.255.255 scope global eth1 valid_lft forever preferred_lft forever/ #
Chaque connexion réseau est matérialisée via une interface réseau virtuelle.
Comme nous pouvons le voir, nous pouvons être connectés à plusieurs réseaux en même temps.
containers/Container_Network_Model.md
Essayons ce que donne la commande symétrique pour déconnecter le container:
$ docker network disconnect dev <container_id>
A partir de maintenant, si on cherche à ping es
, ce ne sera pas résolu:
/ # ping esping: bad address 'es'
Si on essaie de ping l'adresse IP directement, cela ne fonctionne plus non plus:
/ # ping 172.20.0.3... (rien ne se passe jusqu'à ce qu'on tape Ctrl-C)
containers/Container_Network_Model.md
Chaque réseau possède sa propre liste d'alias réseau.
Comme vu précédemment: es
est résolu avec différentes adresses selon les réseaux dev
et prod
.
Si nous sommes connectés à plusieurs réseaux, la résolution passe les noms en revue dans chaque réseau (dans Docker Engine 18.03, par ordre de connexion), et arrête dès que le nom a été trouvé.
Par conséquent, en étant connecté aux réseaux dev
et prod
, la résolution de es
ne nous donnera pas tous les noms des services es
, mais seulement ceux dans dev
ou prod
.
Toutefois, on peut interroger es.dev
ou es.prod
si on a besoin.
containers/Container_Network_Model.md
Nous pouvons lancer des requêtes DNS inverses sur les adresses IP des containers.
Si l'adresse IP appartient à un réseau (autre que le bridge par défaut), le résultat sera:
nom-du-premier-alias-ou-id-container.nom-reseau
Exemple:
$ docker run -ti --net prod --net-alias hello alpine/ # apk add --no-cache drill...OK: 5 MiB in 13 packages/ # ifconfigeth0 Link encap:Ethernet HWaddr 02:42:AC:15:00:03 inet addr:172.21.0.3 Bcast:172.21.255.255 Mask:255.255.0.0 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1.../ # drill -t ptr 3.0.21.172.in-addr.arpa...;; ANSWER SECTION:3.0.21.172.in-addr.arpa. 600 IN PTR hello.prod....
containers/Container_Network_Model.md
On peut construire un Dockerfile avec un réseau spécial via docker build --network NAME
.
Ça peut servir à garantir qu'un build n'accède pas au réseau.
(Gardez à l'esprit que la plupart des Dockerfiles vont échouer,
car ils auront besoin d'installer des paquets à distance et leurs dépendances!)
Cela servira à accéder à un dépôt interne privé.
(Mais essayez si possible d'utiliser un build multi-stage!)
containers/Container_Network_Model.md
Processus de développement local avec Docker
(automatically generated title slide)
A la fin de cette section, vous serez capable de:
Partager du code entre conteneur et hôte.
Utiliser un processus de développement local simple.
containers/Local_Development_Workflow.md
On veut résoudre les problèmes suivants:
"Ça marche sur ma machine"
"Pas la même version"
"Manque une dépendance"
En utilisant les conteneurs Docker, on arrivera à un environnement de développement homogène.
containers/Local_Development_Workflow.md
Nous avons à travailler sur une application dont le code est sur:
De quoi s'agit-il? On ne le sait pas encore!
Récupérons le code.
$ git clone https://github.com/jpetazzo/namer
containers/Local_Development_Workflow.md
$ cd namer$ ls -1company_name_generator.rbconfig.rudocker-compose.ymlDockerfileGemfile
$ cd namer$ ls -1company_name_generator.rbconfig.rudocker-compose.ymlDockerfileGemfile
Aha, un Gemfile
! C'est du Ruby. Probablement. On s'en doute. A moins que?
containers/Local_Development_Workflow.md
Dockerfile
FROM rubyCOPY . /srcWORKDIR /srcRUN bundler installCMD ["rackup", "--host", "0.0.0.0"]EXPOSE 9292
ruby
./src
.bundler
.rackup
.containers/Local_Development_Workflow.md
Dockerfile
!Dockerfile
!$ docker build -t namer .
Dockerfile
!$ docker build -t namer .
Dockerfile
!$ docker build -t namer .
$ docker run -dP namer
Dockerfile
!$ docker build -t namer .
$ docker run -dP namer
Dockerfile
!$ docker build -t namer .
$ docker run -dP namer
$ docker ps -l
containers/Local_Development_Workflow.md
Pointez le navigateur sur le serveur Docker, et sur le port alloué au conteneur.
Cliquez "Recharger" plusieurs fois.
Pointez le navigateur sur le serveur Docker, et sur le port alloué au conteneur.
Cliquez "Recharger" plusieurs fois.
C'est un générateur de nom d'entreprise de première classe, certifié ISO, niveau opérateur de réseau!
(Avec 50% de plus de baratin que la moyenne de la compétition!)
(Attends, c'était 50% de plus, ou 50% de moins? Qu'importe!)
containers/Local_Development_Workflow.md
Option 1:
Option 2:
docker exec
)Option 3:
containers/Local_Development_Workflow.md
On va indiquer à Docker de monter le dossier en cours sur /src
dans le conteneur.
$ docker run -d -v $(pwd):/src -P namer
-d
: le conteneur doit tourner en mode détaché (en arrière-plan).
-v
: le dossier du hôte mentionné doit être monté à l'intérieur du conteneur.
-P
: tous les ports exposés par cette image doivent être publiés.
namer
est le nom de l'image à exécuter.
Nous n'ajoutons pas la commande à lancer car elle est déjà dans le Dockerfile avec CMD
.
Note: sur Windows, remplacer $(pwd)
par %cd%
(ou ${pwd}
avec PowerShell).
containers/Local_Development_Workflow.md
L'option -v
monte un dossier depuis votre hôte dans le conteneur Docker.
La structure de l'option est:
[chemin-hote]:[chemin-conteneur]:[rw|ro]
Si [chemin-hote]
ou [chemin-conteneur]
n'existe pas, il sera créé.
Vous pouvez contrôler l'option d'écriture du volume avec les options ro
et rw
.
Si vous ne spécifiez ni rw
ou ro
, ce sera rw
par défaut.
Il y aura un chapitre complet sur les volumes!
containers/Local_Development_Workflow.md
$ docker ps -lCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES045885b68bc5 namer rackup 3 seconds ago Up ... 0.0.0.0:32770->9292/tcp ...
containers/Local_Development_Workflow.md
Notre client n'aime pas du tout la couleur de notre texte. Allons la changer.
$ vi company_name_generator.rb
Et changeons:
color: royalblue;
En:
color: red;
containers/Local_Development_Workflow.md
Recharger l'application dans notre navigateur
La couleur doit avoir changé.
containers/Local_Development_Workflow.md
Aucune copie ou synchronisation de fichiers entre hôte et conteneur ne se passe dans un volume.
Les volumes sont des bind mounts: un mécanisme du noyau associant un chemin à un autre.
Un bind mount est une sorte de lien symbolique, mais à un niveau très différent.
Tout changement sur l'hôte ou le conteneur sera visible de l'autre côté.
(Puisque sous le capot, c'est le même fichier de toute façon.)
containers/Local_Development_Workflow.md
(C'est le titre d'un billet de blog de 2013 par Chad Fowler, expliquant le concept d'infrastructure immuable.)
(C'est le titre d'un billet de blog de 2013 par Chad Fowler, expliquant le concept d'infrastructure immuable.)
Mettons un grand bazar dans notre conteneur.
(Supprimer des fichiers ou autre.)
Maintenant, comment réparer ça?
(C'est le titre d'un billet de blog de 2013 par Chad Fowler, expliquant le concept d'infrastructure immuable.)
Mettons un grand bazar dans notre conteneur.
(Supprimer des fichiers ou autre.)
Maintenant, comment réparer ça?
Notre vieux conteneur (avec la version bleue du code) tourne toujours.
Voyons sur quel port c'est exposé:
docker ps
containers/Local_Development_Workflow.md
Au lieu de modifier le serveur, nous en déployons un nouveau.
Cela peut sembler un défi pour les serveurs classiques, mais c'est trivial avec les conteneurs.
En fait, avec Docker, le processus le plus logique est de générer une nouvelle image et de la lancer.
Si quoique ce soit cloche avec la nouvelle image, on peut toujours relancer l'ancienne.
On peut même garder les deux versions côte-à-côte.
Si ce motif vous semble intéressant, vous pouvez regarder du côté des déploiements blue/green ou canary
containers/Local_Development_Workflow.md
Ecrire un Dockerfile pour générer une image contenant l'environnement de développement.
(Rails, Django, ... et toutes les dépendances de notre appli)
Démarrer un conteneur de cette image.
Utiliser l'option -v
pour monter notre code source dans le conteneur.
Modifier le code source hors des conteneurs, avec les outils habituels.
(vim, emacs, textmate...)
Tester l'application.
(Certains frameworks détectent les changements automatiquement
D'autres exigent un Ctrl+C / redémarrage après chaque modification..)
Reboucler et répéter les étapes 3 et 4 jusqu'à satisfaction.
Quand c'est fini, faire un "commit+push" des changements de code.
containers/Local_Development_Workflow.md
Docker dispose d'une commande appelée docker exec
.
Cela permet aux utilisateurs de lancer un nouveau processus dans un conteneur déjà lancé.
Si parfois vous sentez que vous aimeriez entrer via SSH sur un conteneur: vous pouvez utiliser docker exec
à la place.
Vous pouvez ainsi récupérer un terminal ou lancer une n'importe quelle autre commande pour automatisation.
containers/Local_Development_Workflow.md
docker exec
$ #Vous pouvez lancer des commandes ruby dans le même conteneur où l'appli tourne!$ docker exec -it <yourContainerId> bashroot@5ca27cf74c2e:/opt/namer# irbirb(main):001:0> [0, 1, 2, 3, 4].map {|x| x ** 2}.compact=> [0, 1, 4, 9, 16]irb(main):002:0> exit
containers/Local_Development_Workflow.md
Maintenant que nous avons fini, arrêtons notre conteneur.
$ docker stop <yourContainerID>
Et supprimons-le.
$ docker rm <yourContainerID>
containers/Local_Development_Workflow.md
Nous avons appris à:
Partager le code entre conteneur et hôte.
Régler notre dossier de travail.
Utiliser un processus simple de développement.
containers/Local_Development_Workflow.md
Travailler avec des volumes
(automatically generated title slide)
A la fin de cette section, vous serez capable de:
Créer des conteneurs gérant des volumes.
Partager des volumes à travers des conteneurs.
Partager un dossier du serveur avec un ou plusieurs conteneurs.
containers/Working_With_Volumes.md
Les volumes Docker sont utilisés pour accomplir bien des buts, y compris:
Contourner le système copy-on-write pour obtenir une performance d'I/O native.
Contourner le copy-on-write pour laisser quelques fichiers hors de docker commit
.
Partager un dossier entre plusieurs conteneurs.
Partager un dossier entre le serveur et le conteneur.
Partager un seul fichier entre l'hôte et le conteneur.
Utiliser un stockage distant et un stockage spécifique avec les "pilotes de volumes".
containers/Working_With_Volumes.md
On peut déclarer des volumes de deux façons différentes.
Dockerfile
, avec une instruction VOLUME
.VOLUME /uploads
-v
avec docker run
.$ docker run -d -v /uploads myapp
Dans les deux cas, /uploads
(à l'intérieur du conteneur) sera un volume.
containers/Working_With_Volumes.md
Les volumes agissent comme des passerelles vers le système de fichier de l'hôte.
La performance d'un volume en termes d'I/O disque est exactement la même que sur l'hôte Docker.
Quand on fait un docker commit
, le contenu des volumes n'est pas intégré
dans l'image résultante.
Si une instruction RUN
dans un Dockerfile
change le contenu d'un volume,
ces changements ne seront pas non plus enregistrés.
Si un conteneur est démarré avec l'option --read-only
, le volume sera
toujours modifiable (à moins que le volume lui-même soit en lecture-seule).
containers/Working_With_Volumes.md
Vous pouvez démarrer un conteneur avec exactement les mêmes volumes qu'un autre.
Le nouveau conteneur aura les mêmes volumes, dans les mêmes dossiers.
Ils contiendront exactement la même chose, et resteront synchronisés.
Sous le capot, ce sont en fait les mêmes dossiers sur le serveur.
C'est possible avec l'option --volumes-from
dans docker run
.
Nous allons en voir un exemple dans les diapos suivantes.
containers/Working_With_Volumes.md
Démarrons un conteneur Tomcat:
$ docker run --name webapp -d -p 8080:8080 -v /usr/local/tomcat/logs tomcat
Maintenant, démarrons un conteneur alpine
avec les mêmes volumes:
$ docker run --volumes-from webapp alpine sh -c "tail -f /usr/local/tomcat/logs/*"
Puis, d'une autre fenêtre, envoyons des requêtes à notre conteneur Tomcat:
$ curl localhost:8080
containers/Working_With_Volumes.md
Si un conteneur est arrêté ou supprimé, ses volumes existent toujours et sont accessibles.
On peut lister et manipuler les volumes avec les sous-commandes de docker volume
:
$ docker volume lsDRIVER VOLUME NAMElocal 5b0b65e4316da67c2d471086640e6005ca2264f3...local pgdata-prodlocal pgdata-devlocal 13b59c9936d78d109d094693446e174e5480d973...
Certains des noms de volumes sont explicites (pgdata-prod, pgdata-dev).
D'autres (les IDs hexa) sont générés automatiquement par Docker.
containers/Working_With_Volumes.md
Ajoutons quelques volumes directement.
$ docker volume create webappswebapps
$ docker volume create logslogs
Nos volumes ne sont attachés à aucun dossier en particulier.
containers/Working_With_Volumes.md
On active les volumes avec l'option -v
.
Quand le chemin côté hôte ne contient pas de /, il est traité comme un nom de volume.
Démarrons un serveur web avec les deux précédents volumes.
$ docker run -d -p 1234:8080 \ -v logs:/usr/local/tomcat/logs \ -v webapps:/usr/local/tomcat/webapps \ tomcat
Vérifions que cela s'exécute normalement:
$ curl localhost:1234... (Tomcat nous raconte combien il est content de tourner) ...
containers/Working_With_Volumes.md
Nous allons modifier le contenu d'un volume depuis un autre conteneur.
Dans cet exemple, nous allons lancer un éditeur de texte dans un autre conteneur.
(Mais ça pourrait être un serveur FTP, un serveur WebDAV, un dépôt Git...)
Démarrons un autre conteneur attaché au volume webapps
.
$ docker run -v webapps:/webapps -w /webapps -ti alpine vi ROOT/index.jsp
Il nous reste à vandaliser la page, enregistrer, et sortir.
Exécutons encore un curl localhost:1234
pour voir nos changements.
containers/Working_With_Volumes.md
Dans certains cas, vous voudrez monter un dossier depuis l'hôte vers le conteneur:
Pour gérer le stockage et les snapshots vous-même;
(Avec LVM, ou un SAN, ou ZFS, ou toute autre chose!)
ou vous avez un autre disque aux meilleures performances (SSD) ou à résilience supérieure (EBS) et vous voulez y placer d'importantes données.
ou vous voulez partager un dossier source entre votre hôte (où se trouve le source) et le conteneur (où se passe la compilation et l'exécution).
Un moment, on a déjà vu ce cas d'usage dans notre exemple de processus de développement! Pas mal.
$ docker run -d -v /chemin/depuis/notre/hote:/chemin/dans/le/conteneur image ...
containers/Working_With_Volumes.md
--volumes-from
L'option --volumes-from
indique à Docker de reprendre tous les volumes
d'un conteneur existant.
Scenario: migrer de Redis 2.8 à Redis 3.0.
Nous avons un conteneur (myredis
) qui fait tourner Redis 2.8.
Arrêtez le conteneur myredis
.
Démarrez un nouveau conteneur, avec l'image Redis 3.0, et l'option --volumes-from
.
Le nouveau conteneur va hériter des données de l'ancien.
Les futurs conteneurs pourront aussi utiliser --volumes-from
.
Ne marche pas entre serveurs, donc impossible en clusters (Swarm, Kubernetes).
containers/Working_With_Volumes.md
Créons un conteneur Redis.
$ docker run -d --name redis28 redis:2.8
Puis connectons-nous au conteneur Redis pour ajouter des données.
$ docker run -ti --link redis28:redis busybox telnet redis 6379
Envoyons les commandes suivantes:
SET counter 42INFO serverSAVEQUIT
containers/Working_With_Volumes.md
Arrêtez le conteneur Redis.
$ docker stop redis28
Démarrer le nouveau conteneur Redis.
$ docker run -d --name redis30 --volumes-from redis28 redis:3.0
containers/Working_With_Volumes.md
Connectez-vous au conteneur Redis pour voir les données.
docker run -ti --link redis30:redis busybox telnet redis 6379
Lancez les commandes suivantes:
GET counterINFO serverQUIT
containers/Working_With_Volumes.md
Au moment de supprimer le conteneur, ses volumes sont conservés.
On peut les lister avec docker volume ls
.
On peut y accéder en créant un conteneur avec docker run -v
.
On peut les supprimer avec docker volume rm
ou docker system prune
.
Au final, vous êtes responsable de logger, surveiller, et sauvegarder vos volumes.
containers/Working_With_Volumes.md
Vous vous demandez si une image a des volumes? Il suffit d'appeler docker inspect
:
$ # docker inspect training/datavol[{ "config": { . . . "Volumes": { "/var/webapp": {} }, . . .}]
containers/Working_With_Volumes.md
Pour voir quels dossiers sont en fait des volumes, et où est-ce qu'ils pointent,
passons par docker inspect
(encore):
$ docker inspect <yourContainerID>[{ "ID": "<yourContainerID>",. . . "Volumes": { "/var/webapp": "/var/lib/docker/vfs/dir/f4280c5b6207ed531efd4cc673ff620cef2a7980f747dbbcca001db61de04468" }, "VolumesRW": { "/var/webapp": true },}]
containers/Working_With_Volumes.md
La même option -v
peut servir à partage un seul fichier (au lieu de tout un dossier).
Un des exemples les plus intéressants est de partager la socket de contrôle de Docker.
$ docker run -it -v /var/run/docker.sock:/var/run/docker.sock docker sh
Depuis ce conteneur, on peut lancer des commandes docker
pour communiquer avec
le Docker Engine qui tourne sur ce serveur. Essayez docker ps
!
Puisque ce conteneur a accès à la socket Docker, il a un accès root au hôte.
containers/Working_With_Volumes.md
Vous pouvez installer des plugins pour gérer les volumes adossés à différents systèmes de stockage ou ayant des fonctions spéciales. Par exemple:
REX-Ray - créer et gérer des volumes adossés à un système de stockage professionnel (SAN ou NAS), ou des solutions cloud (par ex. EBS, EFS).
Portworx - fournit un stockage par bloc distribué pour conteneurs.
Gluster - stockage open source, défini par code, qui peut grimper jusqu'à plusieurs petaoctets. Il fournit une interface pour du stockage d'objet, bloc ou fichier.
et bien d'autres sur le Docker Store!
containers/Working_With_Volumes.md
Depuis Docker 17.06, une nouvelle option est disponible: --mount
.
Elle offre une syntaxe plus riche pour manipuler les données de conteneurs.
Elle introduit une différence explicite entre:
les volumes (identifiés par un nom unique, gérés par un plugin de stockage),
les bind mounts (identifiés par un chemin du hôte, sans gestion intermédiaire).
L'option précédente -v
/ --volume
reste toujours utilisable.
containers/Working_With_Volumes.md
--mount
Attacher un dossier de l'hôte à un chemin du conteneur:
$ docker run \ --mount type=bind,source=/path/on/host,target=/path/in/container alpine
Monter un volume dans un chemin du conteneur:
$ docker run \ --mount source=myvolume,target=/path/in/container alpine
Monter un tmpfs (pour stockage de fichiers temporaires en mémoire):
$ docker run \ --mount type=tmpfs,destination=/path/in/container,tmpfs-size=1000000 alpine
containers/Working_With_Volumes.md
Nous avons appris comment:
Créer et gérer les images.
Partager des volumes entre conteneurs.
Partager un dossier de l'hôte avec un ou plusieurs conteneurs.
containers/Working_With_Volumes.md
Compose pour les développeurs
(automatically generated title slide)
Utiliser des Dockerfiles est super pour générer des images de conteneurs.
Et si nous voulions travailler avec une suite complexe composée de plusieurs conteneurs?
Au final, on voudra disposer de scripts spécifiques et automatisés pour construire, lancer et connecter nos conteneurs entre eux.
Il y a une meilleure méthode: utiliser Docker Compose.
Dans ce chapitre, nous utiliserons Compose pour démarrer un environnement de développement.
containers/Compose_For_Dev_Stacks.md
Docker Compose (à l'origine appelé fig
) est un outil externe.
Contrairement au Docker Engine, il est écrit en Python. C'est aussi un logiciel libre.
L'idée générale de Compose est de permettre un processus de démarrage très facile et puissant:
Récupérez votre code.
Lancez docker-compose up
.
Votre appli est lancée et prête à l'emploi!
containers/Compose_For_Dev_Stacks.md
Voici comment on travaille avec Compose:
Vous décrivez un ensemble (ou stack) de conteneurs dans un fichier YAML appelé docker-compose.yml
.
Vous lancez docker-compose up
.
Compose télécharge automatiquement les images, génère les conteneurs et les démarre.
Compose configure les liens, volumes et autres options de Docker pour vous.
Compose peut lancer les conteneurs en arrière-plan, ou en avant-plan.
Quand on lance nos conteneurs en avant-plan, leur sortie est agrégée à l'affichage.
Avant de s'y plonger, voyons un petit exemple de Compose en action.
containers/Compose_For_Dev_Stacks.md
Si vous utilisez les machines virtuelles de formation officielle, Compose a été pré-installé.
Si vous utilisez Docker pour Mac/Windows ou Docker Toolbox, Compose y est inclus.
Si vous êtes sur Linux (desktop ou serveur), vous devrez install Compose depuis la page de release ou avec pip install docker-compose
.
Vous pouvez vérifier votre installation en tapant:
$ docker-compose --version
containers/Compose_For_Dev_Stacks.md
Première étape: cloner le code source de l'appli que nous allons manipuler.
$ cd$ git clone https://github.com/jpetazzo/trainingwheels...$ cd trainingwheels
Seconde étape: démarrer votre appli.
$ docker-compose up
Observez Compose pendant qu'il génère et lance votre appli avec les paramètres corrects, y compris la mise en réseau des conteneurs entre eux.
containers/Compose_For_Dev_Stacks.md
Vérifiez que notre appli répond sur: http://<yourHostIP>:8000
.
containers/Compose_For_Dev_Stacks.md
Quand vous tapez ^C
, Compose tente d'arrêter en douceur tous les conteneurs.
Après 10 secondes (ou après plusieurs ^C
), ils seront tous stoppés de force.
containers/Compose_For_Dev_Stacks.md
docker-compose.yml
Voici le fichier utilisé dans la démo:
version: "2"services: www: build: www ports: - 8000:5000 user: nobody environment: DEBUG: 1 command: python counter.py volumes: - ./www:/src redis: image: redis
containers/Compose_For_Dev_Stacks.md
Un fichier Compose possède plusieurs sections:
version
est obligatoire. (On devrait utiliser "2"
ou plus. La version 1 est obsolète.)
services
est obligatoire. Un service est une ou plusieurs copies de la même image sous forme de conteneurs.
networks
est optionnel et indique à quels réseaux devraient se connecter nos conteneurs.
(Par défaut, les conteneurs seront liés à un réseau privé, unique par fichier compose.)
volumes
est optionnel et peut définir les volumes utilisés et/ou partagés par les conteneurs.
containers/Compose_For_Dev_Stacks.md
La version 1 est obsolète et ne devrait pas être utilisée.
(Si vous voyez un fichier Compose sans version
ni services
, c'est une version 1.)
La version 2 ajoute le support des réseaux et volumes.
La version 3 ajoute le support des options de déploiements (montée en charge, mises à jour progressives, etc.).
La documentation Docker a un excellent niveau d'information sur le format du fichier Compose, à propos de toutes les différentes versions.
containers/Compose_For_Dev_Stacks.md
docker-compose.yml
Chaque service dans le fichier YAML doit mentionner soit build
, ou image
.
build
indique un chemin contenant un Dockerfile
image
indique un nom d'image (local, ou sur un registre).
Si les deux sont spécifiés, une image sera générée depuis le dossier build
et nommée selon image
.
Les autres paramètres sont optionnels.
Ils encodent tous les paramètres typiques de la commande docker run
.
Ils comportent parfois des améliorations mineures.
containers/Compose_For_Dev_Stacks.md
command
indique quoi lancer (comme la commande CMD
du Dockerfile).
ports
se traduit par une (ou plusieurs) options -p
de correspondance des ports.
Vous pouvez spécifier des ports locaux (par ex. x:y
pour exposer le port public x
).
volumes
se traduit par une (ou plusieurs) options -v
.
Vous pouvez utiliser des chemins relatifs ici.
Pour la liste complète, voir: https://docs.docker.com/compose/compose-file/
containers/Compose_For_Dev_Stacks.md
Nous avons déjà vu docker-compose up
, mais en voici une autre, docker-compose build
.
Cela va lancer docker build
pour tous les conteneurs mentionnant un chemin build
.
On peut aussi l'invoquer automatiquement en lançant l'application:
docker-compose up --build
Une autre option commune est de démarrer les conteneurs en arrière-plan:
docker-compose up -d
containers/Compose_For_Dev_Stacks.md
Cela peut se révéler fastidieux de vérifier le statut de vos conteneurs avec docker ps
, surtout quand plusieurs applis tournent en même temps.
Compose nous facilite la tâche; avec docker-compose ps
, il n'affichera
que le statut des conteneurs de la stack en cours:
$ docker-compose psName Command State Ports----------------------------------------------------------------------------trainingwheels_redis_1 /entrypoint.sh red Up 6379/tcptrainingwheels_www_1 python counter.py Up 0.0.0.0:8000->5000/tcp
containers/Compose_For_Dev_Stacks.md
Si vous avez démarré votre application en arrière-plan avec Compose, et que vous allez l'arrêter vite fait, vous pouvez passer par la commande kill
:
$ docker-compose kill
De même, docker-compose rm
vous permet de supprimer les conteneurs (après confirmation):
$ docker-compose rmGoing to remove trainingwheels_redis_1, trainingwheels_www_1Are you sure? [yN] yRemoving trainingwheels_redis_1...Removing trainingwheels_www_1...
containers/Compose_For_Dev_Stacks.md
Par ailleurs, docker-compose down
va arrêter et supprimer les conteneurs.
Cette commande va aussi supprimer d'autres ressources, comme les réseaux spécialement créés pour cette application.
$ docker-compose downStopping trainingwheels_www_1 ... doneStopping trainingwheels_redis_1 ... doneRemoving trainingwheels_www_1 ... doneRemoving trainingwheels_redis_1 ... done
Enfin, docker-compose down -v
va tout supprimer, y compris les volumes.
containers/Compose_For_Dev_Stacks.md
Compose est malin. Si votre conteneur utilise des volumes, quand vous re-démarrez votre appli, Compose va créer un nouveau conteneur, mais fera attention à reprendre les volumes utilisés à l'origine.
Cela rend plus simple la mise à jour d'un service et ses données, où Compose va télécharger les images et rédémarrer la stack.
containers/Compose_For_Dev_Stacks.md
Quand vous lancez une commande Compose, Compose déduit un "nom de projet" pour votre appli.
Par défaut, le "nom de projet" est le nom de votre dossier en cours.
Par exemple, si vous êtes dans /home/zelda/src/ocarina
, le nom du projet est ocarina
.
Toutes les ressources initiées par Compose sont marquées avec ce nom de projet.
Le nom du projet apparaît comme préfixe des noms pour toutes les ressources.
Par ex., dans l'exemple précédent, le service www
va créer un conteneur ocarina_www_1
.
Le nom du projet peut être surchargé avec docker-compose -p
.
containers/Compose_For_Dev_Stacks.md
Si vous voulez exécuter deux exemplaires de la même appli simultanément, tout ce que vous avez à faire est de vous assurer que chaque exemplaire a un nom de projet différent.
Vous pouvez:
soit copier votre code dans un nouveau dossier avec un nom différent
soit démarrer chaque copie avec docker-compose -p nomdeprojet up
Chaque copie s'exécutera dans un réseau différent, totalement isolé des autres.
C'est idéal pour débogger des régressions, comparer entre 2 versions, etc.
containers/Compose_For_Dev_Stacks.md
Configuration d'applications
(automatically generated title slide)
Il y a de nombreuses façons de passer une configuration aux applications conteneurisées.
Il n'y a pas de "bonne manière", cela dépend de plusieurs facteurs, tels que:
la taille de la configuration;
les paramètres obligatoires et optionnels;
la visibilité de la configuration (par conteneur, par app, par client, par site, etc.);
la fréquence de changement de configuration.
containers/Application_Configuration.md
docker run jpetazzo/hamba 80 www1:80 www2:80
La configuration est fournie via des paramètres en ligne de commande.
Dans l'exemple ci-dessus, ENTRYPOINT
pointe sur un script qui va:
parser les paramètres,
générer un fichier de configuration,
démarrer le service réel.
containers/Application_Configuration.md
Convient pour les paramètres obligatoires (sans lesquels le service ne peut pas démarrer);
Utile pour les services "boîte à outils" qui se lancent à de nombreuses reprises.
(Parce qu'il n'y a pas d'autres étapes: juste à le lancer!)
Pas terrible pour les configurations dynamiques ou plus conséquentes.
(Toujours possible à réaliser, mais plus encombrant)
containers/Application_Configuration.md
docker run -e ELASTICSEARCH_URL=http://es42:9201/ kibana
Configuration fournie à travers des variables d'environnement.
La variable d'environnement peut être utilisée directement par le programme,
ou par un script générant un fichier de configuration.
containers/Application_Configuration.md
Pertinent pour des paramètres optionnels (puisque l'image peut fournir des valeurs par défault)
Aussi pratique pour des services se lançant de nombreuses fois.
(C'est aussi simple que les paramètres en ligne de commande.)
Super pour des services avec beaucoup de paramètres, mais vous voulez juste en changer quelques-uns.
(Et garder les valeurs par défaut pour tout le reste.)
Capacité à examiner les paramètres disponibles et leurs valeurs par défaut.
Pas terrible pour les configurations dynamiques.
containers/Application_Configuration.md
FROM prometheusCOPY prometheus.conf /etc
La configuration est ajoutée à l'image.
L'image peut avoir une configuration par défaut; la nouvelle config peut:
containers/Application_Configuration.md
Permet une personnalisation avancée, et des fichiers de configuration complexes;
Exige d'écrire un fichier de configuration (bien sûr!)
Exige de générer une image pour démarrer le service
Exige de générer une image pour reconfigurer le service
Exige de générer une image pour mettre à jour le service
Toute image pré-configurée peut-être stockée dans une Registry.
(ce qui est super, mais nécessite une Registry)
containers/Application_Configuration.md
docker run -v appconfig:/etc/appconfig myapp
La configuration est stockée dans un volume;
Le volume est attaché au container;
L'image peut avoir une configuration par défaut.
(Mais cela demande un réglage non trivial, via plus de documentation.)
containers/Application_Configuration.md
Permet une personnalisation avancée, et des fichiers de configuration complexes;
Exige de déclarer un nouveau volume pour chaque différente configuration;
Les services avec des configurations identiques peuvent ré-utiliser le même volume;
Ne force pas à générer/regénérer une image lors des mises à jour ou reconfiguration;
Une configuration peut être générée ou modifiée via un container tiers.
containers/Application_Configuration.md
C'est une technique puissante pour des configurations dynamiques et complexes;
La configuration est stockée dans un volume;
La configuration est générée/mise à jour depuis un container spécial;
L'application du container détecte quand la configuration a changé;
(et recharge automatiquement la configuration quand nécessaire.)
La configuration peut être partagée entre service si besoin.
containers/Application_Configuration.md
Dans un premier terminal, démarrer un load balancer avec une configuration initiale:
$ docker run --name loadbalancer jpetazzo/hamba \ 80 goo.gl:80
Dans un autre terminal, reconfigurer ce load balancer:
$ docker run --rm --volumes-from loadbalancer jpetazzo/hamba reconfigure \ 80 google.com:80
La configuration pourrait aussi mise à jour via une API REST.
(L'API REST tournant elle-même depuis un autre container.)
containers/Application_Configuration.md
Stockage des secrets
(automatically generated title slide)
Idéalement, vous ne devriez pas transmettre de secrets (mot de passe, tokens, etc.) via:
la ligne de commande ou des variables d'environnement (quiconque avec un accès à l'API Docker peut les récupérer)
des images, surtout celles stockées dans une Registry.
La gestion des secrets est mieux supportée avec un orchestrateur (type Swarm ou K8S).
Les orchestrateurs autorisent la transmission de secrets en "sens-unique".
Gérer les secrets de manière sécurisée sans orchestrateur peut se révéler "tiré par les cheveux".
Par ex.:
lire le contenu du secret via stdin quand le service démarre;
passer le secret via un endpoint d'une API.
containers/Application_Configuration.md
Gestion des logs
(automatically generated title slide)
Dans ce chapitre, nous expliquerons les différentes manières d'envoyer des logs depuis les conteneurs.
Nous verrons ensuite une méthode particulière en action, avec ELK et les pilotes de logs de Docker.
La méthode la plus simple est d'écrire sur les sorties standard et d'erreur.
Les applications peuvent écrire leur logs dans des fichiers locaux.
(Ces fichiers sont soumis à une compression et une mise en rotation.)
Il est aussi très commun (sur système UNIX) d'utiliser syslog.
(Les logs sont collectés par syslogd ou un équivalent, tel journald)
Pour d'importantes applis aux nombreux composants, il est commun de passer par un service de logs.
(Le code utilise une bibliothèque pour envoyer des messages au service)
Toutes ces méthodes sont possibles avec les conteneurs.
Les sorties standard et erreur de conteneurs sont gérées par le moteur de conteneurs.
Cela signifie que chaque ligne écrite par le conteneur est reçue par le moteur.
Ce moteur peut alors se "débrouiller" avec ces lignes de logs.
Avec Docker, la configuration par défaut est d'écrire ces logs dans des fichiers locaux.
Les fichiers peuvent être consultés avec docker logs
(et les requêtes équivalentes de l'API).
Ce comportement peut être personnalisé, comme on le verra plus tard.
Si on écrit dans des fichiers, il est possible d'y accéder mais c'est assez laborieux.
(On doit passer par docker exec
ou docker cp
.)
En outre, si le conteneur s'arrête, on ne peut plus lancer docker exec
.
Pire, si le conteneur est effacé, les logs disparaîtront.
Alors que faire pour les programmes qui peuvent uniquement écrire en local?
Si on écrit dans des fichiers, il est possible d'y accéder mais c'est assez laborieux.
(On doit passer par docker exec
ou docker cp
.)
En outre, si le conteneur s'arrête, on ne peut plus lancer docker exec
.
Pire, si le conteneur est effacé, les logs disparaîtront.
Alors que faire pour les programmes qui peuvent uniquement écrire en local?
Il y a plusieurs solutions.
Au lieu de stocker les logs dans un dossier normal, on les place sur un volume.
Le volume est accessible par d'autres conteneurs.
On lance un exécutable tel que filebeat
dans un autre conteneur accédant au même volume.
(filebeat
lit en continu les fichiers de logs locaux, comme tail -f
,
et les envoie dans un système central tel que ElasticSearch.)
On peut aussi passer par un point de montage, par ex. -v /var/log/containers/www:/var/log/tomcat
.
Le conteneur va écrire les fichiers de logs dans un dossier exposé sur l'hôte.
Les fichiers logs vont apparaître sur l'hôte et être accessibles directement sur l'hôte.
On peut utiliser des frameworks (comme log4j) ou le paquet Python logging
).
Ces frameworks exigent de coder et/ou configurer notre application.
Ces mécanismes sont valables aussi bien dans le conteneur qu'en dehors.
Parfois, on peut exploiter le réseau de conteneurs pour simplifier de telles configurations.
Par exemple, notre code peut envoyer des messages de logs à un serveur appelé log
.
Le nom log
sera résolu différemment selon qu'on est en développement, production, etc.
Et si notre code (ou le programme qu'on fait tourner dans le conteneur) utilise syslog?
Une possibilité serait de lancer un daemon syslog dans le conteneur.
Et ce daemon peut être configuré pour écrire dans des fichiers locaux ou transmettre les logs à travers le réseau.
Sous le capot, les clients syslog se connectent à une socket locale UNIX, /dev/log
.
On devra donc exposer une socket syslog au conteneur (via un volume ou un point de montage).
Et terminer en créant un lien symbolique depuis /dev/log
vers la socket syslog.
Voilà!
Si on écrit sur stdout et stderr, le moteur de conteneur reçoit les messages de log.
Le Docker Engine dispose d'un système de log modulaire avec de nombreux plugins, dont:
Chaque plugin peut traiter et transmettre les logs à un autre processus ou système.
json-file
Par défaut, la taille du fichier de log est illimitée.
Cela signifie qu'un conteneur très bavard videra certainement tout l'espace disque.
(ou un conteneur moins bavard aussi, mais dans un laps de temps très long.)
La rotation de logs peut-être activée avec l'option max-size
.
D'anciens fichiers de logs peuvent être supprimés avec l'option max-file
.
Toutes ces options relatives à la journalisation peuvent être réglées par conteneur, ou globalement.
Exemple:
$ docker run --log-opt max-size=10m --log-opt max-file=3 elasticsearch
Nous allons déployer la suite ELK.
Elle acceptera les logs via une socket GELF.
Nous allons lancer quelques conteneurs avec le pilote de log gelf
.
Nous verrons alors nos logs dans Kibana, l'interface web fournie par ELK.
Avant-propos important: ce n'est pas une installation "officielle" ou "recommandée"; juste un exemple. Nous avons choisi ELK pour cette démo par sa popularité et les demandes qu'il suscite; mais vous serez aussi gagnant avec Fluent ou d'autres solutions de journalisation!
ELK, c'est trois composants:
ElasticSearch, pour stocker et indexer les messages de log;
Logstash, qui reçoit les messages de diverses sources, les traite, et les transmet à diverses destinations;
Kibana, pour afficher/chercher les messages dans une jolie interface.
Le seul composant que nous allons configurer est Logstash.
Nous accepterons des messages de log au format GELF.
Les messages seront stockés dans ElasticSearch,
et affichés dans la sortie standard de Logstash pour débogage.
Nous allons utiliser un fichier Compose décrivant la solution ELK.
Le fichier Compose est dans le dépôt container.training sur Github
$ git clone https://github.com/jpetazzo/container.training$ cd container.training$ cd elk$ docker-compose up
Nous allons utiliser des images du Docker Hub: elasticsearch
, logstash
, kibana
.
Pas besoin de changer la configuration d'ElasticSearch.
Mais nous devons donner à Kibana l'adresse d'ElasticSearch:
elle est indiquée dans la variable d'environnement ELASTICSEARCH_URL
par défaut, c'est localhost:9200
, on va la changer en elasticsearch:9200
.
On a besoin de configurer Logstash:
on lui passe un fichier de configuration entier via la ligne de commande,
c'est une bidouille pour éviter de générer une image juste pour la config.
La solution ELK accepte des messages via une socket GELF.
La socket GELF écoute sur le port UDP 12201.
Pour envoyer un message, on a besoin de changer le pilote de journalisation utilisé par Docker.
Cela peut être réalisé en global (en reconfigurant le moteur) ou par conteneur.
Essayons de rédéfinir le pilote de journalisation pour un seul conteneur:
$ docker run --log-driver=gelf --log-opt=gelf-address=udp://localhost:12201 \ alpine echo hello world
Se connecter à l'interface Kibana.
Il est exposé sur le port 5601.
Ouvrir http://X.X.X.X:5601.
Kibana devrait vous proposer de "Configure an index pattern":
dans la liste "Time-field name", choisir "@timestamp" et cliquez
le bouton "Create".
Puis:
Vous pouvez voir une série de barres vertes (avec une nouvelle barre toutes les minutes)
Notre message "Hello world" devrait y apparaître.
Ce n'est pas une installation de niveau "production".
Il s'agit d'un exemple à but éducatif. Puisque nous avons un seul serveur, nous avons installé une seule instance ElasticSearch et une seule instance Logstash.
Dans une installation de "production", vous avez besoin d'un cluster ElasticSearch (pour la haute disponibilité et la capacité totale de stockage). Vous avez aussi besoin de plusieurs instances de Logstash.
Et si vous voulez résister aux pics de logs, vous aurez besoin d'une sorte de file d'attente de messages: Redis si c'est léger, Kafka si vous voulez garantir aucune perte. Bonne chance.
Pour en savoir plus sur le pilote GELF, jetez un oeil sur ce billet de blog.
Limiter les ressources
(automatically generated title slide)
Jusqu'ici, nous avons utilisé les conteneurs comme des unités de déploiements assez pratiques.
Que se passe-t-il quand un conteneur essaie d'utiliser plus de ressources que disponible?
(RAM, CPU, disque, entrées/sortie réseau...)
Que se passe-t-il quand plusieurs conteneurs entrent en concurrence pour la même ressource?
Pouvons-nous limiter les ressources allouées à un conteneur?
(Un indice: oui!)
Les conteneurs sont plus proches de "processus spéciaux" que de "VMs légères".
Un processus lancé dans un conteneur est, en fait, un processus s'exécutant sur l'hôte.
Jetons un oeil à l'affichage de ps
sur un hôte hébergeant 3 conteneurs:
0 2662 0.2 0.3 /usr/bin/dockerd -H fd:// 0 2766 0.1 0.1 \_ docker-containerd --config /var/run/docker/containe 0 23479 0.0 0.0 \_ docker-containerd-shim -namespace moby -workdir 0 23497 0.0 0.0 | \_ nginx: master process nginx -g daemon off; 101 23543 0.0 0.0 | \_ nginx: worker process 0 23565 0.0 0.0 \_ docker-containerd-shim -namespace moby -workdir 102 23584 9.4 11.3 | \_ /docker-java-home/jre/bin/java -Xms2g -Xmx2 0 23707 0.0 0.0 \_ docker-containerd-shim -namespace moby -workdir 0 23725 0.0 0.0 \_ /bin/sh
Les processus surlignés sont des processus conteneurisés.
(Cet hôte fait tourner nginx, elasticsearch et alpine.)
Que se passe-t-il quand un processus utilise trop de mémoire sur un système Linux?
Réponse simplifiée:
du swap est consommé;
au cas où le swap ne suffise pas, au final, le out-of-memory killer est invoqué;
le OOM killer exploite des heuristiques pour terminer les processus;
parfois, il tue un processus sans lien.
Que se passe-t-il quand un processus utilise trop de mémoire sur un système Linux?
Réponse simplifiée:
du swap est consommé;
au cas où le swap ne suffise pas, au final, le out-of-memory killer est invoqué;
le OOM killer exploite des heuristiques pour terminer les processus;
parfois, il tue un processus sans lien.
Que se passe-t-il quand un conteneur utilise trop de mémoire?
La même chose!
(i.e. un processus finira par être supprimé, peut-être même dans un autre conteneur.)
Le noyau Linux offre de riches mécanismes pour limiter les ressources de conteneur.
Pour l'usage mémoire, ce mécanisme fait partie du sous-système des cgroups.
Ce sous-système permet de limiter la mémoire d'un processus ou d'un groupe entier.
Un moteur de conteneur exploite ces mécanismes pour limiter la mémoire d'un conteneur.
Le OOM killer expose un nouveau comportement:
il se lance quand un conteneur dépasse la limite de mémoire autorisée;
dans ce cas, il supprime seulement les processus de ce conteneur.
Le Docker Engine offre bien des options pour limiter l'usage mémoire.
Les deux plus utiles sont --memory
et --memory-swap
.
--memory
limite la quantité de RAM physique utilisée par un conteneur.
--memory-swap
limite la quantité de mémoire totale (RAM+swap) disponible par conteneur.
La limite de mémoire peut être exprimée en octets, ou avec un suffixe d'unité.
(par ex.: --memory 100m
= 100 méga-octets.)
Nous examinerons ici deux stratégies: limiter l'usage de la RAM, ou les deux (RAM+swap).
Exemple:
docker run -ti --memory 100m python
Si le conteneur tente d'allouer plus de 100Mo de RAM, et que le swap est disponible:
le conteneur ne sera pas tué,
la mémoire au dessus de 100Mo passera dans le swap,
la plupart des cas, l'appli dans le conteneur sera ralentie (beaucoup).
Si nous saturons le swap, le OOM killer global s'activera quand même.
Exemple:
docker run -ti --memory 100m --memory-swap 100m python
Si un conteneur essaie d'utiliser plus de 100Mo de mémoire, il sera tué.
D'un autre côté, l'application ne sera jamais ralentie à cause du swap.
Les services à données persistentes (telles les bases de données) vont perdre leur données ou les corrompre si elles sont tuées.
On préfère les autoriser à utiliser l'espace swap, mais en surveiller leur usage.
Les services immuables peuvent normalement être tués avec un impact faible.
On pourra limiter leur usage mémoire+swap, mais surveiller s'ils sont tués.
Au final, cela revient à la question "ai-je besoin de swap, et combien?"
Il n'y a pas moins de trois moyens de limiter l'usage CPU:
régler la priorité relative avec --cpu-shares
,
placer une limite en pourcentage de CPU avec --cpus
,
épingler un conteneur à des CPUs spécifiques avec --cpuset-cpus
.
Ils peuvent être utilisés séparément ou ensemble.
Chaque conteneur a une priorité relative utilisée par l'ordonnanceur Linux.
Par défaut, cette priorité est de 1024.
Tant que l'usage du CPU n'est pas maximum, elle n'a pas d'effet.
Dès que le CPU atteint sa limite, chaque conteneur reçoit des cycles CPU en proportion de sa priorité relative.
Autrement dit: un conteneur avec --cpu-shares 2048
en recevra deux fois plus que celui par défaut.
Ce réglage s'assure qu'un conteneur n'utilise pas plus d'un certain pourcentage de CPU.
La limite est exprimée en CPUs; par conséquent:
--cpus 0.1
signifie 10% d'un CPU,
--cpus 1.0
signifie 100% d'un CPU entier,
--cpus 10.0
signifie 10 CPUs entiers.
Sur des machines multi-coeurs, il est possible de restreindre l'exécution à un ensemble de CPUs.
Exemples:
--cpuset-cpus 0
force le conteneur à tourner sur le CPU 0;
--cpuset-cpus 3,5,7
limite le conteneur aux CPUs 3, 5, 7;
--cpuset-cpus 0-3,8-11
épingle le conteneur aux CPUs 0, 1, 2, 3, 8, 9, 10, 11.
Cela ne réservera pas les CPUs correspondants!
(Ils peuvent toujours être sollicités par d'autres conteneurs, ou des processus non-conteneurisés)
La plupart des pilotes de stockage ne supportent pas la limite de disque par conteneur.
(À l'exception de devicemapper, mais cette limite n'est pas simple à régler.)
Cela signifie donc qu'un seul conteneur peut vider l'espace disponible pour tous.
En pratique, toutefois, ce n'est pas un souci, car:
les données (pour services persistents) devraient occuper des volumes,
les assets (par ex. images, contenu généré, etc.) devraient résider dans des banques de données ou des volumes,
les logs sont écrits en sortie standard et collectés par le moteur à conteneur.
L'usage du disque par les conteneurs peut être audité avec docker ps -s
et docker diff
.
docker versiondocker-compose -vdocker-machine -v
Docker 1.13 = Docker 17.03 (année.mois, comme Ubuntu)
Chaque mois sort une version "edge" (avec les dernières nouveautés)
Chaque trimestre sort une version "stable"
Docker CE maintient ses versions pendant au moins 4 mois
Docker EE maintient ses versions pendant au moins 12 mois
Pour plus de détails, consultez le billet de blog d'annonce de Docker EE
Docker EE:
Docker CE:
Plus lisible pour les entreprises
(i.e. les gentilles personnes qui sont assez sympas pour payer grassement nos services)
Pas d'impact sur la communauté
(en dehors du suffixe CE/EE et du changement de versioning)
Les deux lignes exploitent les mêmes composants open source
(containerd, libcontainer, swarmkit...)
Calendrier de mise à jour plus prévisible (voir diapo suivante)
2015 | 1.9 | Réseaux superposés (multi-hôte), plugins réseau/IPAM |
2016 | 1.10 | DNS dynamique embarqué |
2016 | 1.11 | Répartition de charge DNS simple (round robin) |
2016 | 1.12 | Mode Swarm, maillage de routage, réseaux chiffrés, healthchecks |
2017 | 1.13 | Stacks, réseaux attachables, aplatissement d'image et compression |
2017 | 1.13 | Swarm mode pour Windows Server 2016 |
2017 | 17.03 | Secrets, Raft chiffré |
2017 | 17.04 | Retour arrière (rollback), préférences de placement ( non coercitives) |
2017 | 17.06 | Configs swarm, events avancés, build multi-étapes, logs de services |
2017 | 17.06 | Réseaux superposés Swarm, secrets pour Windows Server 2016 |
2017 | 17.09 | chown pour ADD/COPY, start_period, signal de stop, overlay2 par défaut |
2017 | 17.12 | containerd, isolation Hyper-V, maillage de routage pour Windows |
2018 | 18.03 | Modèles pour secrets/configs, stacks à multiple yamls, LCOW |
2018 | 18.03 | Support stack natif pour K8S, docker trust , tmpfs, CLI pour manifest |
Notre application de démo
(automatically generated title slide)
Nous allons cloner le dépôt Github sur notre node1
Le dépôt contient aussi les scripts et outils à utiliser à travers la formation.
node1
:git clone https://github.com/jpetazzo/container.training
(Vous pouvez aussi forker le dépôt sur Github et cloner votre version si vous préférez.)
Démarrons-la avant de s'y plonger, puisque le téléchargement peut prendre un peu de temps...
Aller dans le dossier dockercoins
du dépôt cloné:
cd ~/container.training/dockercoins
Utiliser Compose pour générer et lancer tous les conteneurs:
docker-compose up
Compose indique à Docker de construire toutes les images de conteneurs (en téléchargeant les images de base correspondantes), puis de démarrer tous les conteneurs et d'afficher les logs agrégés.
C'est un miner de DockerCoin! 💰🐳📦🚢
Non, on ne paiera pas le café avec des DockerCoins
C'est un miner de DockerCoin! 💰🐳📦🚢
Non, on ne paiera pas le café avec des DockerCoins
Comment DockerCoins fonctionne
générer quelques octets aléatoires
calculer une somme de hachage
incrémenter un compteur (pour suivre la vitesse)
répéter en boucle!
C'est un miner de DockerCoin! 💰🐳📦🚢
Non, on ne paiera pas le café avec des DockerCoins
Comment DockerCoins fonctionne
générer quelques octets aléatoires
calculer une somme de hachage
incrémenter un compteur (pour suivre la vitesse)
répéter en boucle!
DockerCoins n'est pas une crypto-monnaie
(les seuls points communs étant "aléatoire", "hachage", et "coins" dans le nom)
DockerCoins est composée de 5 services:
rng
= un service web générant des octets au hasard
hasher
= un service web calculant un hachage basé sur les données POST-ées
worker
= un processus en arrière-plan utilisant rng
et hasher
webui
= une interface web pour le suivi du travail
redis
= base de données (garde un décompte, mis à jour par worker
)
Ces 5 services sont visibles dans le fichier Compose de l'application, docker-compose.yml
worker
invoque le service web rng
pour générer quelques octets aléatoires
worker
invoque le service web hasher
pour générer un hachage de ces octets
worker
reboucle de manière infinie sur ces 2 tâches
chaque seconde, worker
écrit dans redis
pour indiquer combien de boucles ont été réalisées
webui
interroge redis
, pour calculer et exposer la "vitesse de hachage" dans notre navigateur
(Voir le diagramme en diapo suivante!)
Comment chaque service trouve l'adresse des autres?
On ne code pas en dur des adresses IP dans le code.
On ne code pas en dur des FQDN dans le code, non plus.
On se connecte simplement avec un nom de service, et la magie du conteneur fait le reste
(Par magie du conteneur, nous entendons "l'astucieux DNS embarqué dynamique")
worker/worker.py
redis = Redis("redis")def get_random_bytes(): r = requests.get("http://rng/32") return r.contentdef hash_bytes(data): r = requests.post("http://hasher/", data=data, headers={"Content-Type": "application/octet-stream"})
(Code source complet disponible ici)
Les conteneurs peuvent avoir des alias de réseau (résolus par DNS)
Compose dans sa version 2+ rend chaque conteneur disponible via son nom de service
Compose en version 1 rendait obligatoire la section "links"
Les alias de réseau sont automatiquement préfixé par un espace de nommage
vous pouvez avoir plusieurs applications déclarées via un service appelé database
les conteneurs dans l'appli bleue vont atteindre database
via l'IP de la base de données bleue
les conteneurs dans l'appli verte vont atteindre database
via l'IP de la base de données verte
Vous pouvez ouvrir le dépôt Github avec tous les contenus de cet atelier:
https://github.com/jpetazzo/container.training
Cette application est dans le sous-dossier dockercoins
Le fichier Compose (docker-compose.yml) liste les 5 services
redis
utilise une image officielle issue du Docker Hub
hasher
, rng
, worker
, webui
sont générés depuis un Dockerfile
Chaque Dockerfile de service et son code source est stocké dans son propre dossier
(hasher
est dans le dossier hasher,
rng
est dans le dossier rng, etc.)
Uniquement pertinent si vous avez utilisé Compose avant 2016...
Compose 1.6 a introduit le support d'un nouveau format de fichier Compose (alias "v2")
Les services ne sont plus au plus haut niveau, mais dans une section services
.
Il doit y avoir une clé version
tout en haut du fichier, avec la valeur "2"
(la chaîne de caractères, pas le chiffre)
Les conteneurs sont placés dans un réseau dédié, rendant les links inutiles
Il existe d'autres différences mineures, mais la mise à jour est facile et assez directe.
A votre gauche, la bande "arc-en-ciel" montrant les noms de conteneurs
A votre droite, nous voyons la sortie standard de nos conteneurs
On peut voir le service worker
exécutant des requêtes vers rng
et hasher
Pour rng
et hasher
, on peut lire leur logs d'accès HTTP
"Les logs, c'est excitant et drôle" (Citation de personne, jamais, vraiment)
Le conteneur webui
expose un écran de contrôle web; allons-y voir.
Avec un navigateur, se connecter à node1
sur le port 8000
Rappel: les alias nodeX
ne sont valides que sur les noeuds eux-mêmes.
Dans votre navigateur, vous aurez besoin de taper l'adresse IP de votre noeud.
Un diagramme devrait s'afficher, et après quelques secondes, une courbe en bleu va apparaître.
On dirait peu ou prou que la vitesse est de 4 hachages/seconde.
Ou plus précisément: 4 hachages/secondes avec des trous reguliers à zéro
Pourquoi?
On dirait peu ou prou que la vitesse est de 4 hachages/seconde.
Ou plus précisément: 4 hachages/secondes avec des trous reguliers à zéro
Pourquoi?
L'appli a en réalité une vitesse constante et régulière de 3.33 hachages/seconde.
(ce qui correspond à 1 hachage toutes les 0.3 secondes, pour certaines raisons)
Oui, et donc?
Le worker ne met pas à jour le compteur après chaque boucle, mais au maximum une fois par seconde.
La vitesse est calculée par le navigateur, qui vérifie le compte à peu près une fois par seconde.
Entre 2 mise à jours consécutives, le compteur augmentera soit de 4, ou de 0 (zéro).
La vitesse perçue sera donc 4 - 4 - 0 - 4 - 4 - 0, etc.
Que peut-on conclure de tout cela?
Le worker ne met pas à jour le compteur après chaque boucle, mais au maximum une fois par seconde.
La vitesse est calculée par le navigateur, qui vérifie le compte à peu près une fois par seconde.
Entre 2 mise à jours consécutives, le compteur augmentera soit de 4, ou de 0 (zéro).
La vitesse perçue sera donc 4 - 4 - 0 - 4 - 4 - 0, etc.
Que peut-on conclure de tout cela?
Si nous stoppons Compose (avec ^C
), il demandera poliment au Docker Engine d'arrêter l'appli
Le Docker Engine va envoyer un signal TERM
aux conteneurs
Si les conteneurs ne quittent pas assez vite, l'Engine envoie le signal KILL
^C
Si nous stoppons Compose (avec ^C
), il demandera poliment au Docker Engine d'arrêter l'appli
Le Docker Engine va envoyer un signal TERM
aux conteneurs
Si les conteneurs ne quittent pas assez vite, l'Engine envoie le signal KILL
^C
Certains conteneurs quittent immédiatement, d'autres prennent plus de temps.
Les conteneurs qui ne gèrent pas le SIGTERM
finissent pas être tués après 10 secs. Si nous sommes vraiment impatients, on peut taper ^C
une seconde fois!
docker
Démarrer l'appli en arrière-plan avec l'option -d
:
docker-compose up -d
Vérifier que notre appli est lancée avec la commande ps
:
docker-compose ps
docker-compose ps
montre aussi les ports exposés par l'application.
docker-compose logs
marche comme docker logs
Afficher tous les logs depuis la naissance du conteneur, et sortir juste après:
docker-compose logs
Suivre le flux de logs du conteneur, en commençant par les 10 dernières lignes de chaque conteneur:
docker-compose logs --tail 10 --follow
Astuce: taper ^S
et ^Q
pour suspendre/reprendre l'affichage des logs.
Notre but est de faire monter ce graphique de performance (sans changer une ligne de code!)
Avant d'essayer de faire monter en charge l'application, voyons si plus de ressources sont nécessaires
(CPU, RAM ...)
Pour ça, nous allons lancer de bons vieux outils UNIX sur notre noeud Docker.
top
pour voir l'usage CPU et mémoire (on devrait voir des cycles de repos)vmstat 1
pour voir l'usage des entrées/sorties (si/so/bi/bo)
bo
pour le logging)Nous avons des ressources disponibles.
worker
et voyons ce qu'il se passe!Démarrer un conteneur worker
supplémentaire:
docker-compose up -d --scale worker=2
Examiner le graphique de performance (on devrait voir un doublement)
Examiner les logs agrégés de nos conteneurs (worker_2
devrait y apparaître)
Examiner l'impact sur la charge de CPU avec par ex. top (il devrait être négligable)
Démarrer huit conteneurs de worker
de plus:
docker-compose up -d --scale worker=10
Examiner le graphique de performance: est-ce que l'amélioration est x10?
Examiner les logs agrégés de nos conteneurs
Examiner l'impact sur la charge CPU et la mémoire
Identifier les goulots d'étranglement
(automatically generated title slide)
Vous devriez constater un facteur de vitesse x3 (pas x10)
Ajouter des workers ne s'est pas traduit pas un gain linéaire.
Quelque chose d'autre nous ralentit donc.
Vous devriez constater un facteur de vitesse x3 (pas x10)
Ajouter des workers ne s'est pas traduit pas un gain linéaire.
Quelque chose d'autre nous ralentit donc.
... Mais quoi?
Vous devriez constater un facteur de vitesse x3 (pas x10)
Ajouter des workers ne s'est pas traduit pas un gain linéaire.
Quelque chose d'autre nous ralentit donc.
... Mais quoi?
Le code ne dispose d'aucun appareillage de mesure.
Sortons donc notre analyseur de performance HTTP dernier cri!
(i.e les bons vieux outils comme ab
, httping
, etc.)
rng
et hasher
sont exposés sur les ports 8001 et 8002
C'est déclaré ainsi dans le fichier Compose:
...rng: build: rng ports: - "8001:80"hasher: build: hasher ports: - "8002:80"...
Nous utiliserons pour cela httping
.
Vérifier la latence de rng
:
httping -c 3 localhost:8001
Vérifier la latence de hasher
:
httping -c 3 localhost:8002
rng
révèle une latence bien plus grande que hasher
.
docker-compose down
SwarmKit
(automatically generated title slide)
SwarmKit est un projet open source sous forme de boîte à outils pour monter des systèmes multi-noeud.
C'est une bibliothèque réutilisable, comme libcontainer, libnetwork, vpnkit ...
C'est un des composants qui font la plomberie de l'éco-système Docker
SwarmKit est un projet open source sous forme de boîte à outils pour monter des systèmes multi-noeud.
C'est une bibliothèque réutilisable, comme libcontainer, libnetwork, vpnkit ...
C'est un des composants qui font la plomberie de l'éco-système Docker
🐳 Saviez-vous que кит veut dire "baleine" en russe?
Base de données distribuée, hautement disponible basée sur Raft
(évite la dépendance à une base externe, plus simple à déployer, meilleure performance)
Reconfiguration dynamique de Raft sans interruption des opérations sur le cluster.
Services gérés avec une API déclarative
(qui applique l'état souhaité grâce à la boucle de réconciliation)
Intégré avec les réseaux superposés et la répartition de charge
Accent important sur la sécurité:
Bien des systèmes d'orchestration utilisent une base clé-valeur exploitée par un algorithme de consensus
(k8s->etcd->Raft, mesos->zookeeper->ZAB, etc.)
SwarmKit implémente l'algorithme Raft directement (Nomad est similaire en ce point, merci à @cbednarski, @diptanu entre autres de l'avoir rappelé!)
Analogie offert par @aluzzardi:
C'est comme les B-trees et les SGBD. Ce sont différentes couches, souvent associées. Mais on n'a pas besoin de lancer un serveur SQL complet, si on a juste besoin d'indexer quelques données.
Par conséquent, l'orchestrateur a directement accès à la donnée
(l'original de la donnée est stocké dans la mémoire de l'orchestrateur)
Plus simple, facile à déployer et administrer; et aussi plus rapide
Un cluster est composé d'au moins une node (plus de préférence)
Une node peut prendre le rôle de manager ou worker
Un manager prend une part active dans le consensus Raft, et conserve le log du Raft
On peut dialoguer avec un manager en utilisant l'API SwarmKit
Un manager est élu en tant que leader; les autres managers ne font que lui transmettre les demandes
Les workers prennent leurs ordres des managers
Tous (workers et managers) peuvent faire tourner des conteneurs
Sur la prochaine diapo:
baleines = noeuds (workers et managers)
singes = managers
singe violet = leader
singes gris = suiveurs
triangle en pointillés = protocole Raft
Les managers exposent l'API SwarmKit
Via cette API, on peut demander à lancer un service
Un service est spécifié par son état souhaité: quelle image, combien d'instances, etc.
Le leader utilise différent sous-systèmes pour décomposer les services en tasks:
orchestrateur, ordonnanceur, allocateur, répartiteur
Une task correspond à un conteneur spécifique, assigné à une node spécifique
Les Nodes savent quelles tasks devraient tourner, et feront lancer et stopper leurs conteneurs en accord (via l'API du Docker Engine)
Vous pouvez vous référer à la NOMENCLATURE du dépôt SwarmKit pour plus de détails. swarm/swarmkit.md
Déclaratif vs Impératif
(automatically generated title slide)
Notre orchestrateur de conteneurs insiste fortement sur sa nature déclarative
Déclaratif:
Je voudrais une tasse de thé
Impératif:
Faire bouillir de l'eau. Verser dans la théière. Ajouter les feuilles de thé. Infuser un moment. Servir dans une tasse.
Notre orchestrateur de conteneurs insiste fortement sur sa nature déclarative
Déclaratif:
Je voudrais une tasse de thé
Impératif:
Faire bouillir de l'eau. Verser dans la théière. Ajouter les feuilles de thé. Infuser un moment. Servir dans une tasse.
Le mode déclaratif semble plus simple au début...
Notre orchestrateur de conteneurs insiste fortement sur sa nature déclarative
Déclaratif:
Je voudrais une tasse de thé
Impératif:
Faire bouillir de l'eau. Verser dans la théière. Ajouter les feuilles de thé. Infuser un moment. Servir dans une tasse.
Le mode déclaratif semble plus simple au début...
... tant qu'on sait comment préparer du thé
Ce que le mode déclaratif devrait vraiment être:
Je voudrais une tasse de thé, obtenue en versant une infusion¹ de feuilles de thé dans une tasse.
Ce que le mode déclaratif devrait vraiment être:
Je voudrais une tasse de thé, obtenue en versant une infusion¹ de feuilles de thé dans une tasse.
¹Une infusion est obtenue en laissant l'objet infuser quelques minutes dans l'eau chaude².
Ce que le mode déclaratif devrait vraiment être:
Je voudrais une tasse de thé, obtenue en versant une infusion¹ de feuilles de thé dans une tasse.
¹Une infusion est obtenue en laissant l'objet infuser quelques minutes dans l'eau chaude².
²Liquide chaud obtenu en le versant dans un contenant³ approprié et le placer sur la gazinière.
Ce que le mode déclaratif devrait vraiment être:
Je voudrais une tasse de thé, obtenue en versant une infusion¹ de feuilles de thé dans une tasse.
¹Une infusion est obtenue en laissant l'objet infuser quelques minutes dans l'eau chaude².
²Liquide chaud obtenu en le versant dans un contenant³ approprié et le placer sur la gazinière.
³Ah, finalement, des conteneurs! Quelque chose qu'on maitrise. Mettons-nous au boulot, n'est-ce pas?
Ce que le mode déclaratif devrait vraiment être:
Je voudrais une tasse de thé, obtenue en versant une infusion¹ de feuilles de thé dans une tasse.
¹Une infusion est obtenue en laissant l'objet infuser quelques minutes dans l'eau chaude².
²Liquide chaud obtenu en le versant dans un contenant³ approprié et le placer sur la gazinière.
³Ah, finalement, des conteneurs! Quelque chose qu'on maitrise. Mettons-nous au boulot, n'est-ce pas?
Saviez-vous qu'il existait une norme ISO spécifiant comment infuser le thé?
Système impératifs:
plus simple
si une tache est interrompue, on doit la redémarrer de zéro
Système déclaratifs:
si une tache est interrompue (ou si on arrive en plein milieu de la fête), on peut déduire ce qu'il manque, et on complète juste par ce qui est nécessaire.
on doit être en mesure d'observer le système
... et de calculer un "diff" entre ce qui tourne en ce moment et ce que nous souhaitons
Mode Swarm
(automatically generated title slide)
Depuis la version 1.12, le Docker Engine embarque SwarmKit
Toutes les fonctions SwarmKit sont mise en "sommeil" jusqu'à activer le "Mode Swarm"
Exemples de commandes Swarm Mode:
docker swarm
(active le mode Swarm; rejoint un Swarm; ajuste les paramètres du cluster)
docker node
(affiche les nodes; désigne les managers; gère les nodes)
docker service
(crée et gère les services)
L'API Docker expose les mêmes concepts
L'API SwarmKit est aussi exposée (sur une socket séparée)
Par défaut, tout ce code nouveau est inactif
Le mode Swarm doit être activé, "déverrouillant" ainsi les fonctions SwarmKit
(services, réseaux superposés prêts à l'emploi, etc.)
docker node ls
Par défaut, tout ce code nouveau est inactif
Le mode Swarm doit être activé, "déverrouillant" ainsi les fonctions SwarmKit
(services, réseaux superposés prêts à l'emploi, etc.)
docker node ls
Vous aurez un message d'erreur:
Error response from daemon: This node is not a swarm manager. [...]
Créer notre premier Swarm
(automatically generated title slide)
Le cluster est initialisé avec docker swarm init
Cette commande devrait être lancée depuis la première node d'amorçage.
NE PAS exécuter docker swarm init
sur d'autres nodes!
Vous auriez plusieurs cluster disjoints.
docker swarm init
Le cluster est initialisé avec docker swarm init
Cette commande devrait être lancée depuis la première node d'amorçage.
NE PAS exécuter docker swarm init
sur d'autres nodes!
Vous auriez plusieurs cluster disjoints.
docker swarm init
Si Docker vous dit could not choose an IP address to advertise
, regardez la prochaine diapo!
En lançant le mode Swarm, chaque noeud annonce son adresse aux autres.
(i.e. il leur dit "vous pouvez me contacter sur 10.1.2.3:2377")
Si le noeud a une seule adresse IP, c'est activé automatiquement
(Les adresses de l'interface loopback et Docker bridge sont ignorées)
Si le noeud à plusieurs adresses IP, vous devez spécifier laquelle utiliser
(Docker refusera d'en choisir une au hasard)
On peut indiquer une adresse IP ou un nom d'interface
(Dans ce dernier cas, Docker va lire l'adresse IP de l'interface et l'utiliser)
On peut aussi spécifier un numéro de port
(autrement, le port par défaut 2377 sera utilisé)
Changer le port annoncé ne change pas le port d'écoute
Si on passe uniquement --advertise-addr eth0:7777
, Swarm va quand même écouter sur 2377
Vous devrez problablement aussi passer l'option --listen-addr eth0:7777
C'est utile dans le cas où il faut s'adapter à des scénarios où les ports doivent être différents
(mapping de ports, répartiteurs de charge...)
Exemple pour lancer Swarm sur un port différent:
docker swarm init --advertise-addr eth0:7777 --listen-addr eth0:7777
Si vos noeuds ont une seule adresse IP, il est plus sûr de laisser l'auto-détection agir.
(Sauf si vos instances ont des adresses ip publiques et privées différentes, par ex. sur EC2, et que vous montez un Swarm impliquant des noeurs à l'intérieur et à l'extérieur du réseau privé: alors vous devriez annoncer l'adresse publique.)
Si vos noeuds ont plusieurs adresses IP, choisissez une adresse qui est visible par tous les autres noeuds du Swarm.
Si vous êtes sur play-with-docker, indiquez l'adresse IP affichée à coté du nom de la node.
(C'est l'adresse de votre noeud sur votre réseau privé interne superposé.
L'autre adresse que vous pourriez voir est l'adresse de votre noeud sur le réseau
docker_gwbridge
, qui est utilisée pour le trafic sortant.)
Exemples:
docker swarm init --advertise-addr 172.24.0.2docker swarm init --advertise-addr eth0
Vous pouvez indiquer différentes interfaces (ou adresses IP) pour le contrôle et la donnée.
On précisera le circuit du plan de contrôle avec --advertise-addr
et --listen-addr
(Cela sera utile pour la communication manager/worker dans SwarmKit, l'élection du leader, etc.)
On précisera le circuit du plan de données avec --data-path-addr
(Cela sera utilisé pour le trafic entre conteneurs)
Les deux options acceptent soit une adresse IP, ou un nom d'interface
(En indiquant un nom d'interface, Docker choisira sa première adresse IP)
Dans la réponse à docker swarm init
, nous avons un message
confirmant que notre noeud est maintenant le (seul) manager:
Swarm initialized: current node (8jud...) is now a manager.
Docker a généré deux jetons de sécurité (comme une phrase de passe, ou un mot de passe) pour notre cluster
La ligne de commande nous montre la commande à lancer sur les autres nodes pour les ajouter au cluster sous forme d'un jeton de sécurité:
To add a worker to this swarm, run the following command: docker swarm join \ --token SWMTKN-1-59fl4ak4nqjmao1ofttrc4eprhrola2l87... \ 172.31.4.182:2377
docker info
:docker info
L'affichage devrait comporter:
Swarm: active NodeID: 8jud7o8dax3zxbags3f8yox4b Is Manager: true ClusterID: 2vcw2oa9rjps3a24m91xhvv0c ...
docker node ls
L'affichage devrait ressembler à ce qui suit:
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS8jud...ox4b * node1 Ready Active Leader
Un cluster avec une seule node n'est pas marrant
Ajoutons node2
!
On a besoin du token qu'on a vu plus tôt
Un cluster avec une seule node n'est pas marrant
Ajoutons node2
!
On a besoin du token qu'on a vu plus tôt
Vous l'avez noté quelque part, pas vrai?
Un cluster avec une seule node n'est pas marrant
Ajoutons node2
!
On a besoin du token qu'on a vu plus tôt
Vous l'avez noté quelque part, pas vrai?
Pas de panique, on peut le retrouver facilement 😏
Afficher le token à nouveau:
docker swarm join-token worker
Se connecter à node2
:
ssh node2
Copier-coller la commande docker swarm join ...
(celle qui a été affichée juste avant)
node2
pour l'instant!docker info
pour vérifier que la node participe au Swarm:docker info | grep ^Swarm
docker node ls
node1
et voyons quelle tête a notre clusternode1
(avec exit
, Ctrl-D
...)node1
, qui est un manager:docker node ls
L'affichage devrait être similaire à ce qui suit:
ID HOSTNAME STATUS AVAILABILITY MANAGER STATUS8jud...ox4b * node1 Ready Active Leaderehb0...4fvx node2 Ready Active
Quand on lance docker swarm init
:
une paire de clés est créée pour le CA racine de notre Swarm
une paire de clés est créée sur la première node
un certificat est émis pour cette node
les tokens d'entrée sont créés
Il existe un jeton pour entrer en tant que worker, et un autre pour entrer en tant que manager.
Les tokens d'entrée ont deux parties:
une clé secrète (empêchant les nodes non autorisées d'entrer)
une empreinte digitale du certificat racine du CA (empêchant les attaques MITM)
Si un token est compromis, on peut en changer instantanément avec:
docker swarm join-token --rotate <worker|manager>
Quand une node rejoint le Swarm:
on lui délivre sa propre paire de clés, signée par le CA racine
si cette node est un manager:
si cette node est un worker:
Le plan de contrôle est chiffré avec AES-GCM; une rotation de clés intervient toutes les 12 heures
L'identification est implémentée via un TLS mutuel; la rotation de certificats se fait tous les 90 jours
(docker swarm update
permet de changer ce délai, ou d'utiliser un CA externe)
Le plan de données (communication entre conteneurs) n'est pas chiffré par défaut
(mais on peut l'activer au niveau de chaque réseau, avec IPSEC, exploitant un cryptage matériel si disponible)
Revisitez les concepts de SwarmKit:
Docker 1.12 Swarm Mode Deep Dive Part 1: Topology (video)
Docker 1.12 Swarm Mode Deep Dive Part 2: Orchestration (video)
Quelques présentations du Docker Distributed Systems Summit à Berlin:
Heart of the SwarmKit: Topology Management (slides)
Heart of the SwarmKit: Store, Topology & Object Model (slides) (video)
Et les présentations Black Belt à DockerCon
DC17US: Everything You Thought You Already Knew About Orchestration (video)
DC17EU: Container Orchestration from Theory to Practice (video)
Jusqu'ici, on a juste un manager (node1)
Si on le perd, on perd le quorum, et c'est très grave!
Les conteneurs sur les autres nodes n'auront pas de problème...
Si le manager est parti pour de bon, on va devoir réparer à la main!
Personne ne veut faire ça... donc passons en haute disponibilité
Obtenir le jeton manager:
TOKEN=$(docker swarm join-token -q manager)
Ajouter la node qui reste:
ssh node3 docker swarm join --token $TOKEN node1:2377
docker node ls
Sur les noeuds manager:
vous verrez la liste des nodes, avec un *
,
indiquant la node qui répond.
Sur les nodes non-manager:
un message d'erreur s'affichera vous disant que
cette node n'est pas un manager.
Comme vu plus haut, on ne peut contrôler le Swarm que depuis une node manager.
On peut changer le rôle d'une node dynamiquement:
docker node promote nodeX
→ passer nodeX en manager
docker node demote nodeX
→ passer nodeX en worker
Afficher la liste courante des noeuds:
docker node ls
Promouvoir tout worker en manager
docker node promote <node_name_or_id>
2N+1 noeuds peuvent (et vont) résister à N pannes
(vous pouvez mettre un nombre pairs de managers mais ça n'a aucun intérêt)
1 manager = pas de tolérance de panne
3 managers = tolérance d'une panne
5 managers = tolérance de 2 pannes (ou 1 panne pendant 1 maintenance)
7 managers ou plus = là vous forcez peut-être un peu sur l'archi
voir Docker's admin guide à propos des pannes de nodes et la redondance en centre de données
Avec Raft, les écritures doivent atteindre (et être confirmées par) tous les noeuds.
Par conséquent, c'est plus dur d'atteindre un consensus dans des groupes plus grands.
Un seul manager est Leader (en écriture), donc "plus de managers ≠ plus de capacité"
Les managers devraient être < 10ms de latence entre eux
Ces facteurs de conceptions nous amènent à de meilleurs designs
Garder les managers dans une région (ou multi-zone/centre de données/rack)
Groupe de 3 à 5 noeuds: tous en managers. Au-delà de 5, séparer les managers et workers
Groupe de 10-100 noeuds: prendre 5 noeuds "stables" pour être les managers
Groupe de plus de 100 noeuds: surveiller le CPU et la RAM des managers
Astuce de pro du cloud: utiliser des groupes auto-scalings distincts pour les managers et workers
Voir le document chez Docker Inc. "Running Docker at scale"
On n'en sait rien!
Tests internes chez Docker Inc.: 1000 à 10000 noeuds sans problème
déployés sur une région de cloud
une des principales leçons était "vous allez avoir besoin d'un plus gros manager"
Tests menés par la communauté: 4700 noeuds hétérogènes à travers le net
ça marche et c'est tout, à condition d'avoir les ressources nécessaires
plus de nodes demandent plus de CPU et réseau pour le manager; plus de conteneurs demande plus de RAM
l'orchestration à très large échelle (70 000 conteneurs) est lente et difficile (mais ça s'arrange!)
Lancer les commandes à la main via SSH
Lancer les commandes à la main via SSH
(lol je blague)
Lancer les commandes à la main via SSH
(lol je blague)
Exploiter votre outil de gestion de configuration préféré
Lancer notre premier service Swarm
(automatically generated title slide)
Comment lancer des services? Version courte:
docker run
→ docker service create
Créer un service basé sur un conteneur Alpine qui ping les serveurs de Google:
docker service create --name pingpong alpine ping 8.8.8.8
Vérifier le résultat:
docker service ps pingpong
(Nouveau dans Docker Engine 17.05)
Tout comme docker logs
affiche la sortie d'un conteneur local spécifique...
... docker service logs
montre les logs de tous les conteneurs d'un certain service
docker service logs pingpong
Les options --follow
et --tail
sont disponibles, ainsi que quelques autres.
Note: par défaut, quand un conteneur est détruit (par ex. en baisse de charge), ses logs sont perdus.
docker service ps
va nous dire où est placé notre conteneurTrouver quel NODE
fait tourner notre conteneur:
docker service ps pingpong
Sur Play-With-Docker, cliquer sur l'onglet de cette node, ou passer par DOCKER_HOST
Autrement, se connecter avec ssh
sur cette ou lancer $(eval docker-machine env node...)
Constater que le conteneur tourne bien, et récupérer son ID:
docker ps
Afficher ses logs:
docker logs containerID
Retourner sur node1
après coup
docker service update
Escalader le service pour assurer 2 clones par node:
docker service update pingpong --replicas 6
Vérifier que nous avons bien 2 conteneurs sur la node en cours:
docker ps
--detach
(Nouveauté du Docker Engine 17.10)
La ligne de commande surveille les commandes qui créent/modifient/suppriment les services.
En pratique, --detach=false
est la valeur par défaut
--detach=true
--detach
ou ne pas --detach
, là est la question--detach=false
--detach=true
super pour des opérations indépendantes qui peuvent être parallélisées.
super pour des scripts non-interactifs (où personne ne regarde de toute façon)
--detach=true
ne va pas plus vite. C'est juste qu'il n'attend pas la fin d'exécution.
--detach
Docker Engine 17.10 et plus: par défaut en --detach=false
A partir de Docker Engine 17.05 à 17.09: par défaut en --detach=true
Avant Docker 17.05: --detach
n'existait pas.
(Vous pouvez le remplacer par ex. avec watch docker service ps <serviceID>
)
--detach
en actionEscalader le service pour garantir 3 conteneurs par node:
docker service update pingpong --replicas 9 --detach=false
Et monter ensuite à 4 replicas par node:
docker service update pingpong --replicas 12 --detach=true
Exposer un service est possible, avec deux propriétés spéciales:
le port public est disponible sur chaque node du Swarm,
les requêtes provenant du port public sont réparties entre toutes les instances.
Techniquement, on utilise l'option -p/--publish
; pour faire vite:
docker run -p → docker service create -p
Si vous indiquer un seul numéro de port, il sera mappé sur un port démarrant
à 30000
(vs. 32768 pour un mappage de conteneur unique)
On peut indiquer deux numéros de port pour configurer le numéro de port public
(tout comme avec docker run -p
)
docker service create --name search --publish 9200:9200 --replicas 5 \ elasticsearch:2
Note: ne pas oublier le tag :2!
La dernière version de l'image ElasticSearch ne peut démarrer sans une configuration obligatoire.
Pendant un déploiement, vous pourrez voir les étapes suivantes:
assigned, la task a été assignée à un noeud spécifique
preparing, qui se résume à "téléchargement de l'image"
starting
running
Quand une tâche est terminée (stopped, killed, etc.) elle ne peut être redémarrée
(Une tâche de remplacement sera créée)
Nous avons attaché le port 9200 sur les nodes au port 9200 des conteneurs.
Essayons de communiquer avec ce port!
curl 127.0.0.1:9200
(Si vous recevez un Connection refused
: félicitations, vous êtes vraiment rapide! Essayez encore.)
ElasticSearch renvoie un petit doc. JSON avec des informations de base sur cette instance; y compris un nom de super-héros aléatoire.
curl
encore et encore, on lire plusieurs noms.for N in $(seq 1 10); do curl -s localhost:9200 | jq .namedone
Note: si vous n'avez pas jq
sur votre instance PWD, il suffit de l'installer:
apk add --no-cache jq
Le trafic est géré par le maillage de routage de notre cluster
Chaque requête est tour à tour transférée à une des instances.
Note: si vous essayez d'accéder au service depuis un navigateur, vous verrez probablement la même instance encore et encore, c'est parce que votre navigateur (contrairement à curl) essaiera de ré-utiliser la même connexion.
La répartition de charge est réalisée avec IPVS
IPVS est un répartiteur de charge de haute performance, interne au noyau
Il existe depuis quelque temps déjà (introduit dans la version 2.4)
Chaque noeud exécute un répartiteur de charge local
(Ce qui permet aux connections d'être routées directement à leur destination, sans sauts superflus)
Il y a bien des manières de s'occuper du trafic entrant dans un cluster SWarm.
Placer tout (ou partie) des noeuds dans un champ A
du DNS (marche bien pour les clients web)
Assigner tout ou partie des nodes à un répartiteur de charge externe (ELB, etc.)
Utiliser une IP virtuelle et s'assurer qu'elle est assignée à une node "vivante"
etc.
Le routing mesh TCP n'interprète par les en-tête HTTP
Si on veut placer plusieurs services HTTP sur le port 80/443, il nous manque un truc.
On peut installer NGINX ou HAProxy sur le port 80/443 pour router les requêtes vers le bon service, mais ils auraient besoin d'écouter le Swarm pour ajuster leur config.
Le routing mesh TCP n'interprète par les en-tête HTTP
Si on veut placer plusieurs services HTTP sur le port 80/443, il nous manque un truc.
On peut installer NGINX ou HAProxy sur le port 80/443 pour router les requêtes vers le bon service, mais ils auraient besoin d'écouter le Swarm pour ajuster leur config.
Docker EE fournit son propre routeur de niveau 7
com.docker.lb.hosts=<FQDN>
sont détectés automatiquement via l'API Docker et mettent à jour leur configuration à la volée.Le routing mesh TCP n'interprète par les en-tête HTTP
Si on veut placer plusieurs services HTTP sur le port 80/443, il nous manque un truc.
On peut installer NGINX ou HAProxy sur le port 80/443 pour router les requêtes vers le bon service, mais ils auraient besoin d'écouter le Swarm pour ajuster leur config.
Docker EE fournit son propre routeur de niveau 7
com.docker.lb.hosts=<FQDN>
sont détectés automatiquement via l'API Docker et mettent à jour leur configuration à la volée.Deux options open source populaires:
Traefik - reconnu, riche de fonctions, requiert de tourner sur les managers (par défaut), nécessite une DB clé-valeur pour la haute disponibilité.
Docker Flow Proxy - utilise HAProxy, orienté Swarm, par @vfarcic
"Labelliser": verbe, par ex. attacher des informations arbitraires aux services
Exemples:
le vhost HTTP d'une web app ou d'un service web
planifier la sauvegarde de service à données persistentes
propriétaire d'un service (pour la facturation, l'astreinte, etc.)
grouper les objets Swarm entre eux (services, volumes, configs, secrets, etc.)
Il est possible d'utiliser un réseau local avec les services Swarm
Cela signifie qu'on peut faire quelque chose comme:
docker service create --network host --mode global traefik ...
(Ça va lancer le load balancer traefik
sur chaque noeud de votre cluster, sur le reseau host
)
On y gagne une performance native (pas de iptables, ni proxy, ni rien!)
Le répartiteur de charge "verra" les adresses IP des clients
Mais: le conteneur ne peut être en même temps dans le réseau host
et dans un autre.
(Vous devrez router le trafic aux conteneurs via des ports exposés ou des sockets UNIX)
host
, macvlan
...)Il est possible de connecter les services aux réseaux locaux
Passer par le réseau host
est plutôt simple
(Avec les réserves décrites dans la diapo précédente)
Pour d'autres pilotes réseaux, c'est un poil plus compliqué
(l'allocation d'IP peut nécessiter une coordination entre les nodes)
Voir par exemple ce guide pour bien démarrer avec macvlan
Voir cette PR pour plus d'information sur les pilotes réseaux locaux dans le mode Swarm
Lancer cette appli simple-mais-sympa de visualisation:
cd ~/container.training/stacksdocker-compose -f visualizer.yml up -d
Faire pointer le navigateur sur le port 8080 de votre node1 (son adresse IP)
(Sur Play-With-Docker, cliquez sur le bouton (8080))
L'appli web met à jour l'affichage à la volée (pas besoin de recharger la page)
Elle affiche juste les services Swarm (pas les conteneurs indépendants)
Elle indique quand les noeuds sont disponibles ou pas.
Il y reste quelques couacs (ce n'est pas du logiciel de Classe-Entreprise, ISO-9001, à résistance thermo-nucléaire)
Le visualiseur accède à l'API Docker de l'intérieur du conteneur
C'est un motif courant: lancer des outils de managements dans un conteneur
Au lieu d'afficher notre cluster, on pourrait traiter les logs, les métriques, la montée en charge automatique...
On peut le lancer en tant que service, aussi! On ne le fera pas tout de suite, mais la commande ressemblerait à:
docker service create \ --mount source=/var/run/docker.sock,type=bind,target=/var/run/docker.sock \ --name viz --constraint node.role==manager ...
Crédits: le code de visualization code a été écrit par Francisco Miranda.
Mano Marks l'a adapté au Swarm et le maintient.
Avant de poursuivre, on va supprimer ces services
docker service rm
accepte plusieurs noms ou IDs de services
docker service ls
accepte l'option -q
Un bout de code Shell par jour éloigne la dette technique pour toujours.
docker service ls -q | xargs docker service rm
Notre appli sur Swarm
(automatically generated title slide)
Dans cette partie, nous allons:
générer les images pour notre appli,
envoyer ces images dans un registre,
lancer les services basés sur ces images.
Avec docker-compose up
, les images sont générées pour nos services
Ces images sont présentes uniquement sur la node locale
Nous avons besoin de distribuer ces images à travers tout le Swarm
Le plus simple pour ceci est d'utiliser un Registry Docker
Une fois nos images transférées sur un registre, nous les téléchargeons au moment de créer nos services.
Si nous avions seulement un service (généré à partir d'un Dockerfile
dans le dossier en cours), notre processus aurait cette tête:
docker build -t jpetazzo/doublerainbow:v0.1 .docker push jpetazzo/doublerainbow:v0.1docker service create jpetazzo/doublerainbow:v0.1
Il nous reste juste à l'adapter à notre application, qui comporte 4 services!
Lancer un build sur notre node locale (node1
)
Étiquetter les images pour les nommer en localhost:5000/<nom-du-service>
Téléverser dans le registre
Créer les services grâce à ces images
Docker Hub
Docker Trusted Registry
Docker open source registry
Tout plein d'autres options dans le cloud ou pas
Si on voulait passer par Docker Hub...
On devrait se connecter au Docker Hub
docker login
Et dans les diapos suivantes, on pourrait utiliser notre compte Docker Hub
(par ex., jpetazzo
au lieu de l'adresse du registre, i.e 127.0.0.1:5000
)
Si on voulait utiliser DTR, on devrait...
S'assurer d'avoir un compte Docker Hub
Installer DTR sur nos machines
Marquer dtraddress:port/user
au lieu de l'adresse du registre
Tout ça est hors de notre périmètre
Héberger notre propre Registry
(automatically generated title slide)
On souhaite lancer un conteneur registry
Il va stocker les images et layers dans le système de fichiers local
(mais on peut y ajouter un fichier de conf pour passer sur S3, Swift, etc.)
Docker exige TLS pour communiquer avec le registre
excepté pour les registres sur 127.0.0.1
(i.e localhost
)
ou avec l'option globale --insecure-registry
127.0.0.1:5000
sur chaque nodeDéclarer le service du registre:
docker service create --name registry --publish 5000:5000 registry
Essayer maintenant la commande suivante: on devrait voir {"repositories":[]}
:
curl 127.0.0.1:5000/v2/_catalog
Vérifier qu'on a une image busybox, et y rajouter un tag:
docker pull busyboxdocker tag busybox 127.0.0.1:5000/busybox
La livrer sur le registre:
docker push 127.0.0.1:5000/busybox
curl http://127.0.0.1:5000/v2/_catalog
La commande curl devrait afficher:
{"repositories":["busybox"]}
On a vu comment build à la main, étiquetter et pousser les images dans un registre.
Mais ...
On a vu comment build à la main, étiquetter et pousser les images dans un registre.
Mais ...
Je suis tellement content que mon déploiement se base sur des scripts Shell de taille astronomique
(par M. Personne, vraiment)
On a vu comment build à la main, étiquetter et pousser les images dans un registre.
Mais ...
Je suis tellement content que mon déploiement se base sur des scripts Shell de taille astronomique
(par M. Personne, vraiment)
Les Stacks Swarm
(automatically generated title slide)
Compose est super pour le développement local
On peut aussi gérer le cycle de vie des images avec
(i.e générer les images et les pousser dans un registre)
Les fichiers Compose en v2 sont bons dans le développement local
Les fichiers Compose en v3 sont orientés vers le déploiement en production!
(Nouveau dans Docker Engine 1.13)
Pratiquement identique à la version 2
Peut être directement appliqué à un cluster Swarm avec les commandes docker stack ...
Introduit une section deploy
pour spécifier les options Swarm
Les limites de ressources sont placées dans cette section deploy
Voir ici pour une liste complète de changements
Supplante les Distributed Application Bundles
(Un format JSON décrivant une application; pouvait à l'origine être généré depuis un fichier Compose)
On a besoin d'un registre pour déplacer les images de point en point.
Sans ce fichier stack, on devrait taper les commandes suivantes:
docker service create --publish 5000:5000 registry
Dès lors, nous allons le déployer avec le fichier de stack suivant:
version: "3"services: registry: image: registry ports: - "5000:5000"
stacks
Aller dans le dossier stack
:
cd ~/container.training/stacks
Parcourir registry.yml
cat registry.yml
Toutes les commandes de manipulation de stacks commencent avec docker stack
En coulisses, ça se traduit par des commandes docker service
Une stack porte un nom principal (qui sert aussi de namespace)
Une stack est portée avant tout par un fichier Compose de version 3 comme dit plus haut
docker stack deploy --compose-file registry.yml registry
docker stack ps
affiche l'état détaillé de tous les services d'une stackVérifier que notre registre tourne correctement:
docker stack ps registry
Confirmer que nous avons le même affichage avec la commande:
docker service ps registry_registry
Notre registre n'est pas exactement identique à celui déployé par docker service create
!
Chaque stack possède son propre réseau superposé (overlay)
Les services de la stack sont connectés à ce réseau
(sauf si spécifié autrement dans le fichier Compose)
Les services récupèrent des alias de réseau correspondant à leur nom dans le fichier Compose
(tout comme quand Compose lance une appli en version 2)
Les services sont nommés explicitement <nom_de_stack>_<nom_de_service>
Les services et tâches récupèrent aussi un label interne indiquant à quelle stack ils appartiennent
Accéder au port 5000 de n'importe quelle node nous redirige vers le registre
Par conséquent, on peut indiquer localhost:5000
ou 127.0.0.1:5000
comme notre registre
curl 127.0.0.1:5000/v2/_catalog
Ça devrait renvoyer:
{"repositories":[]}
Si ça ne marche pas, ré-essayer encore; le conteneur est peut-être en cours de démarrage.
Charger l'image busybox, et la re-tag:
docker pull busyboxdocker tag busybox 127.0.0.1:5000/busybox
La transférer:
docker push 127.0.0.1:5000/busybox
curl http://127.0.0.1:5000/v2/_catalog
La commande curl devrait maintenant afficher:
"repositories":["busybox"]}
Avec le fichier Compose version 2 et plus, vous pouvez spécifier à la fois build
et image
Quand les deux clés sont utilisées:
Compose fait "comme si de rien n'était" (activer le build
)
mais l'image résultante sera nommée selon la valeur de la clé image
(au lieu de <nom-du-projet>_<nom-du-service>:latest
)
avec l'avantage qu'on peut la pousser dans un registry avec docker-compose push
Exemple:
webfront: build: www image: myregistry.company.net:5000/webfront
docker-compose -f dockercoins.yml builddocker-compose -f dockercoins.yml push
Voyons voir à quoi ressemble le fichier dockercoins.yml
pendant que les images sont construites et poussées.
version: "3"services: rng: build: dockercoins/rng image: ${REGISTRY-127.0.0.1:5000}/rng:${TAG-latest} deploy: mode: global ... redis: image: redis ... worker: build: dockercoins/worker image: ${REGISTRY-127.0.0.1:5000}/worker:${TAG-latest} ... deploy: replicas: 10
docker stack deploy --compose-file dockercoins.yml dockercoins
On peut maintenant se connecter à n'importe quel noeud sur le port 8000, et revoir le graphe familier du hachage.
Plusieurs méthodes existent pour gérer les variations entre environnements.
Compose charge docker-compose.yml
et (s'il existe) docker-compose.override.yml
Compose va charger plusieurs fichiers en accumulant l'option -f
ou la variable d'environnement COMPOSE_FILE
Les fichiers Compose peuvent étendre d'autres fichier Compose, pour y inclure des services:
web: extends: file: common-services.yml service: webapp
Voir cette page de documentation pour plus de détails sur ces techniques.
Un fichier Compose en version 3 comporte une section deploy
Les versions plus récentes (3.1, ...) ajoutent plus de fonctions (secrets, configs, etc.)
Mettre à jour la stack consiste à relancer docker stack deploy
On peut changer le service à coups de docker service update
...
... Mais tout changement sera annulé après chaque docker stack deploy
(C'est le comportement attendu, si on y réfléchit bien!)
extends
ne marche pas avec docker stack deploy
(Mais vous pouvez passer par docker-compose config
pour "aplatir" votre conf)
On a vu comment installer un Swarm
On l'a utilisé pour héberger notre propre Registry
On a généré nos images de conteneurs
On a utilisé la Registry pour héberger ces images
On a déployé et escaladé notre application
On a vu comment exploiter Compose pour simplifier les déploiements
Super boulot à toute l'équipe!
CI/CD avec Docker et l'orchestration
(automatically generated title slide)
Une note rapide à propos de l'intégration rapide et du déploiement
Vous n'allez pas monter dans cet atelier vos propres automatisations CI/CD
On va tricher un peu en générant les images sur les serveurs hôtes et non sur l'outil "CI".
Docker et l'orchestration fonctionne avec tous les outils de CI et de déploiement.
En premier, c'est à la CI de build les images, puis de lancer les tests à l'intérieur, avant de les pousser vers la Registry
En cas de scan de sécurité, faites-le sur les images générées, après les tests mais avant de les pousser.
En option, déployer en continu depuis votre CI, si les phases de build/test/push passent
L'outil de CD accèderait ensuite aux noeuds via SSH, ou exploiterait la ligne de commande Docker pour discuter avec le moteur de conteneur distant.
Si disponible, on passerait par l'API TCP du Docker Engine (où l'API Swarm vit aussi)
Docker KBase Development Pipeline Best Practices
Docker KBase Continuous Integration with Docker Hub
Docker KBase Building a Docker Secure Supply Chain
Mettre à jour les services
(automatically generated title slide)
On doit mettre à jour l'interface web.
Pour ce faire, le processus est comme suit:
patcher le code
générer une nouvelle image (build)
stocker cette image à distance (ship)
lancer la nouvelle version (run)
service update
Pour mettre à jour un seul service, on pourrait procéder comme suit:
export REGISTRY=127.0.0.1:5000export TAG=v0.2IMAGE=$REGISTRY/dockercoins_webui:$TAGdocker build -t $IMAGE webui/docker push $IMAGEdocker service update dockercoins_webui --image $IMAGE
Assurez-vous de mettre le bon tag sur l'image: modifier le TAG
à chaque itération
(Quand vous allez vérifier quelles images tournent, on a intérêt à disposer de tags uniques et explicites)
stack deploy
export TAG=v0.2docker-compose -f composefile.yml builddocker-compose -f composefile.yml pushdocker stack deploy -c composefile.yml nameofstack
stack deploy
export TAG=v0.2docker-compose -f composefile.yml builddocker-compose -f composefile.yml pushdocker stack deploy -c composefile.yml nameofstack
C'est exactement ce que nous avons utilisé plus tôt pour déployer l'appli
Pas besoin d'apprendre de nouvelles commandes!
Docker va calculer la différence pour chaque service et ne mettre à jour que ce qui a changé.
sed -i "s/15px/50px/" dockercoins/webui/files/index.html
Quatre étapes:
TAG
docker-compose build
docker-compose push
docker stack deploy
export TAG=v0.2docker-compose -f dockercoins.yml builddocker-compose -f dockercoins.yml pushdocker stack deploy -c dockercoins.yml dockercoins
Attendez au moins 10 secondes (pour laisser arriver la nouvelle version)
Puis rechargez l'interface web
Ou pianoter frénétiquement sur F5 (Cmd-R sur Mac)
... le texte de la légende sur la gauche finira par grossir!
Mises à jour progressive
(automatically generated title slide)
Escalader d'abord hasher à 7 replicas:
docker service scale dockercoins_hasher=7
Forcer une mise à jour progressive (qui remplace les conteneurs) vers une image différente:
docker service update --image 127.0.0.1:5000/hasher:v0.1 dockercoins_hasher
Vous pouvez lancer docker events
sur un autre terminal de node1
pour voir les actions du Swarm.
Vous pouvez forcer --force
pour remplacer les conteneurs sans changer la configuration.
On peut jouer sur plein d'options sur les profils de mise à jour.
docker service update --update-parallelism 2 \--update-max-failure-ratio .25 dockercoins_hasher
Aucun conteneur n'a été remplacé, c'est ce qu'on appelle une mise à jour "sans op".
Les patch de méta-données pures n'exigent aucune opération de l'orchestrateur
Cette même politique peut aussi apparaître dans le fichier Compose.
On le fait en ajoutant une clé update_config
sous la clé deploy
:
deploy: replicas: 10 update_config: parallelism: 2 delay: 10s
A n'importe quel moment (par ex. avant la fin de la mise à jour), on peut tout annuler:
en modifiant le fichier Compose et relancer une mise à jour
en utilisant l'option --rollback
de service update
en utilisant docker service rollback
docker service rollback dockercoins_webui
Que se passe-t-il avec le graphique de l'interface web?
Le retour arrière annule la dernière définition du service
PreviousSpec
dans docker service inspect <nom_du_service>
Si nous nous représentons les mises à jour comme une pile:
ça ne va pas "dépiler" la dernière mise à jour
cela va "empiler" une copie de la dernière mise à jour tout en haut de la pile
ergo, opérer deux retours arrière successifs ne change rien.
La "définition de service" inclut la cadence de déploiement.
Chaque commande docker service update
= une nouvelle définition de service
SwarmKit va mettre à jour N instances à la fois
(suivant la valeur de l'option update-parallelism
)
De nouvelles tâches sont créées, et leur état souhaité est réglé à Ready
(cela va télécharger l'image si nécessaire, s'assurer de la disponibilité des ressources, créer le conteneur ... sans encore le démarrer)
Si une nouvelle tâche échoue à atteindre le statut Ready
, retour à l'étape précédente
(SwarmKit va persister à essayer, jusqu'à dépannage du problème, ou si l'état souhaité est mis à jour)
Quand de nouvelles tâches sont Ready
, les anciennes sont passées en Shutdown
Quand d'anciennes tâches sont Shutdown
, le Swarm en démarre de nouvelles
Puis il attend le délai indiqué dans update-delay
, et poursuit avec le prochain groupe d'instances.
Healthcheck et rollback automatique
(automatically generated title slide)
(Nouveau depuis Docker Engine 1.12)
Des commandes exécutées à intervalles réguliers dans un conteneur.
Doit retourner 0 ou 1 pour indiquer "Tout va bien" ou "Quelque chose me bloque"
Doit s'exécuter rapidement (timeout == erreurs)
Exemple:
curl -f http://localhost/_ping || false
-f
s'assure que curl
retourne un statut non-nul pour 404 et autres erreurs|| false
garantit que tout code de sortie non nul se traduira par 1curl
doit être installé dans le conteneur à vérifierDans un Dockerfile, avec l'instruction HEALTHCHECK
HEALTHCHECK --interval=1s --timeout=3s CMD curl -f http://localhost/ || false
Depuis la ligne de commande, en lançant conteneurs ou services
docker run --health-cmd "curl -f http://localhost/ || false" ...docker service create --health-cmd "curl -f http://localhost/ || false" ...
Depuis les fichiers Compose, avec une section healthcheck par service
www: image: hellowebapp healthcheck: test: "curl -f https://localhost/ || false" timeout: 3s
Avec docker run
, tout contrôle de santé est purement informatif
docker ps
affiche le dernier "bilan" de santé
docker inspect
détaille certaines infos (comme la commande utilisée pour le contrôle)
Avec docker service
:
les tâches en mauvaise santé sont supprimées (i.e le service est redémarré)
les déploiements en échec peuvent être annulés automatiquement
(en spécifiant au moins l'option --update-failure-action rollback
)
Voici un exemple complet utilisant la ligne de commande:
docker service update \ --update-delay 5s \ --update-failure-action rollback \ --update-max-failure-ratio .25 \ --update-monitor 5s \ --update-parallelism 1 \ --rollback-delay 5s \ --rollback-failure-action pause \ --rollback-max-failure-ratio .5 \ --rollback-monitor 5s \ --rollback-parallelism 0 \ --health-cmd "curl -f http://localhost/ || exit 1" \ --health-interval 2s \ --health-retries 1 \ --image votre-image:nouvelle-version votre_service
On se basera pour la démo sur le fichier suivant (stacks/dockercoins+healthcheck.yml
):
... hasher: build: dockercoins/hasher image: ${REGISTRY-127.0.0.1:5000}/hasher:${TAG-latest} healthcheck: test: curl -f http://localhost/ || exit 1 deploy: replicas: 7 update_config: delay: 5s failure_action: rollback max_failure_ratio: .5 monitor: 5s parallelism: 1...
dockercoins
On a d'abord besoin d'indiquer un healthcheck pour nos services.
Entrer dans le dossier stacks
:
cd ~/container.training/stacks
Déployer la stack mise à jour avec les healthchecks intégrés
docker stack deploy --compose-file dockercoins+healthcheck.yml dockercoins
Voici un bon exemple de l'importance des healthchecks
Dans cette nouvelle version, un bug va empêcher l'appli d'écouter sur le bon port
Le conteneur va bien se lancer, sauf qu'aucune connexion sur le port 80 n'est possible
Changer le port HTTP à écouter:
sed -i "s/80/81/" dockercoins/hasher/hasher.rb
Générer, livrer, et exécuter la nouvelle image:
export TAG=v0.3docker-compose -f dockercoins+healthcheck.yml builddocker-compose -f dockercoins+healthcheck.yml pushdocker service update --image=127.0.0.1:5000/hasher:$TAG dockercoins_hasher
--health-cmd string Command to run to check health--health-interval duration Time between running the check (ms|s|m|h)--health-retries int Consecutive failures needed to report unhealthy--health-start-period duration Start period for the container to initialize before counting retries towards unstable (ms|s|m|h)--health-timeout duration Maximum time to allow one check to run (ms|s|m|h)--no-healthcheck Disable any container-specified HEALTHCHECK--restart-condition string Restart when condition is met ("none"|"on-failure"|"any")--restart-delay duration Delay between restart attempts (ns|us|ms|s|m|h)--restart-max-attempts uint Maximum number of restarts before giving up--restart-window duration Window used to evaluate the restart policy (ns|us|ms|s|m|h)--rollback Rollback to previous specification--rollback-delay duration Delay between task rollbacks (ns|us|ms|s|m|h)--rollback-failure-action string Action on rollback failure ("pause"|"continue")--rollback-max-failure-ratio float Failure rate to tolerate during a rollback--rollback-monitor duration Duration after each task rollback to monitor for failure (ns|us|ms|s|m|h)--rollback-order string Rollback order ("start-first"|"stop-first")--rollback-parallelism uint Maximum number of tasks rolled back simultaneously (0 to roll back all at once)--update-delay duration Delay between updates (ns|us|ms|s|m|h)--update-failure-action string Action on update failure ("pause"|"continue"|"rollback")--update-max-failure-ratio float Failure rate to tolerate during an update--update-monitor duration Duration after each task update to monitor for failure (ns|us|ms|s|m|h)--update-order string Update order ("start-first"|"stop-first")--update-parallelism uint Maximum number of tasks updated simultaneously (0 to update all at once)
Gestion des secrets et chiffrement au repos
(automatically generated title slide)
(Nouveau dans Docker Engine 1.13)
Gestion des secrets = lier les secrets et les services quand il le faut, et en toute sécurité
Chiffrement au repos = protéger contre le vol de données et l'espionnage
Rappelez-vous:
le plan de contrôle est authentifié via un TLS mutuel, dont les certificats sont renouvelés tous les 90 jours
le plan de contrôle est chiffré en AES-GCM, et ses clés sont renouvelées toutes les 12 heures.
le plan de données n'est pas chiffré par défaut (pour raison de performance),
mais nous avons vu plus haut comment l'activer avec une seule option
Docker a son propre "coffre-fort à secrets" (pour stockage chiffré de clé-valeur)
Vous pouvez y déposer autant de secrets que vous souhaitez
On peut ensuite associer ces secrets aux services
Les secrets sont exposés comme des fichiers texte simples,
mais ils sont conservés en mémoire (via tmpfs
)
Les secrets sont immuables (depuis Docker Engine 1.13)
Un secret peut peser jusqu'à 500Ko
piratemoi
:echo love | docker secret create piratemoi -
Si le secret est dans un fichier, on peut simplement pointer vers le chemin complet du fichier.
(Le chemin spécial -
indique que la source est l'entrée standard stdin)
base64 /dev/urandom | head -c16 | docker secret create rienadeclarer -
Note: dans ce cas, on n'a même aucune idée du mot de passe. Mais Swarm, le sait, lui.
docker service create \ --secret piratemoi --secret rienadeclarer \ --name dummyservice \ --constraint node.hostname==$HOSTNAME \ alpine sleep 1000000000
On force le conteneur à être sur la node locale pour plus de convenance.
(On va lancer un docker exec
dans la foulée!)
/run/secrets
(qui est en réalité un système de fichiers sur-mémoire)Trouver l'ID du conteneur pour le service de test:
CID=$(docker ps -q --filter label=com.docker.swarm.service.name=dummyservice)
Entrer dans le conteneur:
docker exec -ti $CID sh
Vérifier les fichiers dans /run/secrets
On ne peut mettre à jour un secret
(On dirait un inconvénient au premier abord; mais cela permet des rollbacks propres si un changement de secret se passe mal)
Vous pouvez ajouter un secret à un service avec docker service update --secret-add
(Cela va redéployer le service; le secret ne sera pas ajouté à la volée)
Vous pouvez retirer un secret avec docker service update --secret-rm
Les secrets peuvent être associés à des noms différents en utilisant un micro-format:
docker service create --secret source=secretname,target=filename
piratemoi
avec une meilleure version.Retirer le secret piratemoi
trop faible:
docker service update dummyservice --secret-rm piratemoi
Ajouter notre meilleur secret à sa place:
docker service update dummyservice \ --secret-add source=rienadeclarer,target=piratemoi
Attendez que le service soit complètement mis à jour, avec par ex. watch docker service ps dummyservice
.
(Avec Docker Engine 17.10 et plus, la CLI attendra pour vous!)
docker exec
!Récupérer l'ID de notre nouveau conteneur:
CID=$(docker ps -q --filter label=com.docker.swarm.service.name=dummyservice)
Vérifier le contenu des fichiers secrets:
docker exec $CID grep -r . /run/secrets
A consommer sans modération, jusqu'à stocker des fichiers de config complets
Pour renouveler un secret foo
, renommez-le plutôt foo.N
, et l'attacher à foo
(N peut être un compteur, un timestamp ...)
docker service create --secret source=foo.N,target=foo ...
On peut mettre à jour (ajouter+supprimer) en une seule commande:
docker service update ... --secret-rm foo.M --secret-add source=foo.N,target=foo
Pour plus de détails et d'exemples, voir la documentation
Modèle du moindre privilège
(automatically generated title slide)
Toute la donnée importante est stockée dans le "journal Raft"
Les noeuds des managers y ont accès en lecture/écriture
les noeuds type workers n'ont aucun accès à cette donnée
Les workers ne font que recevoir le strict nécessaire pour savoir:
Faire tomber un noeud worker ne donne pas accès au cluster en entier swarm/leastprivilege.md
Je peux m'introduire dans les conteneurs lancés sur ce noeud
Je peux accéder à la configuration et aux secrets utilisés par ces conteneurs
Je peux inspecter le trafic réseau entre ces conteneurs
Je ne peux pas inspecter ou interrompre le trafic réseau des autres conteneurs
(la config réseau est fournie par les managers; le spoofing d'ARP est impossible)
Je ne peux pas déduire la topologie du cluster et sa taille
Je peux uniquement collecter les adresses IP des managers.
Définir des niveaux de sécurité
Définir des zones de sécurité
Placer les managers dans la plus haute zone de sécurité
S'assurer que les applicatifs d'un certain niveau de sécurité ne tournent que sur une certaine zone
Forcer ce comportement peut se faire via un plugin d'autorisation
A l'installation, l'accès à l'API Docker est "tout ou rien"
Quand quelqu'un accès à l'API Docker, il peut faire n'importe quoi
Si vos développeurs utilisent l'API Docker pour déployer sur le cluster de dev...
... et que le cluster de dev est le même que le cluster de prod ...
... ça revient à donner aux devs l'accès aux données de production, mots de passe, etc.
C'est assez simple d'éviter ça.
Quelques solutions, par ordre croissant de flexibilité:
Installer plusieurs clusters avec différent périmètre de sécurité
(et différents identifiants d'accès pour chacun)
Quelques solutions, par ordre croissant de flexibilité:
Installer plusieurs clusters avec différent périmètre de sécurité
(et différents identifiants d'accès pour chacun)
Ajouter une couche supplémentaire d'abstraction (scripts sudo, hooks, ou un vrai PAAS)
Quelques solutions, par ordre croissant de flexibilité:
Installer plusieurs clusters avec différent périmètre de sécurité
(et différents identifiants d'accès pour chacun)
Ajouter une couche supplémentaire d'abstraction (scripts sudo, hooks, ou un vrai PAAS)
Activer les plugins d'autorisation
chaque requête vers l'API est filtrée par un ou plusieurs plugins(s)
par défaut, le champ subject name du certificat TLS client est utilisé comme identifiant
exemple: user and permission management dans UCP swarm/apiscope.md
Logs centralisés
(automatically generated title slide)
On veut pouvoir envoyer tous nos logs de conteneur à un service central
Si ce service pouvait offrir une jolie interface web, ce serait bien.
On veut pouvoir envoyer tous nos logs de conteneur à un service central
Si ce service pouvait offrir une jolie interface web, ce serait bien.
Nous allons déployer la suite ELK.
Elle acceptera les logs via une socket GELF.
Nous allons configurer nos services avec le pilote de log gelf
.
Installer ELK pour stocker les logs de conteneur
(automatically generated title slide)
Avant-propos important: ce n'est pas une installation "officielle" ou "recommandée"; juste un exemple. Nous avons choisi ELK pour cette démo par sa popularité et les demandes qu'il suscite; mais vous serez aussi gagnant avec Fluent ou d'autres solutions de journalisation!
Ce qu'on va faire:
Lancer une suite ELK via des services
Admirer le chic de l'interface Kibana
Envoyer quelques logs à la main avec des conteneurs temporaires
Configurer nos conteneurs pour envoyer leurs logs à Logstash
ELK, c'est trois composants:
ElasticSearch, pour stocker et indexer les messages de log;
Logstash, qui reçoit les messages de diverses sources, les traite, et les transmets à diverses destinations;
Kibana, pour afficher/chercher les messages dans une jolie interface.
Le seul composant que nous allons configurer est Logstash.
Nous accepterons des messages de log au format GELF.
Les messages seront stockés dans ElasticSearch,
et affichés dans la sortie standard de Logstash pour débogage.
On aura besoin de trois conteneurs: ElasticSearch, Logstash, Kibana
On les placera dans un réseau commun, logging
Déclarer le réseau:
docker network create --driver overlay logging
Déclarer le service ElasticSearch:
docker service create --network logging --name elasticsearch elasticsearch:2.4
Kibana expose une interface web
Son port par défaut (5601) doit être publié.
Il a besoin d'une touche de config: l'adresse du service ES
On ne voudrait pas des logs Kibana dans l'interface (cela ajouterait de la pollution)
on va donc dir à Logstash de les ignorer
docker service create --network logging --name kibana --publish 5601:5601 \ -e ELASTICSEARCH_URL=http://elasticsearch:9200 kibana:4.6
Logstash exige une config pour recevoir les messages GELF et les envoyer dans ES.
On pourrait générer notre propre image avec la bonne configuration.
On peut aussi passer la configuration en ligne de commande
docker service create --network logging --name logstash -p 12201:12201/udp \ logstash:2.4 -e "$(cat ~/container.training/elk/logstash.conf)"
Trouver la node qui exécute le conteneur Logstash:
docker service ps logstash
Se connecter à cette node
Afficher les logs du service Logstash:
docker service logs logstash --follow
Vous devriez voir le message indiquant le "pouls" du service:
{ "message" => "ok", "host" => "1a4cfb063d13", "@version" => "1", "@timestamp" => "2016-06-19T00:45:45.273Z"}
docker-compose -f elk.yml builddocker-compose -f elk.yml pushdocker stack deploy -c elk.yml elk
Note: les étapes de build et push ne sont pas strictement nécessaires, c'est juste une bonne habitude!
Jetons un oeil au fichier Compose.
Affichons les logs de Logstash
(Qui gardera les gardiens? version log)
docker service logs --follow --tail 1 elk_logstash
Vous devriez voir passer les messages de "pouls":
{ "message" => "ok", "host" => "1a4cfb063d13", "@version" => "1", "@timestamp" => "2016-06-19T00:45:45.273Z"}
Dans une nouvelle fenêtre, nous allons générer un message de log.
Nous utiliserons une conteneur éphémère, et le pilote de log GELF de Docker.
docker run --log-driver gelf --log-opt gelf-address=udp://127.0.0.1:12201 \ --rm alpine echo hello
Ce message de test devrait s'afficher dans les logs du conteneur Logstash.
Jusqu'ici, nos logs partaient d'un conteneur "classique"; allons faire la même chose au niveau d'un service.
C'est notre jour de chance: les options --log-driver
et --log-opt
sont exactement les mêmes!
Envoyer un message de test:
docker service create \ --log-driver gelf --log-opt gelf-address=udp://127.0.0.1:12201 \ alpine echo hello
Ce message de test devrait s'afficher pareil dans les logs du conteneur Logstash.
Jusqu'ici, nos logs partaient d'un conteneur "classique"; allons faire la même chose au niveau d'un service.
C'est notre jour de chance: les options --log-driver
et --log-opt
sont exactement les mêmes!
Envoyer un message de test:
docker service create \ --log-driver gelf --log-opt gelf-address=udp://127.0.0.1:12201 \ alpine echo hello
Ce message de test devrait s'afficher pareil dans les logs du conteneur Logstash.
En réalité, plusieurs messages vont remonter, et continuerons d'arriver de temps en temps
Par défaut, si un conteneur sort (ou est tué par docker kill
, ou s'il manque de mémoire...)
le Swarm va le redémarrer (potentiellement sur une autre machine)
Ce comportement peut être modifié en utilisant l'option de condition de redémarrage
docker service update xxx --restart-condition none
Les conditions de redémarrage sont none
, any
, and on-error
.
D'autres options existent comme --restart-delay
, --restart-max-attempts
, et --restart-window
.
Se connecter au port 5601 du cluster
si vous utilisez "Play-With-Docker", cliquez sur le badge (5601) au dessus du terminal
sinon, ouvrez dans le navigateur l'url : http://(adresse-IP-noeud):5601
Kibana devrait vous proposer de "Configure an index pattern":
dans la liste "Time-field name", choisir "@timestamp" et cliquez
le bouton "Create".
Puis:
Vous pouvez voir une série de barres vertes (avec une nouvelle barre toutes les minutes)
Nous allons dire à notre Swarm d'ajouter le log GELF à tous nos services
C'est réalisé avec la commande docker service update
Les options de log sont les mêmes qu'avant
rng
:docker service update dockercoins_rng \ --log-driver gelf --log-opt gelf-address=udp://127.0.0.1:12201
Après env. 15 secondes, vous devriez voir les messages de log dans Kibana.
Retourner à Kibana
Les logs de conteneur devrait s'afficher!
On peut personnaliser l'interface web pour la rendre plus claire.
Que se serait-il passé si nous avions modifié le service Redis?
Quand un service change, SwarmKit remplace un conteneur existant par un autre.
C'est très bien pour des services stateless.
Mais si vous changez un service à données persistentes (stateful), ses données vont être perdues dans l'opération.
Mais si on met à jour notre service Redis, tous nos DockerCoins vont être perdus.
Ce n'est pas une installation de niveau "production".
Il s'agit d'un exemple à but éducatif. Puisque nous avons un seul serveur, nous avons installé une seule instance ElasticSearch et une seule instance Logstash.
Dans une installation de "production", vous avez besoin d'un cluster ElasticSearch (pour la haute disponibilité et la capacité totale de stockage). Vous avez aussi besoin de plusieurs isntances de Logstash.
Et si vous voulez résister aux pics de logs, vous aurez besoin d'une sorte de file d'attente de messages: Redis si c'est léger, Kafka si vous voulez garantir aucune perte. Bonne chance.
<<<<<<< HEAD Pour en savoir plus sur le pilote GELF, jetez un oeil sur
If you want to learn more about the GELF driver, have a look at [this blog post](
master https://jpetazzo.github.io/2017/01/20/docker-logging-gelf/).
Collecter les métriques
(automatically generated title slide)
On veut rassembler des métriques dans sur un seul service
On veut collecter les métriques de noeuds et de conteneurs
On veut aussi une jolie interface pour les consulter (des graphes)
CPU, RAM, usage disque pour toute la node
Nombre total de processus en cours d'exécution, et leur état
activité I/O (entrées/sorties sur disque et réseau), par opération ou par volume
indicateurs physiques et matériels (si disponible): température, vitesse du ventilateur...
... et bien plus!
Similaires aux métriques de nodes, sans être identiques
Répartition de la RAM différente:
l'activité I/O est aussi plus difficile à suivre
Pour plus de détails sur les métriques de conteneurs, voir:
https://jpetazzo.github.io/2013/10/08/docker-containers-metrics/
Métriques arbitraires liées à notre applicatif et au métier
Performance système: latence des requêtes, taux d'erreur ...
Information de volume: nombres de lignes dans la base de données, taille de la file d'attente...
Données métier: inventaire, articles vendus, chiffre d'affaire ...
Nous allons monter deux collecteurs de métriques différents:
Le premier basé sur Intel Snap,
Le second sur Prometheus.
Nous allons utiliser trois projets open source en Go pour notre premier collecteur de métriques:
Intel Snap
Collecte, traite, et publie les métriques
InfluxDB
Stocke les métriques
Grafana
Présente les métriques visuellement
Peut collecter, traiter, et exposer les données de métriques
Ne stocke aucune métrique
Fonctionne en mode daemon, controllé par une ligne de commande (snapctl)
Délègue la collecte, le traitement et la publication à des plugins
Ne peut rien faire à l'installation; obligation de configurer!
Documentation: https://github.com/intelsdi-x/snap/blob/master/docs/
Snap ne stocke aucune donnée de métrique
InfluxDB est spécifiquement conçu pour les données basées sur le temps
CRud vs CRUD (on modifie rarement ou jamais ces données)
motifs de lecture/écriture orthogonaux
la clé est dans l'optimisation du format de stockage (pour l'usage et la performance du disque)
Snap dispose d'un plugin permettant la publication vers InfluxDB
Snap ne peut pas afficher de graphes
InfluxDB ne peut pas non plus
Grafana va s'en occuper
Grafana peut lire ses données depuis InfluxDB et l'afficher dans des graphes
Nous installerons Snap directement sur chaque noeud
Les versions publiées sous tarballs sont disponibles depuis Github
Nous l'utiliserons comme service global
(disponible sur chaque noeud, y compris les futurs arrivants)
Ce service va télécharger et décompresser Snap dans /opt et /usr/local
/opt et /usr/local sont des points de montage depuis l'hôte
Ce service va concrètement installer Snap sur tous les hôtes
docker service create --restart-condition=none --mode global \ --mount type=bind,source=/usr/local/bin,target=/usr/local/bin \ --mount type=bind,source=/opt,target=/opt centos sh -c 'SNAPVER=v0.16.1-betaRELEASEURL=https://github.com/intelsdi-x/snap/releases/download/$SNAPVERcurl -sSL $RELEASEURL/snap-$SNAPVER-linux-amd64.tar.gz | tar -C /opt -zxf-curl -sSL $RELEASEURL/snap-plugins-$SNAPVER-linux-amd64.tar.gz | tar -C /opt -zxf-ln -s snap-$SNAPVER /opt/snapfor BIN in snapd snapctl; do ln -s /opt/snap/bin/$BIN /usr/local/bin/$BIN; done' # Si vous copier-coller ce block, n'oubliez pas l'apostrophe finale ☺
snapd
Le coeur de Snap est snapd
, le daemon Snap
L'application est composée d'une API REST, un module de contrôle et un module d'ordonnancement
snapd
sans vérification de plugin et en mode debug:snapd -t 0 -l 1
Pour aller plus loin:
https://github.com/intelsdi-x/snap/blob/master/docs/SNAPD.md https://github.com/intelsdi-x/snap/blob/master/docs/SNAPD_CONFIGURATION.md
snapctl
to interact with snapd
snapctl
pour intéragir avec snapd
Ouvrir un nouveau terminal
Charger le plugin de collection psutil:
snapctl plugin load /opt/snap/plugin/snap-plugin-collector-psutil
Charger le plugin de publication de fichier:
snapctl plugin load /opt/snap/plugin/snap-plugin-publisher-mock-file
ls
, celle de Snap préfère list
Voir vos plugins chargés:
snapctl plugin list
Voir les métriques qu'on peut collecter:
snapctl metric list
Pour démarrer les phases de collecte/traitement/publication des données de métriques, on doit déclarer une nouvelle task
Une tâche indique:
Les tâches peuvent être définies via des manifestes écrits en JSON ou YAML
Quelques plugins, tels que le collecteur Docker, autorisent les jokers (*) dans les "chemins" de métriques
(voir snap/docker-influxdb.json)
Plus de ressources: https://github.com/intelsdi-x/snap/blob/master/docs/TASKS.md
version: 1 schedule: type: "simple" # collect on a set interval interval: "1s" # of every 1s max-failures: 10 workflow: collect: # first collect metrics: # metrics to collect /intel/psutil/load/load1: {} config: # there is no configuration publish: # after collecting, publish - plugin_name: "file" # use the file publisher config: file: "/tmp/snap-psutil-file.log" # write to this file
snap/psutil-file.yml
.Déclarer une nouvelle tâche basée sur le manifeste:
cd ~/container.training/snapsnapctl task create -t psutil-file.yml
L'affichage devrait ressembler à:
Using task manifest to create task Task created ID: 240435e8-a250-4782-80d0-6fff541facba Name: Task-240435e8-a250-4782-80d0-6fff541facba State: Running
Cela va confirmer que notre tâche tourne correctement, et nous rappeler son ID de tâche.
snapctl task list
L'affichage devrait ressembler à ce qui suit:
ID NAME STATE HIT MISS FAIL CREATED 24043...acba Task-24043...acba Running 4 0 0 2:34PM 8-13-2016
La tâche utilise un éditeur très simple, mock-file
Cet éditeur ne fait qu'écrire des lignes dans un fichier (une ligne par point de donnée)
tail -f /tmp/snap-psutil-file.log
Pour sortir, taper ^C
Quand une tâche n'écrit pas directement dans un fichier local, passez par snapctl task watch
snapctl task watch
va faire défiler les métriques collectées vers STDOUT
snapctl task watch <ID>
Pour sortir, taper ^C
Notre déploiement Snap garde quelques défauts:
snapd a été démarré à la main
il est lancé sur une seule node
la configuration est purement locale
Notre déploiement Snap garde quelques défauts:
snapd a été démarré à la main
il est lancé sur une seule node
la configuration est purement locale
Notre déploiement Snap garde quelques défauts:
snapd a été démarré à la main
il est lancé sur une seule node
la configuration est purement locale
Mais d'abord, retournons au terminal où tourne snapd
, et tapons ^C
Toutes les tâches seront stoppées; tous les plugins déchargés; Snap va sortir
Tribe (tribu en français), est le mécanisme de cluster chez Snap
Quand le mode tribu est activé, les noeuds peuvent rejoindre des agreements
Quand un noeud au sein d'un agreement fait quelque chose (par ex. charger un plugin ou lancer une tâche), les autres noeuds dans le même agreement font de même.
Nous allons l'exploiter pour charger le collecteur Docker et l'éditeur InfluxDB sur toutes les nodes, puis lancer une tâche pour les activer.
Sans le mode Tribe, nous aurions du charger les plugins et lancer les tâches à la main sur chaque noeud.
Pour en savoir plus: https://github.com/intelsdi-x/snap/blob/master/docs/TRIBE.md
&
ou le démarrer dans un tmuxsnapd -t 0 -l 1 --tribe --tribe-seed node1:6000
Si vous n'utilisez pas Play-With-Docker, il y a une autre manière de lancer Snap!
Grosse bidouille en vue!
Nous allons créer un service global
Ce service global va installer un client SSH
Avec ce client SSH, le service va se connecter sur sa node locale
(i.e "s'échapper" du conteneur, grâce à la clé SSH fournie)
Une fois connecté à la node, le service démarre snapd avec le mode Tribe
docker service create --name snapd --mode global \ --mount type=bind,source=$HOME/.ssh/id_rsa,target=/sshkey \ alpine sh -c " apk add --no-cache openssh-client && ssh -o StrictHostKeyChecking=no -i /sshkey root@172.17.0.1 \ /usr/local/bin/snapd -t 0 -l 1 --tribe --tribe-seed node1:6000 " # Si vous copier-coller ce bloc, n'oubliez pas l'apostrophe finale :-)
Rappel : ceci ne fonctionne pas si vous êtes sur Play-With-Docker (à cause de SSH).
snapctl member list
Vous devriez voir les 5 noeuds et leurs noms d'hôtes.
Un agreement est un pacte entre membres d'un cluster Snap qui garantit le même comportement.
Nous pouvons désormais déclarer un agreement pour nos plugins et tâches.
snapctl agreement create docker-influxdb
La sortie d'écran devrait ressembler à ceci:
Name Number of Members plugins tasks docker-influxdb 0 0 0
Pas besoin d'un autre service global superflu!
On peut ajouter des noeuds depuis n'importe quel noeud du cluster
snapctl member list | tail -n +2 | xargs -n1 snapctl agreement join docker-influxdb
Le dernier bout d'affichage devrait ressembler à ceci:
Name Number of Members plugins tasks docker-influxdb 5 0 0
Le plugin Docker exige au moins un conteneur pour être démarré
Normalement, à ce niveau de la procédure, vous devriez disposer d'au moins un conteneur sur chaque node
Mais, juste au cas où quelque chose aurait divergé, déclarons un service global de démo.
docker service create --name ping --mode global alpine ping 8.8.8.8
Nous allons créer un service pour InfluxDB
Nous utiliserons pour cela l'image officielle
InfluxDB expose plusieurs ports:
8086 (HTTP API; nous en avons besoin)
8083 (l'interface admin; il nous la faut)
8088 (communication de cluster; superflu ici)
d'autres ports pour d'autres protocoles (graphite, collectd, etc.)
On se suffira des deux premiers ports pour la suite.
docker service create --name influxdb \ --publish 8083:8083 \ --publish 8086:8086 \ influxdb:0.13
Note: Cela va autoriser n'importe quel noeud à publier des métriques sur localhost:80806
,
et par la même, ouvrir l'interface admin depuis n'importe quel noeud sur le port 8083.
Assurez-vous bien d'utiliser la version 0.13 d'InfluxDB; quelques petits trucs ont changé en version 1.0 (comme le nom de la politique de rétention par défaut, qui est maintenant "autogen"), ce qui casserait notre démo.
Ouvrir le port 8083 sur navigateur
Entrer la requête suivante dans le champ de saisie:
CREATE DATABASE "snap"
En haut à droite, sélectionner "Database: snap"
Note: le langage de requête InfluxDB ressemble à SQL, mais il n'en est rien.
En passant à la version 1.0, InfluxDB a changé le nom de la politique par défaut.
A l'origine baptisée "default", elle s'appelle désormais "autogen"
Au grand dam de Snap qui ne connait que "default", nous occasionnant des erreurs potentielles.
CREATE RETENTION POLICY "default" ON "snap" DURATION 1w REPLICATION 1
Nous allons charger les plugins depuis la node locale
Puisque notre node locale est un membre d'agreement, toutes les autres nodes de ce même agreement vont agir en miroir.
Charger le collecteur Docker:
snapctl plugin load /opt/snap/plugin/snap-plugin-collector-docker
Charger l'éditeur InfluxDB:
snapctl plugin load /opt/snap/plugin/snap-plugin-publisher-influxdb
Comme tout à l'heure, nous allons déclarer une nouvelle tâche sur la node locale
Ladite tâche va être répliquée sur les nodes membres du même agreement
cd ~/container.training/snapsnapctl task create -t docker-influxdb.json
Note: la description de tâche envoie les métriques au point d'entrée de l'API InfluxDB, écoutant sur 127.0.0.1:8086. Puisque le conteneur InfluxDB est publié sur le port 8086, 127.0.0.1:8086 va toujours router le trafic vers le conteneur InfluxDB.
Note: si une tâche tombe en panne (par ex. en essayant de publier des données vers une base de métrique inaccessible), la tâche va se mettre à l'arrêt.
Vous devrez la redémarrer à la main en lançant:
snapctl task enable <ID>snapctl task start <ID>
C'est une procédure à lancer sur chaque noeud. L'alternative serait de supprimer+re-déclarer la tâche (commandes à l'effet global sur tout le cluster)
Lister les "measurements":
SHOW MEASUREMENTS
(Vous devriez voir deux entrées génériques correspondant aux deux métriques collectées.)
Afficher les données séries-temps pour une des métriques:
SELECT * FROM "intel/docker/stats/cgroups/cpu_stats/cpu_usage/total_usage"
(Vous devriez voir une liste de points de données avec time, docker_id, source, et value.)
Vous pouvez utiliser une image quasi-officielle, grafana/grafana
Vous pouvez rendre publique l'interface web de Grafana sur son port par défaut (3000)
docker service create --name grafana --publish 3000:3000 grafana/grafana:3.1.1
Ouvrir le port 3000 avec le navigateur
Se connecter en "admin" en identifiant/mot de passe
Cliquer sur le logo Grafana (la spirale orange dans le coin en haut à gauche)
Cliquer sur les "Data sources"
Cliquer sur "Add data source" (le bouton vert à droite)
Remplir le formulaire exactmeent comme suit:
Dans les paramètres HTTP, renseigner comme suit:
Dans les détails pour InfluxDB, écrire comme suit:
Pour finir, cliquer sur "add", vous devriez voir un message vert affirmant "Success - Data source is working". Si vous voyez un encart orange (parfois sans message), cela veut dire que quelque chose s'est mal passé. Vérifier bien à nouveau.
Cliquer sur le logo Grafana encore (la spirale orange dans le coin en haut à gauche)
Passer sur "Dashboards"
Cliquer sur "+ New"
Cliquer sur le petit rectangle vert qui apparait en haut à gauche
Passer sur "Add panel"
Cliquer sur "Graph"
A ce moment précis, vous devriez voir un graphe d'exemple s'afficher.
Félicitations, vous avez sous les yeux l'usage CPU d'un seul conteneur!
Laissez cet onglet ouvert!
Nous allons installer un autre système de métrique
... Puis comparer les 2 graphes côte-à-côte
Prometheus est un autre système de collecte de métriques
Snap pousse les métriques, là où Prometheus les aspire
Le serveur Prometheus aspire, stocke et affiche les métriques
Sa configuration définit une liste de points exportateurs
(cette liste peut être dynamique, via par ex. Consul, DNS, etcd ...)
Les exportateurs exposent des métriques via HTTP dans un simple format ligne à ligne
(Un format optimisé usant de protobuf existe aussi)
/metrics
Voici à quoi ressemble un exportateur de noeud:
Prometheus lui-même expose aussi ses propres métriques internes:
Un serveur Prometheus va aspirer les URLs telles que celles-ci
(On passera plutôt par protobuf pour éviter le supplément de traitement des formats ligne-à-ligne!)
Nous allons lancer deux services globaux (i.e. planifiés sur toutes les nodes):
Un exportateur de noeud Prometheus pour lire les métriques de node
Le cAdvisor de Google pour lire les métriques de conteneurs.
C'est un serveur Prometheus qui va interroger ces exportateurs.
Ce serveur Prometheus sera configuré pour la découverte de services par DNS
Nous utiliserons tasks.<nom_du_service>
pour cette découverte de services.
Tous ces services seront placés dans un réseau privé interne.
docker network create --driver overlay prom
docker service create --name node --mode global --network prom \ --mount type=bind,source=/proc,target=/host/proc \ --mount type=bind,source=/sys,target=/host/sys \ --mount type=bind,source=/,target=/rootfs \ prom/node-exporter \ --path.procfs /host/proc \ --path.sysfs /host/proc \ --collector.filesystem.ignored-mount-points "^/(sys|proc|dev|host|etc)($|/)"
Dans la même veine, cAdvisor devrait tourner directement sur nos hôtes.
Mais on peut le lancer dans des conteneurs configurés correctement.
docker service create --name cadvisor --network prom --mode global \ --mount type=bind,source=/,target=/rootfs \ --mount type=bind,source=/var/run,target=/var/run \ --mount type=bind,source=/sys,target=/sys \ --mount type=bind,source=/var/lib/docker,target=/var/lib/docker \ google/cadvisor:latest
Voici notre fichier de configuration pour Prometheus:
global: scrape_interval: 10sscrape_configs: - job_name: 'prometheus' static_configs: - targets: ['localhost:9090'] - job_name: 'node' dns_sd_configs: - names: ['tasks.node'] type: 'A' port: 9100 - job_name: 'cadvisor' dns_sd_configs: - names: ['tasks.cadvisor'] type: 'A' port: 8080
Le plus simple serait de générer une image spécifique, incluant cette config.
On va utiliser un Dockerfile très simple:
FROM prom/prometheus:v1.4.1COPY prometheus.yml /etc/prometheus/prometheus.yml
(Le fichier de configuraiton et le Dockerfile sont tous deux dans le dossier prom
)
On va lancer un build, puis pousser cette image dans notre Registry locale
On terminera en créant un service invoquant cette image
Note: il est aussi possible d'utiliser un objet config
pour injecter ce fichier de configuration
sans avoir à créer une image spéciale.
Générer l'image grâce au Dockerfile fourni:
docker build -t 127.0.0.1:5000/prometheus ~/container.training/prom
Pousser l'image sur notre registre local:
docker push 127.0.0.1:5000/prometheus
C'est le seul service qu'on devra rendre public
(Si on veut pouvoir accéder à Prometheus de l'extérieur!)
docker service create --network prom --name prom \ --publish 9090:9090 127.0.0.1:5000/prometheus
S'assurer que nous sommes dans le dossier stacks
:
cd ~/container.training/stacks
Générer, envoyer et lancer la stack Prometheus:
docker-compose -f prometheus.yml builddocker-compose -f prometheus.yml pushdocker stack deploy -c prometheus.yml prometheus
Ouvrir le port 9090 avec le navigateur
Cliquer sur "status", puis "targets"
Vous devriez voir sept points d'entrées (3 cadvisor, 3 node, 1 prometheus)
Leur statut devrait être "UP".
(Nouveau dans Docker Engine 17.06)
Nous générons une image sur-mesure juste pour injecter un fichier de configuration
Au lieu de cela, nous pourrions rester sur l'image Prometheus officielle + une config
Une config
est un blob (habituellement, un fichier de conf) qui:
est créé et géré à travers l'API Docker (dont la ligne de commande)
est stocké dans le log Raft (synonyme de sécurité)
peut être associé à un service
(cette opération consistant à injecter le blob sous forme de fichier classique dans les conteneurs du service)
configs
et secrets
Les deux se ressemblent vraiment, à ceci près que:
configs
:
peut être injecté à n'importe quel endroit du système de fichiers
peut être affiché et extrait à l'aide de l'API Docker ou la CLI
secrets
peut uniquement être injecté dans /run/secrets
n'est jamais stocké en clair sur le disque
ne pourra jamais être affiché ou extrait avec l'API Docker ou la CLI
config
Le fichier Compose qui suit (prometheus+config.yml
) accomplit
la même tâche, mais en utilisant un config
au lieu de cuisiner
une nouvelle image "farcie" de configuration.
version: "3.3"services:prometheus: image: prom/prometheus:v1.4.1 ports: - "9090:9090" configs: - source: prometheus target: /etc/prometheus/prometheus.yml...configs: prometheus: file: ../prom/prometheus.yml
config
dans un fichier ComposeDans chaque service, une section configs
optionnelle peut lister autant de configuration que nécessaire.
Chaque config peut préciser:
un champ target
optionnel (chemin où injecter la config; par défaut: à la racine du conteneur)
les permissions et/ou propriété (par défaut, le fichier appartient à l'UID 0, i.e. root
)
Ces configs pointent vers la section principale de configs
Cette section principale peut déclarer une ou plusieurs configs telles que:
external, à savoir qu'elle est supposée pré-exister avant de déployer la stack
le référencement d'un fichier, dont le contenu est utilisé pour initialiser la config
prometheus+config.yml
Re-déployer la stack prometheus
:
docker stack deploy -c prometheus+config.yml prometheus
Vérifier que Prometheus fonctionne encore comme attendu:
(En se connectant à n'importe quel noeud du cluster, sur le port 9090)
Lister les objets de config existant:
docker config ls
Afficher les détails sur notre objet de config:
docker config inspect prometheus_prometheus
Note: le contenu du blob de configuration est affiché en encodate BASE64
(En effet, cela peut ne pas être du texte; par exemple une image ou n'importe quel binaire!)
Extraire le contenu en BASE64 avec jq
:
docker config inspect prometheus_prometheus | jq -r .[0].Spec.Data
Le décoder avec base64 -d
:
docker config inspect prometheus_prometheus | jq -r .[0].Spec.Data | base64 -d
Cliquer sur "Graph", et dans "expression", coller ce qui suit:
sum by (container_label_com_docker_swarm_node_id) ( irate( container_cpu_usage_seconds_total{ container_label_com_docker_swarm_service_name="dockercoins_worker" }[1m] ))
Cliquer sur le bouton bleu "Execute" et sur l'onglet "Graph" juste en dessous.
Nous allons monter la même requête de zéro
Le but n'est pas de remplacer un vrai cours détaillé sur PromQL
C'est juste suffisant pour que vous (et moi) faisions semblant de comprendre la requête précédente et pour impressioner vos collègues au bureau (ou pas)
(ou, pour construire d'autres requêtes si nécessaire, ou les adapter si cAdvisor, Prometheus, ou n'importe quoi demande des changements, et exige de changer la requête!)
Cliquer sur l'onglet "Graph" au dessus
On arrive dans un tableau de bord vierge
Cliquer sur la liste "Insert metric at cursor", et choisir container_cpu_usage_seconds_total
Ça va placer le nom de la métrique dans le champ de requête
Cliquer sur "Execute"
La table des mesures du dessous va se remplir
Cliquer sur "Graph" (à côté de "Console")
La table des mesures est remplacée par une série de graphes (après quelques secondes)
Passer sur les lignes du graphe
(Essayer de repérer ceux qui ont des labels comme container_label_com_docker_...
)
Changer la requête, en ajoutant une condition entre accolades:
container_cpu_usage_seconds_total{container_label_com_docker_swarm_service_name="dockercoins_worker"}
Cliquer sur "Execute"
On devrait voir maintenant une ligne par CPU par conteneur
Si vous voulez limiter à un conteneur précis, ajouter une expression régulière: id=~"/docker/c4bf.*"
Vous pouvez aussi cumuler les conditions, en les séparant par virgule.
Ce qu'on voit, c'est le montant total de CPU utilisé (en secondes)
On voudrait afficher un taux (temps de CPU utilisé / temps réel)
Pour avoir une moyenne mobile sur 1 minute, encapsulez l'expression en cours dans:
rate ( ... { ... } [1m] )
Cela devrait convertir notre compteur CPU qui grimpe en courbe gracieuse
Pour afficher plutôt un taux instantané, choisir irate
au lieu de rate
(La fenêtre de temps sert ensuite à filtrer la quantité de données dans le passé à récupérer, dans le cas où des points sont manquants à cause de collecte défaillante; voir ici pour plus de détails!)
On devrait voir des pics, qui étaient restés cachés, à cause du lissage sur le temps
On a une courbe par CPU par conteneur; on voudrait les cumuler
Encapsulez toute l'expression dans:
sum ( ... )
On peut voir maintenant une seule courbe
Avec plusieurs conteneurs, on peut juste éclater la dimension "CPU":
sum without (cpu) ( ... )
On affichera la même courbe, en préservant les autres labels
Fécilitations, vous venez d'écrire votre première expression PromQL de zéro!
(Merci à Johannes Ziemke et Julius Volz pour leur aide avec Prometheus!)
Si vous n'avez pas monté Snap, InfluxDB et Grafana, sautez cette section
Si vous avez fermé l'onglet Grafana, il faudra peut-être ré-installer un nouveau tableau de bord
(sauf si vous l'avez enregistré avant de quitter)
Pour tout récupérer, il suffit de suivre les instructions du chapitre précédent
Dans un nouvel onglet, ouvrir Grafana (port 3000)
Cliquer sur le logo Grafana (la spirale Orange dans le coin en haut à gauche)
Cliquer sur "Data sources"
Cliquer sur le bouton vert "Add data source"
On voit le même formulaire qu'on a rempli la dernière fois pour InfluxDB.
Entrer "prom" dans le champ "name"
Choisir "Prometheus" comme le type de source
Entrer http://(addresse.IP.de.votre.node):9090 dans le champ Url
Choisir "direct" dans la méthode d'accès
Cliquer sur "Save and Test"
Encore une fois, on devrait voir une boîte verte disant "Data source is working".
Autrement, réviser chaque étape de la procédure!
Retourner à l'onglet de notre premier tableau de bord Grafana
Cliquer sur le bouton bleu "Add row" dans le coin en bas à droite
Cliquer sur l'onglet vert à gauche; choisir "Add panel" et "Graph"
On atterrit alors sur l'éditeur de graphe vu précédemment.
L'éditeur est un peu moins sympa que celui pour InfluxDB.
Choisir "prom" comme source de données du panneau
Coller la requête dans le champ "requête":
sum without (cpu, id) ( irate ( container_cpu_usage_seconds_total{ container_label_com_docker_swarm_service_name="influxdb"}[1m] ) )
Cliquer hors du champ de requête pour confirmer
Fermer l'éditeur de ligne en cliquant "X" dans le coin en haut à droite.
Les deux courbes devraient se ressembler
Astuce de pro: alignez les légendes de temps!
Cliquer sur l'horloge dans le coin haut-droit
Choisir "last 30 minutes"
Cliquer sur "Zoom out"
Maintenant taper sur la touche "flèche droite" (rester appuyé pour faire monter le CPU!)
Ajuster les unités est un exercice laissé au lecteur.
Prometheus, a Whirlwind Tour, an original overview of Prometheus
Docker Swarm & Container Overview, a custom dashboard for Grafana
Gathering Container Metrics, a blog post about cgroups
The Prometheus Time Series Database, a talk explaining why custom data storage is necessary for metrics
DC17US: Monitoring, the Prometheus Way (video)
DC17EU: Prometheus 2.0 Storage Engine (video)
Liens et ressources
(automatically generated title slide)
Ces diapos (et les futures mises à jour) sont sur → https://container.training/
Bonjour, je suis:
Cet atelier se déroulera de 9h à 17h.
La pause déjeuner se fera entre 12h et 13h30.
(avec 2 pauses café à 10h30 et 15h!)
N'hésitez pas à m'interrompre pour vos questions, à n'importe quel moment.
Surtout quand vous verrez des photos de conteneurs en plein écran!
Keyboard shortcuts
↑, ←, Pg Up, k | Go to previous slide |
↓, →, Pg Dn, Space, j | Go to next slide |
Home | Go to first slide |
End | Go to last slide |
Number + Return | Go to specific slide |
b / m / f | Toggle blackout / mirrored / fullscreen mode |
c | Clone slideshow |
p | Toggle presenter mode |
t | Restart the presentation timer |
?, h | Toggle this help |
Esc | Back to slideshow |