Docker-compose Cómo esperar hasta que el contenedor esté listo

Introduccion


Hay muchos artículos sobre cómo ejecutar contenedores y escribir docker-compose.yml . Pero para mí, durante mucho tiempo, la pregunta no fue clara sobre cómo proceder correctamente si algún contenedor no se inicia hasta que otro contenedor esté listo para procesar sus solicitudes o completar una cantidad de trabajo.

Esta pregunta se volvió relevante después de que comenzamos a usar activamente docker-compose , en lugar de lanzar dockers individuales.

Para que sirve


De hecho, deje que la aplicación en el contenedor B dependa de la disponibilidad del servicio en el contenedor A. Y al inicio, la aplicación en el contenedor B no recibe este servicio. Que debe hacer

Hay dos opciones:

  • el primero es morir (preferiblemente con un código de error)
  • el segundo es esperar y morir de todos modos, si la aplicación en el contenedor B no respondió durante el tiempo de espera asignado

Después de que el contenedor B haya muerto, Docker-compose (dependiendo de la configuración, por supuesto) lo reiniciará y la aplicación en el contenedor B nuevamente intentará llegar al servicio en el contenedor A.

Esto continuará hasta que el servicio en el contenedor A esté listo para responder a las solicitudes, o hasta que notemos que el contenedor se sobrecarga constantemente.
Y de hecho, esta es la forma normal para la arquitectura de contenedores múltiples.

Pero, en particular, nos enfrentamos a una situación en la que el contenedor A se inicia y prepara los datos para el contenedor B. La aplicación en el contenedor B no puede verificar si los datos están listos o no, inmediatamente comienza a trabajar con ellos. Por lo tanto, tenemos que recibir y procesar la señal sobre la disponibilidad de datos por nuestra cuenta.

Creo que todavía puedes dar algunos casos de uso. Pero lo más importante, debe comprender exactamente por qué está haciendo esto. De lo contrario, es mejor usar las herramientas estándar de composición acoplable .

Un poco de ideologia


Si lees cuidadosamente la documentación, entonces todo está escrito allí. A saber, cada
la unidad es independiente y debe cuidar que todos los servicios con
con el que va a trabajar, están disponibles para él.

Por lo tanto, la pregunta no es iniciar o no iniciar el contenedor, sino
dentro del contenedor, verifique la disponibilidad de todos los servicios requeridos y solo
luego transfiera el control a la aplicación del contenedor.

¿Cómo se implementa?


Para resolver este problema, la descripción de docker-compose me ayudó mucho, esta parte
y un artículo sobre el uso adecuado de entrypoint y cmd .

Entonces, lo que necesitamos obtener:

  • hay un apéndice A que envolvimos en el contenedor A
  • comienza y comienza a responder OK en el puerto 8000
  • y también, hay una aplicación B, que comenzamos desde el contenedor B, pero debería comenzar a funcionar no antes de que la aplicación A comience a responder a las solicitudes en el puerto 8000

La documentación oficial ofrece dos formas de resolver este problema.

El primero es escribir su propio punto de entrada en el contenedor, que realizará todas las verificaciones y luego iniciará la aplicación de trabajo.

El segundo es usar el archivo por lotes ya escrito wait-for-it.sh .
Intentamos en ambos sentidos.

Escribir su propio punto de entrada


¿Qué es el punto de entrada ?

Este es solo el archivo ejecutable que especifica al crear el contenedor en el Dockerfile en el campo ENTRYPOINT . Este archivo, como ya se mencionó, realiza comprobaciones y luego inicia la aplicación principal del contenedor.

Entonces, lo que obtenemos:

Crear una carpeta de punto de entrada.

Tiene dos subcarpetas: container_A y container_B . Crearemos nuestros contenedores en ellos.

Para el contenedor A, tomemos un servidor http simple en python. Él, después de comenzar, comienza a responder para obtener solicitudes en el puerto 8000.

