Docker-compor Como esperar até que o contêiner esteja pronto

1. Introdução


Existem muitos artigos sobre como executar contêineres e como escrever o docker-compose.yml . Mas, para mim, por um longo tempo, a pergunta não ficou clara sobre como proceder corretamente se algum contêiner não deveria ser iniciado até que outro contêiner esteja pronto para processar suas solicitações ou concluir alguma quantidade de trabalho.

Essa questão se tornou relevante depois que começamos a usar ativamente o docker-compondo , em vez de iniciar estivadores individuais.

Para que serve?


De fato, deixe o aplicativo no contêiner B depender da disponibilidade do serviço no contêiner A. E na inicialização, o aplicativo no contêiner B não recebe esse serviço. O que deveria fazer?

Existem duas opções:

  • o primeiro é morrer (de preferência com um código de erro)
  • o segundo é esperar e morrer de qualquer maneira, se o aplicativo no contêiner B não responder pelo tempo limite alocado

Após a morte do contêiner B, o docker-compose (dependendo da configuração, é claro) o reinicia e o aplicativo no contêiner B tenta novamente acessar o serviço no contêiner A.

Isso continuará até que o serviço no contêiner A esteja pronto para responder às solicitações ou até percebermos que o contêiner está sendo sobrecarregado constantemente.
E, de fato, esse é o caminho normal para a arquitetura de vários contêineres.

Mas, em particular, nos deparamos com uma situação em que o contêiner A é inicializado e prepara os dados para o contêiner B. O aplicativo no contêiner B não é capaz de verificar se os dados estão prontos ou não, ele imediatamente começa a trabalhar com eles. Portanto, temos que receber e processar o sinal sobre a disponibilidade de dados por conta própria.

Eu acho que você ainda pode dar alguns casos de uso. Mas o mais importante, você precisa entender exatamente por que está fazendo isso. Caso contrário, é melhor usar as ferramentas padrão de composição do docker .

Um pouco de ideologia


Se você ler atentamente a documentação, tudo estará escrito lá. Ou seja, cada
a unidade é independente e deve cuidar para que todos os serviços com
com o qual ele vai trabalhar, estão disponíveis para ele.

Portanto, a questão não é iniciar ou não iniciar o contêiner, mas sim
dentro do contêiner, verifique a disponibilidade de todos os serviços necessários e apenas
depois transfira o controle para o aplicativo de contêiner.

Como é implementado


Para resolver esse problema, a descrição da janela de encaixe-composição me ajudou muito, essa parte dela
e um artigo sobre o uso adequado do ponto de entrada e do cmd .

Então, o que precisamos obter:

  • há um apêndice A que embrulhámos no recipiente A
  • ele inicia e começa a responder OK na porta 8000
  • e também há o aplicativo B, que iniciamos a partir do contêiner B, mas ele deve começar a funcionar não antes que o aplicativo A comece a responder às solicitações na porta 8000

A documentação oficial oferece duas maneiras de resolver esse problema.

A primeira é escrever seu próprio ponto de entrada no contêiner, que executará todas as verificações e, em seguida, iniciará o aplicativo de trabalho.

O segundo é usar o arquivo em lotes já gravado wait-for-it.sh .
Tentamos nos dois sentidos.

Escrevendo seu próprio ponto de entrada


O que é ponto de entrada ?

Este é apenas o arquivo executável que você especifica ao criar o contêiner no Dockerfile no campo ENTRYPOINT . Esse arquivo, como já mencionado, executa verificações e inicia o aplicativo principal do contêiner.

Então, o que temos:

Crie uma pasta de ponto de entrada.

Possui duas subpastas - container_A e container_B . Vamos criar nossos contêineres neles.

Para o contêiner A, vamos usar um servidor http simples em python. Ele, após iniciar, começa a responder para obter solicitações na porta 8000.

Para tornar nosso experimento mais explícito, definimos um atraso de 15 segundos antes de iniciar o servidor.

Acontece o seguinte arquivo de janela de encaixe para o contêiner A :

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

Para o contêiner B, crie o seguinte arquivo de janela de encaixe para o contêiner 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 !!!!!!!!"] 

E coloque nosso executável entrypoint.sh na mesma pasta. Teremos assim

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

O que está acontecendo no contêiner B:

  • Quando inicia, inicia ENTRYPOINT , ou seja, lança entrypoint.sh
  • entrypoint.sh , usando curl , inicia a porta de pesquisa 8000 para o contêiner A. Faz isso até receber uma resposta de 200 (ou seja, curl nesse caso terminará com um resultado zero e o loop terminará)
  • Quando 200 é recebido, o loop termina e o controle passa para o comando especificado na variável $ cmd . E indica o que indicamos no arquivo docker no campo CMD , ou seja, eco "!!! Container_A já está disponível !!!!!!!!" Por que isso é assim, está descrito no artigo acima
  • Nós imprimimos - !!! Container_A já está disponível !!! e concluir.

Começaremos tudo com o docker-compose .

docker-compose.yml aqui temos o seguinte:

 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 

Aqui, em conteiner_a, não é necessário especificar portas: 8000: 8000 . Isso foi feito para poder verificar a operação do servidor http em execução a partir do exterior.

Além disso, o contêiner B não reinicia após o desligamento.

Lançamos:

 docker-compose up —-build 

Vemos que por 15 segundos há uma mensagem sobre a indisponibilidade do contêiner A e, em seguida,

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

Temos uma resposta para o seu pedido, imprima !!! Container_A já está disponível !!!!!!!! e concluir.

Usando wait-for-it.sh


Vale dizer imediatamente que esse caminho não funcionou para nós, conforme descrito na documentação.
Ou seja, sabe-se que, se ENTRYPOINT e CMD forem gravados no Dockerfile , quando o contêiner iniciar, o comando de ENTRYPOINT será executado e o conteúdo do CMD será passado como parâmetros.

Também é sabido que ENTRYPOINT e CMD especificados no Dockerfile podem ser redefinidos em docker-compose.yml

O formato de inicialização wait-for-it.sh é o seguinte:

 wait-for-it.sh __ -- ___ 

Em seguida, conforme indicado no artigo , podemos definir um novo ENTRYPOINT no docker-compose.yml e o CMD será substituído no Dockerfile .

Então, nós temos:

O arquivo do Docker para o contêiner A permanece inalterado:

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

Arquivo Docker para contêiner B

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

O Docker-compose.yml fica assim:

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

Executamos o comando wait-for-it , esperamos 20 segundos até que o contêiner A ganhe vida e especificamos outro parâmetro “-” , que deve separar os parâmetros do wait-for-it do programa que será iniciado após sua conclusão.

Nós tentamos!
E, infelizmente, não recebemos nada.

Se verificarmos com quais argumentos executamos o wait-for-it, veremos que apenas o que especificamos no ponto de entrada é passado para ele , o CMD do contêiner não é anexado.

Opção de trabalho


Então, há apenas uma opção. O que especificamos no CMD no Dockerfile , devemos transferir para o comando no docker-compose.yml .

Em seguida, deixe o Dockerfile do contêiner B inalterado e o docker-compose.yml ficará assim:

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

E nesta versão, funciona.

Em conclusão, deve-se dizer que, em nossa opinião, o caminho certo é o primeiro. É o mais versátil e permite que você faça uma verificação de prontidão da maneira que for possível. Wait-for-it é apenas um utilitário útil que você pode usar separadamente ou incorporando em seu entrypoint.sh .

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


All Articles