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
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.ymlO
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 .