Alô galera! Dando continuidade a série sobre docker vamos ao hands on.
Nos dois primeiros posts da série apresentamos os conceitos por trás da plataforma docker. No primeiro post abordamos os principais componentes e a função que cada um desempenha na arquitetura. No post seguinte abordamos o método de gerenciamento de redes usado pelo docker. Compreendemos a estrutura básica das redes fornecidas pelo docker, e os componentes do kernel linux usados na construção dos diferentes tipos de redes.
Nesse post vamos abordar na prática, os conceitos vistos até agora. Então, chega de papo e vamos ao hands on.
Toda a prática abordada nesse post, segue o modelo mais básico de funcionamento do docker, ou seja, cliente e daemon rodando na mesma máquina. A máquina pode ser virtual ou física e precisa estar conectada a internet para efetuar o download das imagens. Para esse post utilizamos uma máquina virtual (KVM) chamada docker-host, rodando Debian 8 em sua instalação mais básica (netinst).
É possível utilizar sistemas como Microsoft Windows ou Mac OS X (não abordados nesse post), porém, o docker utilizar recursos do kernel Linux, o que significa que ele não pode ser instalado nativamente nos sistemas em questão. Para esses sistemas a Docker desenvolveu a solução Docker Toolbox, que trás uma máquina virtual (Virtualbox) preparada para rodar o docker, além de outras ferramentas necessárias.
Apenas dois requisitos são exigidos para a instalação do docker em sistemas Linux. O sistema tem de ser 64-bits e a versão do kernel igual ou superior a 3.10. Com a máquina pronta, vamos à instalação.
O processo consiste em instalar o suporte ssl do manipulador de pacotes (apt-get) e os certificados necessários. Em seguida é necessário adicionar a chave pública para acessar o repositório dockerproject. Por fim, basta instalar o docker-engine. Todo esse processo é apresentados abaixo:
# apt-get install apt-transport-https ca-certificates -y
# apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys 58118E89F3A912897C070ADBF76221572C52609D
# echo "deb https://apt.dockerproject.org/repo debian-jessie main" > /etc/apt/sources.list.d/docker.list
# apt-get update
# apt-get install docker-engine -y
Como vimos no primeiro post, a instalação do docker-engine configura o cliente e o daemon (server) no mesmo host. Dois comandos podem ser utilizados para obter informações sobre esses componentes, o docker version e o docker info. O comando docker version exibe as informações de versão tanto do cliente quanto do daemon. Já o comando docker info exibe informações mais completas da plataforma docker.
Das informações geradas pelo comando docker info destacamos abaixo: a quantidade de containers (com seus respectivos estados) e imagens no host, o tipo de driver de armazenamento e o path utilizado por este, e o serviço de registro utilizado pelo docker (hub).
# docker info Containers: 0 Running: 0 Paused: 0 Stopped: 0 Images: 0 ... Storage Driver: aufs Root Dir: /var/lib/docker/aufs ... Registry: https://index.docker.io/v1/ ...
Vale ressaltar que o driver de armazenamento está diretamente associado ao sistema operacional utilizado. A instalação do docker-engine em sistemas Red Hat (RHEL/CentOS/Fedora) por exemplo, utiliza o driver devicemapper. Isso porque o AuFS não faz parte do mainline do kernel Linux, logo, cada distribuição implementa sua própria solução de sistema de arquivo unificado. Sendo assim, a RedHat, em parceria com a Docker, optou por implementar o devicemapper como backend de armazenamento do docker.
Uma observação importante é que os comandos docker mostrados aqui, foram executados como usuário root. É possível executar esses comandos como um usuário regular, no entanto, será necessário atribuir as permissões necessárias para que este usuário possa se comunicar com o daemon. Isso acontece porque o daemon docker utiliza socket Unix, cujo dono é o root, para comunicação com o cliente.
Embora seja necessário elevar os privilégios do usuário regular, não precisamos adicioná-lo ao sudo (o que permitiria o acesso a todo o sistema). Ao invés disso podemos adicionar o usuário ao grupo docker, criado na instalação do docker-engine. O docker atribui a este grupo as permissões necessárias para interagir com o daemon, o que permite ao usuário regular executar os comandos no cliente sem problemas.
Voltando ao docker info, repare nas últimas linhas geradas pelo comando:
# docker info ... WARNING: No memory limit support WARNING: No swap limit support WARNING: No kernel memory limit support WARNING: No oom kill disable support WARNING: No cpu cfs quota support WARNING: No cpu cfs period support
As linhas de warning informam que o kernel não foi configurado para suportar algumas funções do cgroups (veja o primeiro post). Por hora vamos habilitar apenas o recurso de limite de memória e swap, logo, precisamos configurar o kernel para suportar essa função. Para isso, basta editar o arquivo do gerenciador de boot (grub) e adicionar a linha conforme abaixo:
# vim /etc/default/grub
...
GRUB_CMDLINE_LINUX="cgroup_enable=memory swapaccount=1"
...
# update-grub
# reboot
Após reiniciar o sistema, o kernel já estará com suporte ao limite de memória.
O primeiro passo para a criação de um container é executar o comando docker run. Sua sintaxe é docker run [OPTIONS] IMAGE[:TAG] [COMMAND] [ARG…], porém, normalmente não é necessário especificar toda a sintaxe para criar um container. Isso porque algumas opções são fornecidas pelo próprio docker caso não sejam especificadas (como a TAG), enquanto outras são configuradas na criação da imagem base (COMMAND). Vale ressaltar que o nome da imagem é item obrigatório do comando. Para listar todas as opções (OPTIONS) disponíveis utilize o comando docker run –help.
Vamos começar criando um container do ubuntu. Execute o comando conforme abaixo:
root@docker-host:~# docker run -it ubuntu
As opções fornecidas (-it) informam que o container será executado em modo interativo ao ser criado. Após as opções informamos o nome da imagem (ubuntu).
O comando é enviado ao daemon que inicia o processo de criação do container, verificando se a imagem já existe no host (acrescentando a TAG latest automaticamente). Caso negativo, o daemon inicia o processo de download (pull) da imagem a partir do registro padrão do docker (hub). Repare que a imagem é baixada em camadas e que cada camada possui seu próprio identificador:
Unable to find image 'ubuntu:latest' locally
latest: Pulling from library/ubuntu
5ba4f30e5bea: Downloading [==========> ] 9.823 MB/48.65 MB
9d7d19c9dc56: Download complete
ac6ad7efd0f9: Download complete
e7491a747824: Download complete
a3ed95caeb02: Download complete
Encerrado o processo de download o daemon extrai as camadas da imagem, faz a montagem unificada, exibe o identificador hash da imagem e cria o container:
Digest: sha256:46fb5d001b88ad904c5c732b086b596b92cfb4a4840a3abd0e35dbb6870585e4 Status: Downloaded newer image for ubuntu:latest root@11056628fcd3:/#
O comando padrão da imagem do ubuntu é o /bin/bash – normalmente definido nas imagens de sistemas operacionais. O docker atribui o comando padrão da imagem como valor do campo [COMMAND] da sintaxe do docker run. Esse comando será executado automaticamente na criação dos containers, a menos que outro comando seja especificado após o nome da imagem.
O bash é um interpretador de linha de comando (shell) que permite a execução de comandos a partir da entrada padrão (STDIN) de um terminal. As opções -it do docker run em conjunto com o bash, criam um pseudo-terminal (-t) mantendo o STDIN aberto (-i), o que nos permite interagir com o container. Para sair do container mantendo-o em execução usamos a combinação Ctrl+p Ctrl+q (ou Ctrl+pq), enquanto Ctrl+d encerra (stop) o container.
Após serem criados, containers podem ser manipulados (parar, iniciar, interromper, reiniciar, etc.) através de comandos específicos. Cada comando possui suas próprias opções porém, todos exigem que o container seja identificado, e isso pode ser feito usando o identificador (container id) ou nome do container. Para exibir tais informações (além de outras), usamos o comando docker ps.
Saia do container mantendo-o em execução e em seguida liste os containers:
root@docker-host:~# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 11056628fcd3 ubuntu "/bin/bash" 3 hours ago Up 3 hours nauseous_aryabhata
O identificador do container, como dissemos em outros posts, é definido como os primeiros doze dígitos da camada do container. Além disso o docker configura automaticamente esse identificador como hostname do container, a menos que seja definido com a opção -h.
Já o nome do container é gerado automaticamente no momento de sua criação e, assim como o hostname, pode ser definido manualmente através da opção –name seguida do nome desejado. A diferença entre hostname do container e nome do container foi abordada no post sobre gerenciamento de redes.
Podemos retornar ao container usando o comando docker attach. No entanto, é necessário que o container esteja em execução e que tenha sido criado em modo interativo. Caso o container não esteja em execução, devemos utilizar o docker start.
Utilizando o container id obtido no passo anterior, retorne ao container com o comando docker attach:
root@docker-host:~# docker attach 11056628fcd3
root@11056628fcd3:/#
Verifique a versão do sistema operacional e os processos em execução. Repare que apenas os processos necessários para execução do container são criados. Em seguida saia usando as teclas Ctrl+d (encerrando o container):
root@11056628fcd3:/# cat /etc/issue Ubuntu 16.04 LTS \n \l root@11056628fcd3:/# ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.3 18240 3200 ? Ss 14:23 0:00 /bin/bash root 12 0.0 0.2 34424 2876 ? R+ 14:24 0:00 ps aux
Liste novamente os containers, dessa vez utilizando a opção -a do comando docker ps. Essa opção é usada para exibir todos os containers (independente do status).
Volume de dados
A partir desse ponto vamos mostrar na prática alguns conceitos abordados anteriormente. O primeiro deles é o volume de dados. Pra começar, remova o container anterior com o comando docker rm:
root@docker-host:~# docker rm 11056628fcd3
Em seguida crie dois novos containers, usando as opções -h e –name para definir os nomes dos container e seus hostnames (apenas para melhor visualização).
Definimos o nome do primeiro container como temp e do segundo como persist, exportando o volume /data desse último com a opção -v do comando docker run.
root@docker-host:~# docker run -it -h temp --name temp ubuntu root@docker-host:~# docker run -it -h persist --name persist -v /data/:/data/ ubuntu
O método de exportação de volumes permite especificar o diretório do host e o diretório do container (/hostdir/:/containerdir/), como no caso acima, ou somente o diretório do container (/containerdir/). A diferença é que no segundo método o volume será montado dentro do diretório base do docker, o que significa que ele poderá ser excluído junto com o container (veremos mais adiante). Vale ressaltar que o diretório pode ou não existir. Caso o diretório não exista (nosso caso) ele será criado tanto no host quanto no container.
Em seguida crie um arquivo chamado userlist no diretório /data dos containers. Dessa vez, ao invés de retornar ao container com o comando docker attach, utilize o comando docker exec como mostrado abaixo:
root@docker-host:~# docker exec temp touch /data/userlist root@docker-host:~# docker exec temp ls /data/ userlist root@docker-host:~# docker exec persist touch /data/userlist root@docker-host:~# docker exec persist ls /data/ userlist
O comando docker exec executa instruções (comandos) no container em execução, sem a necessidade de conectar-se ao seu terminal. Esses comandos, definidos após o nome (ou identificador) do container, serão executados diretamente nele.
Em seguida remova os dois containers. Vale ressaltar que o docker não remove containers em execução, logo, precisamos encerrá-los primeiro com o comando docker stop. Veja os comandos abaixo:
root@docker-host:~# docker stop $(docker ps -a -q) root@docker-host:~# docker rm $(docker ps -a -q)
Para simplificar esse processo utilizamos a variável $(docker ps -a -q) como alvo dos comandos stop e rm. Essa combinação é bastante útil quando precisamos manipular diversos containers. O comando docker ps, assim como a opção -a, já foram vistos anteriormente. Nós acrescentamos a opção -q, que exibe apenas a coluna CONTAINER ID do comando docker ps.
Agora vamos recriar os containers exatamente como antes, e listar o conteúdo do diretório /data em cada um:
root@docker-host:~# docker run -it -h temp --name temp ubuntu root@temp:/# ls /data/ root@docker-host:~# docker run -it -h persist --name persist -v /data/:/data/ ubuntu root@persist:/# ls /data/ userlist
Repare que apenas o container persist possui o arquivo criado. Isso porque o volume de dados onde o arquivo foi criado foi exportado para o host.
Vamos testar novamente o volume de dados, mas dessa vez especificando apenas o diretório do container. Para isso, crie um novo container (chamado volatile), exportando o volume de dados conforme mostrado abaixo. Em seguida crie um arquivo dentro do diretório.
root@docker-host:~# docker run -it -h volatile --name volatile -v /store ubuntu
root@volatile:/# touch /store/grouplist
Como não especificamos um diretório no host, o docker criou um diretório no host para montar o volume exportado. A localização e o nome desse diretório podem ser obtidos com o comando docker inspect. Saia do container e execute o comando conforme abaixo:
root@docker-host:~# docker inspect --format='{{json .Mounts}}' volatile | python -m json.tool [ { "Destination": "/store", "Driver": "local", "Mode": "", "Name": "612ac4846b3a7d58fdad81dca17b281a2f62a439f5c91845c71cf470d7790b26", "Propagation": "", "RW": true, "Source": "/var/lib/docker/volumes/612ac4846b3a7d58fdad81dca17b281a2f62a439f5c91845c71cf470d7790b26/_data" } ]
O comando docker inspect é usado para listar todas as informações de um container. No exemplo acima utilizamos uma formatação (–format) para exibir apenas as informações dos volumes. Repare que o docker criou um diretório em sua área de armazenamento (Source) para montar o diretório exportado do container (Destination). Esse tipo de volume pode ser removido juntamente com o container usando a opção -v do comando docker rm:
root@docker-host:~# ls /var/lib/docker/volumes/612ac4846b3a7d58fdad81dca17b281a2f62a439f5c91845c71cf470d7790b26/_data/
grouplist
root@docker-host:~# docker stop volatile && docker rm -v volatile
root@docker-host:~# ls /var/lib/docker/volumes/612ac4846b3a7d58fdad81dca17b281a2f62a439f5c91845c71cf470d7790b26/_data/
ls: não é possível acessar /var/lib/docker/volumes/612ac4846b3a7d58fdad81dca17b281a2f62a439f5c91845c71cf470d7790b26/_data/: Arquivo ou diretório não encontrado
Um volume exportado pode, inclusive, ser compartilhado entre containers. Crie um container chamado shared, que irá compartilhar o volume /data do container persist, através da opção –volumes-from do comando docker run:
root@docker-host:~# docker run -it -h shared --name shared --volumes-from persist ubuntu root@shared:/# ls /data/ userlist
Essa opção é bastante útil quando se pretende compartilhar arquivos e diretórios entre diversos containers. Além disso, é possível limitar o acesso (somente leitura) de um determinado container ao volume compartilhado, especificando a opção –volumes-from:ro.
Gerenciamento de redes
O próximo conceito que veremos na prática é o gerenciamento de redes. Para começar, vamos listar os tipos de redes disponíveis no docker:
root@docker-host:~# docker network ls NETWORK ID NAME DRIVER 16fe8105e615 bridge bridge c74c760a8f0d host host 690b0eb7c85d none null
O comando usado é o docker network com a opção ls. Para obter uma lista completa das opções desse comando use docker network –help.
A opção usada (–ls) lista todas as redes atualmente presentes no docker, seus respectivos identificadores, e os drivers usados por cada uma. Como não criamos nenhuma rede personalizada, o comando mostra apenas as redes default criadas na instalação do docker-engine. Para obter informações detalhadas sobre as redes podemos utilizar a opção inspect seguida do nome da rede ou identificador. Vamos analisar as informações da rede usada por padrão na criação dos containers, a rede bridge:
root@docker-host:~# docker network inspect bridge [ { "Name": "bridge", "Id": "16fe8105e615b82814fc2376991366f4fbd346789dac12f3cbf6dbf97d7a2903", "Scope": "local", "Driver": "bridge", "EnableIPv6": false, "IPAM": { "Driver": "default", "Options": null, "Config": [ { "Subnet": "172.17.0.0/16" } ] }, "Internal": false, "Containers": {}, "Options": { "com.docker.network.bridge.default_bridge": "true", "com.docker.network.bridge.enable_icc": "true", "com.docker.network.bridge.enable_ip_masquerade": "true", "com.docker.network.bridge.host_binding_ipv4": "0.0.0.0", "com.docker.network.bridge.name": "docker0", "com.docker.network.driver.mtu": "1500" }, "Labels": {} } ]
Várias informações podem ser obtidas através desse comando como por exemplo, o identificador completo da rede, se o tipo de driver usado é padrão (fornecido pelo docker) ou um driver personalizado, e a sub-rede utilizada. Além disso, é possível visualizar algumas opções controladas pelo daemon como, o controle de comunicação entre container (icc), mascaramento de IP e MTU da rede.
O nome da interface virtual usada pela rede é exibida no campo com.docker.network.bridge.name. Conforme abordamos no post de gerenciamento de redes, essa é uma interface do tipo ponte (bridge). Ao criar um container o docker adiciona uma interface virtual correspondente ao container, à essa ponte. Para verificar as interfaces agregadas podemos usar o comando brctl que faz parte do pacote bridge-utils:
root@docker-host:~# apt-get install bridge-utils -y root@docker-host:~# brctl show bridge name bridge id STP enabled interfaces docker0 8000.024209c5b653 no veth1addb2d vethb05a374 vethbe494f2
Repare que as interfaces dos containers que criamos anteriormente foram adicionadas à ponte docker0 (enabled interfaces). Vale ressaltar que apenas as interfaces dos containers em execução são apresentadas na saída desse comando.
Para testar o funcionamento da rede bridge, vamos criar um container do nginx. Dessa vez vamos baixar a imagem com o comando docker pull e criar o container separadamente:
root@docker-host:~# docker pull nginx Using default tag: latest latest: Pulling from library/nginx 51f5c6a04d83: Pull complete a3ed95caeb02: Pull complete 51d229e136d0: Pull complete bcd41daec8cc: Pull complete Digest: sha256:0fe6413f3e30fcc5920bc8fa769280975b10b1c26721de956e1428b9e2f29d04 Status: Downloaded newer image for nginx:latest root@docker-host:~# docker run -d nginx 8d0ce387cd95bd76e1f1111d1f2dc32519fe299052c14228285e5584998e2f69 root@docker-host:~#
O container foi criado usando a opção -d do comando docker run. Essa opção executa o container em background (detach), exibindo apenas o identificador da camada no terminal. Utilizamos essa opção pois a imagem foi configurada para executar o nginx automaticamente quando o container for criado. Ou seja, ao invocarmos o comando acima, o daemon preenche o campo [COMMAND], da sintaxe do docker run, com o comando configurado na imagem (nginx -g ‘daemon off). Dessa forma não precisamos interagir com o container como fizemos no caso do ubuntu. Entenderemos melhor esse conceito ao vermos construção de imagens com o comando docker build.
Vamos verificar as opções de redes atribuídas ao container recém criado:
root@docker-host:~# docker inspect --format='{{json .NetworkSettings.Networks}}' 8d0ce387cd95 | python -m json.tool { "bridge": { "Aliases": null, "EndpointID": "0c4bee8d28fc74e1d2634164d4bb850af13ccd10a1b61a49f25e0d7e330ea184", "Gateway": "172.17.0.1", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, "IPAMConfig": null, "IPAddress": "172.17.0.6", "IPPrefixLen": 16, "IPv6Gateway": "", "Links": null, "MacAddress": "02:42:ac:11:00:06", "NetworkID": "16fe8105e615b82814fc2376991366f4fbd346789dac12f3cbf6dbf97d7a2903" } }
Analisando as opções de redes podemos verificar diversas informações, como endereço IP, MAC Address e gateway. Repare que o endereço IP atribuído ao container faz parte da sub-rede escolhida pelo docker. Além disso, o endereço MAC atribuído é escolhido de acordo com o endereço IP.
Agora, liste os containers e repare na coluna PORTS do container do nginx:
root@docker-host:~# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS 8d0ce387cd95 nginx "nginx -g 'daemon off" 14 minutes ago Up 14 minutes 80/tcp, 443/tcp
Essa coluna informa quais portas foram configuradas na imagem para serem exportadas. No entanto, não utilizamos nenhuma opção para mapeá-las na criação do container. Então, vamos criar um novo container com as opções a seguir:
root@docker-host:~# docker run -d -p 80:80 nginx
5a4e8b15ee660f56f4206dc905441c196b97814da3567d6ce4fe55fb133a435a
A opção -p faz o mapeamento da porta do container em uma porta no host. Nesse caso estamos dizendo que toda comunicação com destino ao host na porta 80, será encaminhada para o container nessa mesma porta. Identifique o endereço IP do container e em seguida a regra criada no iptables:
root@docker-host:~# docker inspect --format='{{json .NetworkSettings.IPAddress}}' 5a4e8b15ee66 "172.17.0.7" root@docker-host:~# iptables -t nat -nL Chain DOCKER (2 references) target prot opt source destination DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:80 to:172.17.0.7:80
Dessa forma, poderemos acessar a aplicação (nginx) através da url http://<ip_do_host>.
Conforme abordamos no post de redes, a regra será aplicada a todo pacote com destino ao host na porta 80 (dpt:80), alterando o endereço de destino (DNAT) para o ip do container (to:172.17.0.7:80). Dessa forma a aplicação (nginx) poderá ser acessada pela rede do host.
Outra forma de disponibilizar o acesso à aplicação do container por meio de portas, é exportar automaticamente as portas configuradas na imagem usando a opção -P. Diferente da opção anterior, onde devemos especificar tanto a porta do host quanto a porta do container, o docker selecione portas aleatórias no host para mapear as portas do container configuradas na imagem.
Crie um novo container usando a opção -P, conforme abaixo. Em seguida liste os containers:
root@docker-host:~# docker run -d -P nginx 07bdf12bd588592a0c7a966e9e553e15f5ba6fe5ac262726a958e84ec60dc72c root@docker-host:~# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS 07bdf12bd588 nginx "nginx -g 'daemon off" 2 seconds ago Up 1 seconds 0.0.0.0:32769->80/tcp, 0.0.0.0:32768->443/tcp 5a4e8b15ee66 nginx "nginx -g 'daemon off" 29 minutes ago Up 29 minutes 0.0.0.0:80->80/tcp, 443/tcp ...
Analisando a coluna PORTS, compare as informações dos últimos dois containers criados. Repare que usando a opção -P o docker selecionou as portas 32769 e 32768 do host, para mapear nas porta 80 e 443 do container, respectivamente.
O docker permite, inclusive, que as duas opções sejam usadas simultaneamente, e que a opção -p seja usada mais de uma vez. Vamos criar dois containers para testar esse recurso. No primeiro container faça o mapeamento da porta 443 manualmente (-p), exportando a porta 80 automaticamente (-P). No segundo, faça o mapeamento manual das duas portas conforme mostrado abaixo:
root@docker-host:~# docker run -d -P -p 443:443 nginx c5ca7e2a7c84e235d2c38b06096da25303c57d8cff5892090865e5d89403d308 root@docker-host:~# docker run -d -p 8080:80 -p 8443:443 nginx 9f47b207540978600e76f171b97ff35df1fbd30fe61cf62cfcf6404867986a4d root@docker-host:~# docker ps CONTAINER ID ... PORTS 9f47b2075409 ... 0.0.0.0:8080->80/tcp, 0.0.0.0:8443->443/tcp c5ca7e2a7c84 ... 0.0.0.0:443->443/tcp, 0.0.0.0:32771->80/tcp ...
O mapeamento de porta, da forma como mostramos até agora, disponibiliza a aplicação à todas as redes do host (caso ele esteja conectado a várias redes). Veja novamente a coluna PORTS no exemplo anterior e repare que nenhum IP foi definido (0.0.0.0). Isso pode ser um problema se não desejarmos que a aplicação seja acessada por todas as redes conectadas ao host.
A solução é exportar as portas do container para endereços IPs específicos no host, e isso pode ser feito através da opção -p. A sintaxe é -p <ip_do_host>:<porta_host>:<porta_container>. Por exemplo, para tornar a porta 80 de um container acessível através do endereço 192.168.122.100 do host, na mesma porta, a sintaxe seria -p 192.168.122.100:80:80. Suprimindo a porta do host, o docker escolhe aleatoriamente uma porta alta (assim como a opção -P).
Vamos testar o conceito na prática. Primeiro remova todos os containers criados (docker stop $(docker ps -a -q) e docker rm $(docker ps -a -q)). Em seguida adicione dois IPs virtuais no host e crie os containers conforme mostrado abaixo:
root@docker-host:~# ip addr add 192.168.122.100/24 dev eth0:0 root@docker-host:~# ip addr add 192.168.122.110/24 dev eth0:1 root@docker-host:~# docker run -d -p 192.168.122.100:80:80 --name webapp-100 nginx 0e509459f4cb8216fa5214fc56aadb30243af586d713ab50fe73be1aaf79d74c root@docker-host:~# docker run -d -p 192.168.122.110::80 --name webapp-110 nginx 2520f929bca1f55c038206d2b3604f15470eda1d9e7b38712e4087c159e1ce79
No primeiro container (webapp-100) definimos o IP virtual 192.168.122.100 e a porta 80 do host. Para o segundo container (webapp-110), especificamos o endereço 192.168.122.110 mas deixamos o docker escolher a porta do host para mapear.
Execute novamente o comando docker ps e veja a coluna PORTS:
root@docker-host:~# docker ps CONTAINER ID IMAGE COMMAND ... PORTS NAMES 3dc938b74435 nginx "nginx -g ... 443/tcp, 192.168.122.110:32768->80/tcp webapp-110 2101eafc90a8 nginx "nginx -g ... 192.168.122.100:80->80/tcp, 443/tcp webapp-100
Nesse caso a aplicação do primeiro container poderá ser acessada através da url http://192.168.122.100, enquanto a do segundo container será acessada via http://192.168.122.110:32768. Além disso, limitamos o acesso à apenas uma rede (192.168.122.0/24). Isso significa que caso o host esteja conectado a outras redes, nenhum host das demais redes, que não a mencionada, poderá acessar a aplicação.
A regra criada pelo docker no iptables garante o isolamento:
root@docker-host:~# docker inspect --format='{{json .NetworkSettings.IPAddress}}' webapp-100 "172.17.0.2" root@docker-host:~# docker inspect --format='{{json .NetworkSettings.IPAddress}}' webapp-110 "172.17.0.3" root@docker-host:~# iptables -t nat -nL Chain DOCKER (2 references) target prot opt source destination ... DNAT tcp -- 0.0.0.0/0 192.168.122.100 tcp dpt:80 to:172.17.0.2:80 DNAT tcp -- 0.0.0.0/0 192.168.122.110 tcp dpt:32768 to:172.17.0.3:80
Para finalizar esse post, vamos construir um cenário para efetuar o deploy de aplicações web. Nesse modelo vamos usar uma imagem do apache personalizada, e uma imagem oficial do mysql. Usaremos estas imagens para publicar três aplicações (glpi, wordpress e mediawiki) de forma isolada umas das outras. Antes de começar, remova todos os containers e adicione mais um IP virtual (192.168.122.120).
O primeiro passo é baixar as imagens do apache e do mysql:
root@docker-host:~# docker pull cmotta2016/apache root@docker-host:~# docker pull mysql
Em seguida crie os diretórios que serão mapeados nos containers. Dessa forma as configurações das aplicações serão mantidas, caso os containers sejam excluídos.
root@docker-host:~# mkdir /opt/sistemas root@docker-host:~# mkdir /opt/database
O sub-diretório /opt/sistemas será usado para armazenar os arquivos das aplicações. Dentro desse diretório vamos efetuar o download dos pacotes das aplicações e descompactá-los. O diretório database será usado para mapear o local de armazenamento das bases de dados do mysql (/var/lig/mysql).
root@docker-host:~# cd /opt/sistemas root@docker-host:/opt/sistemas# wget https://github.com/glpi-project/glpi/releases/download/0.90.3/glpi-0.90.3.tar.gz root@docker-host:/opt/sistemas# wget https://br.wordpress.org/wordpress-4.5.2-pt_BR.tar.gz root@docker-host:/opt/sistemas# wget https://releases.wikimedia.org/mediawiki/1.26/mediawiki-1.26.3.tar.gz root@docker-host:/opt/sistemas# tar xzvf glpi-0.90.3.tar.gz root@docker-host:/opt/sistemas# tar xzvf mediawiki-1.26.3.tar.gz root@docker-host:/opt/sistemas# tar xzvf wordpress-4.5.2-pt_BR.tar.gz
O próximo passo é criar o container do banco de dados:
root@docker-host:~# docker run -d -e MYSQL_ROOT_PASSWORD=<password> -m 498MB --name database -v /opt/database/:/var/lib/mysql mysql 0fe994531e38a0425a66db47d2677621937e123e5276692deb3f44029d7d457a
A opção -e do comando docker run é utilizada para definir variáveis de ambiente no container. Essa imagem em particular utiliza uma variável para definir a senha do usuário root do mysql. Além disso, estamos limitando a memória desse container à no máximo 498MB, para testarmos o recurso do kernel.
Criado o container de banco de dados, vamos criar o container para as aplicações. Começando pelo glpi:
root@docker-host:~# docker run -d -p 192.168.122.100:80:80 -p 192.168.122.100:443:443 -v /opt/sistemas/glpi/:/var/www/html --link database --name glpi cmotta2016/apache 97c60e8f48700a6890ef2f919d5ca57ac4c9ea6e304dbb3459f0efdc228a4635
Atribuímos o endereço virtual 192.168.122.100, exportando as portas 80 e 443 do container. Dessa forma a aplicação poderá ser acessada tanto por http quanto por https. Além disso, mapeamos o diretório com os arquivos da aplicação para o diretório base do apache.
A conexão entre os containers do glpi e do banco de dados é feita através da opção –link. Ao usar essa opção, o docker adiciona ao arquivo hosts do container do glpi, uma entrada com o IP e o nome do container do mysql (database). Dessa forma, a aplicação poderá ser configurada para se conectar ao banco através do nome do container.
Os containers das aplicações wordpress e mediawiki serão criados da mesma forma que o glpi. Lembrando apenas de alterar o diretório do volume exportado, o nome do container e o endereço IP.
root@docker-host:~# docker run -d -p 192.168.122.110:80:80 -p 192.168.122.110:443:443 -v /opt/sistemas/wordpress/:/var/www/html --link database --name wordpress cmotta2016/apache root@docker-host:~# docker run -d -p 192.168.122.120:80:80 -p 192.168.122.120:443:443 -v /opt/sistemas/mediawiki/:/var/www/html --link database --name mediawiki cmotta2016/apache
Caso seja necessário criar a base de dados para alguma das aplicações diretamente no mysql, execute os comandos diretamente com o docker exec, mas dessa vez utilize as opções de interatividade (-it):
root@docker-host:~# docker exec -it database mysql -u root -p Enter password: ... mysql> create database <nome>;
Com todos os containers criados, teste o acesso às aplicações. Para obter o status de execução dos containers, execute o comando docker stats. Repare que apenas o container de banco de dados (ee17d7ea6d09) possui limite de memória, conforme especificamos na criação.
root@docker-host:~# docker stats $(docker ps -a -q) CONTAINER CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS 56a55502ca7d 0.02% 32.8 MB / 1.049 GB 3.13% 2.696 kB / 2.351 kB 1.749 MB / 12.29 kB 0 97bcadfea086 0.02% 135.8 MB / 1.049 GB 12.95% 1.946 MB / 2.282 MB 10.85 MB / 2.351 MB 0 b2bead1a102a 0.02% 128.7 MB / 1.049 GB 12.27% 724.7 kB / 1.493 MB 46.33 MB / 24.58 kB 0 ee17d7ea6d09 0.05% 270.4 MB / 522.2 MB 51.79% 1.42 MB / 1.322 MB 37.99 MB / 39.96 MB 0
É possível automatizar a criação de múltiplos containers através de ferramentas como, docker compose, puppet, chef, etc. Veremos esse recurso nos próximos posts. Além disso, existem imagens oficiais de algumas aplicações, como o wordpress, redmine, joomla, etc. Sendo assim, caso você queira testar outras formas de deploy, basta procurar a imagem no HUB (http://hub.docker.com) e seguir as instruções do mantenedor para criar o container.
Por enquanto é só pessoal. Aguardem os próximos posts e não esqueçam de deixar seu comentário. Até a próxima.
root@docker-host:~# docker exec presist touch /data/userlist
Não seria
root@docker-host:~# docker exec persist touch /data/userlist
Tem alguma coisa errada com a sequencia de comandos abaixo.
root@docker-host:~# docker exec presist touch /data/userlist
root@docker-host:~# docker exec temp ls /data/
userlist
Resultado do comandos acima
$ sudo docker exec temp touch /data/userlist
touch: cannot touch ‘/data/userlist’: No such file or directory
Caio, falha minha na hora de especificar o container. No primeiro comando o container é temp e não persist. O erro foi apresentado porque no primeiro comando criamos o arquivo em um container (persist), e no segundo comando estamos listando outro (temp). Já corrigi a falha, obrigado pelo feedback.
Corrigido Caio. Na verdade o container é o temp.
Prezado Carlos Augusto Motta
Seguindo as recomendações do site[1] em relação a senha, eu executei os seguintes comandos:
$ sudo docker run -d -e MYSQL_RANDOM_ROOT_PASSWORD=yes -e MYSQL_ONETIME_PASSWORD=yes –name database -v /opt/database/:/var/lib/mysql mysql
$ sudo docker logs database
Através do comando acima, deveria aparecer a senha de root na linha que tivesse as palavras “GENERATED ROOT PASSWORD”. Infelizmente não apareceu essa linha.
Você por acaso já utilizou esse parâmetro?
1-https://hub.docker.com/r/mysql/mysql-server/
Caio, tente criar o container usando um novo volume no host.
Acontece que o script usado para iniciar a aplicação, define a variável de ambiente MYSQL_ROOT_PASSWORD com o valor da senha gerada randomicamente:
if [ ! -z “$MYSQL_RANDOM_ROOT_PASSWORD” ]; then
MYSQL_ROOT_PASSWORD=”$(pwgen -1 32)”
echo “GENERATED ROOT PASSWORD:$MYSQL_ROOT_PASSWORD”
Porém, o diretório /opt/database já contém uma base de dados do mysql, nesse caso a variável será descartada.
Para usar essa variável, em conjunto com o volume de dados, certifique-se de que o diretório usado para mapeamento no host esteja vazio, ou defina um diretório que não existe e assim o docker vai criar o diretório automaticamente. Por exemplo:
$ sudo docker run -d -e MYSQL_RANDOM_ROOT_PASSWORD=yes -e MYSQL_ONETIME_PASSWORD=yes –name database -v /opt/mysql/:/var/lib/mysql mysql
Espero que tenha ajudado. Abç
Prezado Carlos
Deu certo, obrigado.