Docker-compose Comment attendre que le conteneur soit prêt

Présentation


Il existe de nombreux articles sur l'exécution de conteneurs et l'écriture de docker-compose.yml . Mais pour moi pendant longtemps, la question n'était pas claire de savoir comment procéder correctement si un conteneur ne devait pas être lancé jusqu'à ce qu'un autre conteneur soit prêt à traiter ses demandes ou à effectuer une certaine quantité de travail.

Cette question est devenue pertinente après que nous avons commencé à utiliser activement docker-compose , au lieu de lancer des dockers individuels.

À quoi ça sert?


En effet, laissons l'application dans le conteneur B dépendre de la disponibilité du service dans le conteneur A. Et au démarrage, l'application dans le conteneur B ne reçoit pas ce service. Que faut-il faire?

Il y a deux options:

  • le premier est de mourir (de préférence avec un code d'erreur)
  • la seconde consiste à attendre, puis à mourir de toute façon, si l'application dans le conteneur B n'a pas répondu pendant le délai imparti

Après la mort du conteneur B, docker-compose (en fonction de la configuration bien sûr) le redémarrera et l'application dans le conteneur B tentera à nouveau d'accéder au service dans le conteneur A.

Cela continuera jusqu'à ce que le service du conteneur A soit prêt à répondre aux demandes ou jusqu'à ce que nous remarquions que le conteneur est constamment surchargé.
Et en fait, c'est la voie normale pour une architecture multi-conteneurs.

Mais, en particulier, nous avons été confrontés à une situation où le conteneur A démarre et prépare les données pour le conteneur B.L'application dans le conteneur B n'est pas en mesure de vérifier si les données sont prêtes ou non, elle commence immédiatement à travailler avec elles. Par conséquent, nous devons recevoir et traiter nous-mêmes le signal de disponibilité des données.

Je pense que vous pouvez toujours donner quelques cas d'utilisation. Mais surtout, vous devez comprendre exactement pourquoi vous faites cela. Sinon, il est préférable d'utiliser les outils de composition Docker standard.

Un peu d'idéologie


Si vous lisez attentivement la documentation, tout y est écrit. À savoir, chaque
l'unité est indépendante et doit veiller à ce que tous les services
avec lequel il va travailler, sont à sa disposition.

Par conséquent, la question n'est pas de démarrer ou de ne pas démarrer le conteneur, mais de
à l'intérieur du conteneur, vérifiez l'état de préparation de tous les services requis et seulement
puis transférez le contrôle à l'application conteneur.

Comment est-il mis en œuvre


Pour résoudre ce problème, la description de docker-compose m'a beaucoup aidé, cette partie
et un article sur la bonne utilisation de point d' entrée et cmd .

Donc, ce que nous devons obtenir:

  • il y a une annexe A que nous avons enveloppée dans le conteneur A
  • il démarre et commence à répondre OK sur le port 8000
  • et aussi, il y a l'application B, que nous commençons à partir du conteneur B, mais elle devrait commencer à fonctionner pas plus tôt que l'application A commencera à répondre aux demandes sur le port 8000

La documentation officielle propose deux façons de résoudre ce problème.

La première consiste à écrire votre propre point d' entrée dans le conteneur, qui effectuera toutes les vérifications, puis à démarrer l'application de travail.

La seconde consiste à utiliser le fichier de commandes déjà écrit wait-for-it.sh .
Nous avons essayé dans les deux sens.

Écrire votre propre point d'entrée


Qu'est-ce que le point d' entrée ?

Il s'agit uniquement du fichier exécutable que vous spécifiez lors de la création du conteneur dans le Dockerfile dans le champ ENTRYPOINT . Ce fichier, comme déjà mentionné, effectue des vérifications, puis lance l'application principale du conteneur.

Donc, ce que nous obtenons:

Créez un dossier Entrypoint .

Il a deux sous-dossiers - container_A et container_B . Nous y créerons nos conteneurs.

Pour le conteneur A, prenons un simple serveur http sur python. Après le démarrage, il commence à répondre pour obtenir des demandes sur le port 8000.

Pour rendre notre expérience plus explicite, nous avons défini un délai de 15 secondes avant de démarrer le serveur.

Il s'avère que le fichier docker suivant pour le conteneur A :

FROM python:3 EXPOSE 8000 CMD sleep 15 && python3 -m http.server --cgi 

Pour le conteneur B, créez le fichier docker suivant pour le conteneur B :

 FROM ubuntu:18.04 RUN apt-get update RUN apt-get install -y curl COPY ./entrypoint.sh /usr/bin/entrypoint.sh ENTRYPOINT [ "entrypoint.sh" ] CMD ["echo", "!!!!!!!! Container_A is available now !!!!!!!!"] 

Et mettez notre exécutable entrypoint.sh dans le même dossier. Nous allons l'avoir comme ça

 #!/bin/bash set -e host="conteiner_a" port="8000" cmd="$@" >&2 echo "!!!!!!!! Check conteiner_a for available !!!!!!!!" until curl http://"$host":"$port"; do >&2 echo "Conteiner_A is unavailable - sleeping" sleep 1 done >&2 echo "Conteiner_A is up - executing command" exec $cmd 

Que se passe-t-il dans le conteneur B:

  • Quand il démarre, il démarre ENTRYPOINT , c'est-à-dire lance entrypoint.sh
  • entrypoint.sh , en utilisant curl , commence à interroger le port 8000 pour le conteneur A. Il le fait jusqu'à ce qu'il reçoive une réponse 200 (c'est-à-dire que curl se terminera dans ce cas par un résultat nul et la boucle se terminera)
  • Lorsque 200 est reçu, la boucle se termine et le contrôle passe à la commande spécifiée dans la variable $ cmd . Et cela indique ce que nous avons indiqué dans le fichier docker dans le champ CMD , c'est-à-dire echo "!!! Container_A est disponible maintenant !!!!!!!!" Pourquoi en est-il ainsi, est décrit dans l' article ci-dessus
  • Nous imprimons - !!! Container_A est disponible maintenant !!! et conclure.

Nous allons tout démarrer avec docker-compose .

docker-compose.yml nous avons ici ceci:

 version: '3' networks: waiting_for_conteiner: services: conteiner_a: build: ./conteiner_A container_name: conteiner_a image: conteiner_a restart: unless-stopped networks: - waiting_for_conteiner ports: - 8000:8000 conteiner_b: build: ./conteiner_B container_name: conteiner_b image: waiting_for_conteiner.entrypoint.conteiner_b restart: "no" networks: - waiting_for_conteiner 

Ici, dans conteiner_a, il n'est pas nécessaire de spécifier les ports: 8000: 8000 . Cela a été fait afin de pouvoir vérifier le fonctionnement du serveur http fonctionnant dedans de l'extérieur.

En outre, le conteneur B ne redémarre pas après l'arrêt.

Nous lançons:

 docker-compose up —-build 

Nous voyons que pendant 15 secondes, il y a un message sur l'indisponibilité du conteneur A, puis

 conteiner_b | Conteiner_A is unavailable - sleeping conteiner_b | % Total % Received % Xferd Average Speed Time Time Time Current conteiner_b | Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> conteiner_b | <html> conteiner_b | <head> conteiner_b | <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> conteiner_b | <title>Directory listing for /</title> conteiner_b | </head> conteiner_b | <body> conteiner_b | <h1>Directory listing for /</h1> conteiner_b | <hr> conteiner_b | <ul> conteiner_b | <li><a href=".dockerenv">.dockerenv</a></li> conteiner_b | <li><a href="bin/">bin/</a></li> conteiner_b | <li><a href="boot/">boot/</a></li> conteiner_b | <li><a href="dev/">dev/</a></li> conteiner_b | <li><a href="etc/">etc/</a></li> conteiner_b | <li><a href="home/">home/</a></li> conteiner_b | <li><a href="lib/">lib/</a></li> conteiner_b | <li><a href="lib64/">lib64/</a></li> conteiner_b | <li><a href="media/">media/</a></li> conteiner_b | <li><a href="mnt/">mnt/</a></li> conteiner_b | <li><a href="opt/">opt/</a></li> conteiner_b | <li><a href="proc/">proc/</a></li> conteiner_b | <li><a href="root/">root/</a></li> conteiner_b | <li><a href="run/">run/</a></li> conteiner_b | <li><a href="sbin/">sbin/</a></li> conteiner_b | <li><a href="srv/">srv/</a></li> conteiner_b | <li><a href="sys/">sys/</a></li> conteiner_b | <li><a href="tmp/">tmp/</a></li> conteiner_b | <li><a href="usr/">usr/</a></li> conteiner_b | <li><a href="var/">var/</a></li> conteiner_b | </ul> conteiner_b | <hr> conteiner_b | </body> conteiner_b | </html> 100 987 100 987 0 0 98700 0 --:--:-- --:--:-- --:--:-- 107k conteiner_b | Conteiner_A is up - executing command conteiner_b | !!!!!!!! Container_A is available now !!!!!!!! 

Nous obtenons une réponse à votre demande, imprimez !!! Container_A est disponible maintenant !!!!!!!! et conclure.

Utilisation de wait-for-it.sh


Il vaut la peine de dire tout de suite que ce chemin n'a pas fonctionné pour nous comme décrit dans la documentation.
À savoir, il est connu que si ENTRYPOINT et CMD sont écrits dans le Dockerfile , alors lorsque le conteneur démarre, la commande de ENTRYPOINT sera exécutée et le contenu de CMD lui sera transmis en tant que paramètres.

Il est également connu que ENTRYPOINT et CMD spécifiés dans le Dockerfile peuvent être redéfinis dans docker-compose.yml

Le format de démarrage wait-for-it.sh est le suivant:

 wait-for-it.sh __ -- ___ 

Ensuite, comme indiqué dans l' article , nous pouvons définir un nouveau ENTRYPOINT dans docker-compose.yml , et le CMD sera remplacé à partir du Dockerfile .

Ainsi, nous obtenons:

Le fichier Docker pour le conteneur A reste inchangé:

 FROM python:3 EXPOSE 8000 CMD sleep 15 && python3 -m http.server --cgi 

Fichier Docker pour le conteneur B

 FROM ubuntu:18.04 COPY ./wait-for-it.sh /usr/bin/wait-for-it.sh CMD ["echo", "!!!!!!!! Container_A is available now !!!!!!!!"] 

Docker-compose.yml ressemble à ceci:

 version: '3' networks: waiting_for_conteiner: services: conteiner_a: build: ./conteiner_A container_name: conteiner_a image: conteiner_a restart: unless-stopped networks: - waiting_for_conteiner ports: - 8000:8000 conteiner_b: build: ./conteiner_B container_name: conteiner_b image: waiting_for_conteiner.wait_for_it.conteiner_b restart: "no" networks: - waiting_for_conteiner entrypoint: ["wait-for-it.sh", "-s" , "-t", "20", "conteiner_a:8000", "--"] 

Nous exécutons la commande wait-for-it , lui demandons d'attendre 20 secondes jusqu'à ce que le conteneur A prenne vie et spécifions un autre paramètre «-» , qui devrait séparer les paramètres wait-for-it du programme qu'il lancera après son achèvement.

Nous essayons!
Et malheureusement, nous n'obtenons rien.

Si nous vérifions avec quels arguments nous exécutons l' attente , alors nous verrons que seul ce que nous avons spécifié dans le point d' entrée lui est transmis , la CMD du conteneur n'est pas attachée.

Option de travail


Ensuite, il n'y a qu'une seule option. Ce que nous avons spécifié dans le CMD dans le Dockerfile , nous devons le transférer à la commande dans docker-compose.yml .

Ensuite, laissez le Dockerfile du conteneur B inchangé, et docker-compose.yml ressemblera à ceci:

 version: '3' networks: waiting_for_conteiner: services: conteiner_a: build: ./conteiner_A container_name: conteiner_a image: conteiner_a restart: unless-stopped networks: - waiting_for_conteiner ports: - 8000:8000 conteiner_b: build: ./conteiner_B container_name: conteiner_b image: waiting_for_conteiner.wait_for_it.conteiner_b restart: "no" networks: - waiting_for_conteiner entrypoint: ["wait-for-it.sh", "-s" ,"-t", "20", "conteiner_a:8000", "--"] command: ["echo", "!!!!!!!! Container_A is available now !!!!!!!!"] 

Et dans cette version, cela fonctionne.

En conclusion, il faut dire qu'à notre avis, la bonne voie est la première. Il est le plus polyvalent et vous permet d'effectuer un contrôle de préparation de toutes les manières possibles. L'attente est juste un utilitaire utile que vous pouvez utiliser séparément ou en l'intégrant dans votre entrypoint.sh .

Source: https://habr.com/ru/post/fr454552/


All Articles