Dans cette seconde partie, on va commencer à aller légèrement plus loin. Comme lancer des conteneurs à la main c’est fatigant, on va s’intéresser à docker-compose qui permet d’orchestrer le déploiement de plusieurs conteneurs. Et comme la pratique c’est plus fun, on fera une petite étude de cas à la fin :smile:
Préambule
Docker compose, ça permet de déployer une application de plusieurs services sans lancer tous les conteneurs manuellement, et en faisant tous les exports/publish/liens à la main. C’est du YAML très simple, et ça permet aussi de gérer les dépendances des services.
Avant de lancer web, il faut lancer maria:
version: '2'
services:
web:
build: .
ports:
- "5000:5000"
depends_on:
- maria
links:
- maria
maria:
image: mariadb
expose:
- "3306"
environment:
- MYSQL_ROOT_PASSWORD=secret
Bon, c’était l’exemple bateau, la syntaxe de base est très simple et on peut voir facilement ce que ça fait: Lance maria avec l’image mariadb, puis lance web qui contient un lien vers maria.
La règle “ports” est l’équivalent de notre “-p” d’avant. La règle expose est équivalente à avant. Sauf qu’ici, elle ne sert à rien puisqu’il y a déjà un EXPOSE dans l’image mariadb. Mais c’est pour l’exemple.
On lance les conteneurs avec docker-compose up -d
et on a accès à notre belle page web sur le port 5000.
Création de réseau
Bon, avec tout ça, qu’est ce que notre compose fait exactement ?
Il créé un nouveau réseau et fait tourner les conteneurs dedans. Ici, il a créé le réseau “experiments_default” parce que je l’ai lancé depuis mon dossier “experiments” et que le réseau créé par défaut s’appelle… default.
Là où c’est encore un peu tordu (de mon point de vue) c’est que la règle “links” ne sert pas à grand chose. Il faut fouiller un peu la documentation pour voir qu’en fait, tous les conteneurs du réseau compose par défaut peuvent se parler, après tout c’est un réseau défini par l’utilisateur.
Donc, les liens permettent surtout d’aliaser des services puisque lesdits services peuvent déjà se parler par défaut.
Un petit docker network inspect experiments_default
nous permet de vérifier ce que l’on savait déjà, et on est parés pour la suite.
=> Magie, un nouveau réseau bridge avec deux conteneurs dedans.
Un peu plus loin
Le réseau par défaut c’est bien, mais tout le monde connait tout le monde, et les réseaux flat c’est pas super joli. Heureusement, on peut établir des réseaux depuis compose. Pour cela, on va modifier un peu les services actuels: On garde maria, et on supprime web pour le séparer en webapp et api. Ce à quoi on rajoute le service job qui va taper sur maria (en toute amitié évidemment).
---------- ------- ---------
| webapp | ----- | api | -----| maria |
---------- ------- ---------
|
---------
| job |
---------
Donc, notre application ressemble à peu près à ça. Et comme aujourd’hui j’ai beaucoup d’imagination, ‘webapp’ va être sur le réseau ‘front’ et les trois autres sur le réseau ‘back’.
version: '2'
services:
webapp:
build: webapp/
ports:
- "5000:5000"
depends_on:
- api
links:
- api
networks:
- front
api:
build: api/
expose:
- "80"
depends_on:
- maria
networks:
- back
maria:
image: mariadb
environment:
- MYSQL_ROOT_PASSWORD=secret
networks:
- back
job:
build: job/
depends_on:
- maria
networks:
- back
networks:
front:
driver: bridge
back:
driver: bridge
Et ça ne marche PAS. Parce que assez logiquement, deux réseaux ne peuvent pas se parler entre eux.
En inspectant les réseaux ‘experiments_front’ et ‘experiments_back’, on se rend compte que webapp est tout seul, et que les trois autres s’amusent sans lui. Mais du coup, impossible de parler entre webapp et api.
Pour remédier à ça, il suffit de dire qu’api fait partie de ‘front’ ET de ‘back’ (ou que webapp fait partie des deux, selon ce que vous trouvez le plus logique). Je ne remettrai pas le docker-compose.yml pour une seule ligne modifiée, mais du coup ça marche. On a au final 4 conteneurs, répartis entre ‘experiments_front’ et ‘experiments_back’.
En inspectant les réseaux, on retrouve ce à quoi on s’attendait:
docker network inspect experiments_front:
{ blabla,
"Containers": {
"1e476411a9002c4fd4edf63febcf7d10c723b92236c2a5715ead8550f615e83d": {
"Name": "experiments_webapp_1",
"EndpointID": "4999027bef1543ecd79bb06e1a8570917e6d35f72275c0e0052142b0294e5644",
"MacAddress": "02:42:ac:13:00:02",
"IPv4Address": "172.19.0.2/16",
"IPv6Address": ""
},
"43f82406fd9f723e10e1b0f4c1c82c2a5dc1d0d7b45e1e250965493cd40b4122": {
"Name": "experiments_api_1",
"EndpointID": "218425c5daf10ef985eeec71f2b89e7f1dcbde9d097be760173a4d7818d230f9",
"MacAddress": "02:42:ac:13:00:03",
"IPv4Address": "172.19.0.3/16",
"IPv6Address": ""
}
}
}
}
docker network inspect experiments_back:
{ blabla,
"Containers": {
"02185afa1ed85e065d8d64b7b69d08417bee4fda35bd4a5f445bf5537889fd58": {
"Name": "experiments_maria_1",
"EndpointID": "15e49e652bf28969137e5a7f98ea2a68651984550e8eb586c988cdf0bf0810ee",
"MacAddress": "02:42:ac:14:00:02",
"IPv4Address": "172.20.0.2/16",
"IPv6Address": ""
},
"0ffc4e4d90af59277962422be2bc09091e65980fa8bfe04e137753a9da15d944": {
"Name": "experiments_job_1",
"EndpointID": "a01c0d713db3f0dc7dc2035cc1313ebc518e3143af0de03d343fe039f3ea7c1e",
"MacAddress": "02:42:ac:14:00:04",
"IPv4Address": "172.20.0.4/16",
"IPv6Address": ""
},
"43f82406fd9f723e10e1b0f4c1c82c2a5dc1d0d7b45e1e250965493cd40b4122": {
"Name": "experiments_api_1",
"EndpointID": "af3323d97fe09dbf74a9fd278ddb4a3981f6e0c158428cb6aeb6fe3519512083",
"MacAddress": "02:42:ac:14:00:03",
"IPv4Address": "172.20.0.3/16",
"IPv6Address": ""
}
}
}
}
Et voilà, on peut facilement créer des réseaux avec docker-compose, pour créer des infrastructures proches de la réalité. La syntaxe est simple, et c’est assez facile de comprendre ce qu’il se passe.
Etude de cas
Et maintenant, allons un peu avant dans les entrailles de la baleine.
Scénario: Un modèle master-slaves distribué -> chaque noeud doit pouvoir parler à chaque autre. Le master est composé de 3 services, un slave est composé de 5 services.
Problématique: Comment est-ce que je peux configurer mon docker-compose pour poper 1 master et n slaves, n étant raisonnable (2-50, puisque c’est une infra de test en local). Et comme on aime jouer, chacun des services nécessite un fichier de configuration différent, notamment pour lui indiquer ou trouver les autres services (parce que les URIs hardcodées, hmmm hmm….)
Vous avez 2h.
Bon, l’application est quand même bien faite, il se trouve que le master possède un service d’entrée/sortie et deux services internes. Un slave possède qwant à lui un service d’entrée, un service de sortie et trois services internes.
Maintenant, il faut configurer tout ça pour que les services se lancent et arrivent à se contacter sans problème.
Première solution
IaaS (Intern as a Service):
Concrètement, il s’agit de contacter un stagiaire pour qu’il écrive un gros docker-compose.yml, la conf, et lancer le résultat. Pas super.
Deuxième solution
Flat network
Même idée que précédemment, un gros docker-compose avec tous les conteneurs sur le même réseau. En étant un petit peu malin, on peut automatiser via un script bash qui va générer un set de fichiers corrects et en utilisant des volumes pour la conf. Ça marche, mais ça reste TRÈS moche.
Troisième solution
Chacun chez soi
Avec l’exemple webapp/api/maria/job, on a vu que l’on pouvait faire communiquer des conteneurs tout en séparant les points d’entrée/sortie des services internes.
Du coup, il est possible de faire deux fichiers compose de référence: un pour slave et un pour master. Chaque noeud a son réseau interne, sauf les points d’entrée/sortie qui sont sur le réseau interne au conteneur ET sur un réseau prédéfini où ils peuvent communiquer allègrement.
Ça mérite un petit exemple, donc c’est parti. Les exemples sont évidemment allégés pour ne montrer que la partie réseau, les links et dépendences dépendent de l’organisation de l’application et n’ont pas grand intérêt, surtout pour une application inexistante.
cat docker-compose-master.yml
version: '2'
services:
service_entry:
image: image_1
expose:
- "443"
networks:
- internal
- inout
service_2:
image: image_2
networks:
- internal
service_3:
image: image_2
networks:
- internal
networks:
internal:
driver: bridge
inout:
external:
name: inout
Le réseau ‘internal’ sera donc le réseau interne à l’appli. Comme docker-compse préfixe le nom d’un réseau par le nom d’un projet, le réseau ‘inout’ est déclaré comme externe, avec un nom fixe. C’est à dire que docker-compose n’essaiera pas de le créer au lancement et le cherchera parmi les réseau déjà existants. Seul pré-requis: il faut le créer à la main avant de lancer les services.
Même chose pour le slave, rien de transcendant:
cat docker-compose-slave.yml
version: '2'
services:
service_entry:
image: service_1
expose:
- "443"
networks:
- internal
- inout
service_out:
image: service_2
networks:
- internal
- inout
service_3:
image: service_3
networks:
- internal
service_4:
image: service_4
networks:
- internal
service_5:
image: service_5
networks:
- internal
networks:
internal:
driver: bridge
inout:
external:
name: inout
Et un petit script bash pour orchestrer tout ça:
cat run.sh
#!/bin/bash
docker-compose -p master -f docker-compose-master.yml up -d
for i in `seq 1 $1`; do
docker-compose -p "slave$i" -f docker-compose-slave.yml up -d
done
Le flag -p
spécifie le nom du projet, et -f
le fichier à prendre. Le script prend en argument simplement le nombre de slaves à lancer, et créé tout seul les réseau master_internal
, slave1_internal
, slave2_internal
…
Vous pouvez essayer de faire un ./run.sh 10
avec des fakes conteneurs et vous verrez 11 réseaux poper, pour un total de 53 conteneurs. C’est le moment d’utiliser la fameuse commande docker rm -f $(docker ps -a -q)
dont j’avais parlé en partie 1.
Tip: Si vous avez déjà des conteneurs qui tournent que vous ne voulez pas arrêter, vous pouvez supprimer tous les conteneurs d’un réseau via:
docker rm -f `docker network inspect <network> | grep -o "[a-zA-Z0-9_]\+_[a-zA-Z0-9_]\+_[0-9]\+"`
Sinon, en utilisant les fichiers compose:
docker-compose -p <projet> -f docker-compose-slave.yml stop
Malheureusement, si on se contente du setup précédent, on va avoir quelques surprises pour communiquer à la bonne personne.
Après avoir lancé un master et deux slaves, voici l’état de mes réseaux:
inout: 172.24.0.0/16
master_internal: 172.21.0.0/16
slave1_internal: 172.22.0.0/16
slave2_internal: 172.23.0.0/16
On va se connecter à slave2 pour étudier un peu les connexions auxquelles on a accès:
docker-compose -p slave2 -f docker-compose-slave.yml exec service_out bash
ping service_3 # On ping service_3 sur slave2_internal
ping service_4 # On ping service_4 sur slave2_internal
ping service_entry # On ping le premier service_entry sur inout (!!)
ping service_out # On ping le premier service_out sur inout (!!)
ping master_service_entry # Hôte non accessible
ping master_service_entry_1 # On ping service_entry de master sur inout
En résumé, pour atteindre le bon hôte, la manière la plus sure est de passer par le nom complet du conteneur: <nom projet>_<nom conteneur>_<numéro conteneur>
.
Cela dit, on peut aussi se rendre la vie plus facile en utilisant les alias, qui permettent de nommer des conteneurs sur un réseau. Seule limite: sur un réseau donné, les alias doivent être uniques.
Je ne vais pas remettre tous les fichiers ici, seulement le docker-compose-slave.yml et run.sh pour donner l’idée:
cat docker-compose-slave.yml
version: '2'
services:
service_entry:
image: service_1
expose:
- "443"
networks:
internal:
aliases:
- entry
inout:
aliases:
- SLAVE_ALIAS
service_out:
image: service_2
networks:
internal:
aliases:
- out
inout:
service_3:
image: service_3
networks:
- internal
service_4:
image: service_4
networks:
- internal
service_5:
image: service_5
networks:
- internal
networks:
internal:
driver: bridge
inout:
external:
name: inout
cat run.sh
#!/bin/bash
docker-compose -p master -f docker-compose-master.yml up -d
for i in `seq 1 $1`; do
sed -i -e "s/SLAVE_ALIAS/slave$i/g" docker-compose-slave.yml
docker-compose -p "slave$i" -f docker-compose-slave.yml up -d
sed -i -e "s/slave$i/SLAVE_ALIAS/g" docker-compose-slave.yml
done
Je l’admets, c’est pas très beau tous ces sed. Voici donc un script alternatif: on va faire des connections à la main. Imaginez juste que toute référence au réseau inout a disparu du fichier compose:
cat run.sh
#!/bin/bash
docker-compose -p master -f docker-compose-master.yml up -d
docker network connect --alias=master inout master_service_entry_1
for i in `seq 1 $1`; do
docker-compose -p "slave$i" -f docker-compose-slave.yml up -d
docker network connect --alias="slave$i" inout "slave$i"_service_entry_1 # On connecte l'entrée à inout
docker network connect inout "slave$i"_service_out_1 # On connecte la sortie à inout
done
La topologie du réseau n’a pas changé par rapport à avant, les mêmes conteneurs restent sur les mêmes réseaux. Seulement, pour communiquer sur un réseau interne, les conteneurs entry
, out
, service_3
, service_4
et service_5
sont accessibles.
Sur le réseau inout, les conteneurs master
(pourvu qu’il soit aliasé). slave1
et slave2
sont disponibles, slave1 correspondant au point d’entrée du réseau slave1 et de même pour slave2.
Les points de sortie ont seulement besoin de pouvoir communiquer avec les points d’entrée, c’est pour cela qu’ils n’ont pas d’alias.
Résumé
Qu’est ce qu’on a vu ? Pas mal de choses au final. Docker compose est super pour lancer des applications simplement. Et quand on veut générer des infras un peu plus compliquées, il permet de gérer très simplement des réseaux divers.
Lors de la partie d’avant on a vu comment connecter des conteneurs entre eux. Là on a vu comment connecter des réseaux de conteneurs entre eux, en juste quelques lignes de configuration.
Est-ce qu’on peut aller plus loin ? Et bien, il y a une troisième partie :smile: .