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í
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.ymlEl
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 .