Para que nuestro experimento sea más explícito, establecemos un retraso de 15 segundos antes de iniciar el servidor.

Resulta el siguiente archivo acoplable para el contenedor A :

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

Para el contenedor B, cree el siguiente archivo acoplable para el contenedor 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 !!!!!!!!"] 

Y ponga nuestro ejecutable entrypoint.sh en la misma carpeta. Lo tendremos así

 #!/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 

Lo que está sucediendo en el contenedor B:

  • Cuando comienza, comienza ENTRYPOINT , es decir lanza entrypoint.sh
  • entrypoint.sh , usando curl , comienza a sondear el puerto 8000 para el contenedor A. Lo hace hasta que recibe una respuesta 200 (es decir, curl en este caso terminará con un resultado cero y el ciclo finalizará)
  • Cuando se recibe 200, el ciclo finaliza y el control pasa al comando especificado en la variable $ cmd . E indica lo que indicamos en el archivo acoplable en el campo CMD , es decir echo "!!! Container_A está disponible ahora !!!!!!!!" Por qué esto es así, se describe en el artículo anterior.
  • Imprimimos - !!! ¡Container_A ya está disponible! y concluir

Comenzaremos todo con docker-compose .

docker-compose.yml aquí tenemos esto:

 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 

Aquí, en conteiner_a, no es necesario especificar puertos: 8000: 8000 . Esto se hizo para poder verificar el funcionamiento del servidor http que se ejecuta desde afuera.

Además, el contenedor B no se reinicia después del apagado.

Lanzamos:

 docker-compose up —-build 

Vemos que durante 15 segundos hay un mensaje sobre la falta de disponibilidad del contenedor A, y luego

 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 !!!!!!!! 

Recibimos una respuesta a su solicitud, imprime !!! Container_A está disponible ahora !!!!!!!! y concluir

Usando wait-for-it.sh


Vale la pena decir de inmediato que este camino no funcionó para nosotros como se describe en la documentación.
A saber, se sabe que si ENTRYPOINT y CMD se escriben en el Dockerfile , cuando se inicie el contenedor, el comando de ENTRYPOINT se ejecutará y el contenido de CMD se pasará a él como parámetros.

También se sabe que ENTRYPOINT y CMD especificados en el Dockerfile se pueden redefinir en docker-compose.yml

El formato de inicio wait-for-it.sh es el siguiente:

 wait-for-it.sh __ -- ___ 

Luego, como se indica en el artículo , podemos definir un nuevo ENTRYPOINT en docker-compose.yml , y el CMD será sustituido por el Dockerfile .

Entonces, obtenemos:

El archivo Docker para el contenedor A permanece sin cambios:

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

Archivo Docker para contenedor 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 tiene este aspecto:

 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", "--"] 

Ejecutamos el comando wait-for-it , le decimos que espere 20 segundos hasta que el contenedor A cobre vida, y especifiquemos otro parámetro "-" , que debería separar los parámetros wait-for-it del programa que se lanzará después de su finalización.

Lo intentamos!
Y desafortunadamente, no obtenemos nada.

Si verificamos con qué argumentos ejecutamos wait-for-it, veremos que solo se le pasa lo que especificamos en el punto de entrada , no se adjunta el CMD del contenedor.

Opción de trabajo


Entonces, solo hay una opción. Lo que hemos especificado en el CMD en el Dockerfile , debemos transferirlo al comando en docker-compose.yml .

Luego, deje el Dockerfile del contenedor B sin cambios, y docker-compose.yml se verá así:

 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 !!!!!!!!"] 

Y en esta versión, funciona.

En conclusión, hay que decir que, en nuestra opinión, la forma correcta es la primera. Es el más versátil y le permite hacer una verificación de preparación de cualquier manera posible. Wait-for-it es solo una utilidad útil que puede usar por separado o incrustando en su entrypoint.sh .

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


All Articles