Redes no Docker

Olá, e obrigado por sua visita! Após um longo e tenebroso “inverno” de praticamente seis meses desde o terceiro capítulo da nossa série, lanço mais um agora..Avancemos com o estudo, entendendo como funcionam os conceitos de redes no Docker.

Nos capítulos anteriores, você já viu que o Docker tanto é o nome de uma implementação muito popular de containers, como também da empresa que o mantém.Você já sabe quais são os principais componentes do Docker, e como o gerenciamento de armazenamento se dá na plataforma. Agora, vamos falar sobre comunicação de redes. Este capítulo também dará início ao contato com comandos.

Se quiser conferir os outros capítulos:

*** Como este post é um pouco extenso, você pode ter uma melhor experiência de leitura a partir do computador. ***

 

O Básico

Não pretendo explicar a você o básico de redes, pois tanto há excelentes livros, como bons blogs na Internet explicando conceitos, fornecendo exemplos e até realizando exercícios. Com algumas buscas rápidas, encontra-se bastante material para se divertir.

No entanto, é importante esclarecer alguns pontos cruciais antes de falar especificamente de como o Docker manipula a rede entre os containers. Conforme na vimos num capítulo anterior, o que você vir aqui servirá para qualquer sistema operacional onde o Docker estiver em operação, uma vez que já existem versões dele para os mais populares, como Linux, Windows e macOS. Há, no entanto, um pequeno detalhe, a ser mostrado logo a seguir.

No ambiente do Docker, a rede é nada mais do que uma abstração, ou seja, uma forma de suprimir todos os detalhes de protocolos, drivers e serviços de baixo nível, de maneira a deixar mais fácil a implementação da comunicação. Por padrão, assim que você instala o produto, ele cria automaticamente três redes com características distintas:

  • Uma rede do tipo bridge, com o mesmo nome;
  • Uma rede do tipo host, como o mesmo nome;
  • Uma rede sem nome e sem tipo específico.

Cada tipo de rede possui um driver correspondente. As redes bridge e host acima possuem drivers com os mesmos nomes, para facilitar a compreensão e uso. Entretanto, você pode criar redes com quaisquer nomes e usar os mesmos drivers. Vou explicar melhor mais abaixo.

Agora, vamos entender o que seria uma rede do tipo bridge e uma do tipo host. Para isso, vamos recorrer ao Virtualbox. Após já ter criado pelo menos uma VM no Virtualbox, você pode abrir as configurações da mesma clicando no botão correspondente na barra de ferramentas ou abrindo o menu de contexto ao clicar com o botão direito do mouse sobre ela. Navegue até a aba de redes e abra a combobox que diz “Attached to:” ou “Conectado a:”, conforme a figura 1 abaixo.

Figura 1: tipos de rede no Virtualbox

Ao escolher “NAT” (o padrão que o produto escolhe ao se criar uma VM), o serviço de DHCP fornecido pelo Virtualbox entregará um endereço para a VM. Então, para que seja possível acessar algo fora da VM, o produto realizar um NAT, ou melhor, um PAT (Port Address Translation) entre o endereço do host e o da VM.

Se você escolher “Bridge“, a VM terá acesso direto à rede física do host, inclusive obtendo um endereço IP do servidor DHCP físico, se existir. Isso permite um contato direto da VM com a rede real.

Por fim, o modo “Host-only” isola a VM dentro do próprio host onde ela está sendo executada. Isto serve para garantir que nenhum tráfego poderá chegar ou vir da rede física. Muito útil quando se precisa realizar testes preliminares de alguma solução, e não se deseja misturá-la com algo já em produção.

Ao ser instalado em um host Linux, o Docker Server cria um adaptador de rede lógico chamado “docker0”. Veja o comando “ifconfig -a” executado em um host Linux:

$ ifconfig -a
docker0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet 172.17.0.1  netmask 255.255.0.0  broadcast 172.17.255.255
        ether 02:42:3c:77:e1:57  txqueuelen 0  (Ethernet)
        RX packets 0  bytes 0 (0.0 B)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 0  bytes 0 (0.0 B)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
...
saída suprimida

No caso de estar usando no Windows ou no macOS, e é justamente o detalhe que citei um pouco mais acima, esta interface lógica não se encontrará diretamente no sistema operacional, mas na VM que é iniciada pelo Docker Server para dar suporte aos containers. Você não tem acesso direto a esta VM, sendo toda a comunicação com ela intermediada pelo Docker Server.

Veja abaixo a saída do comando “ps aux” executado a partir de um host com macOS. Observe a existência de um segmento de rede (192.168.65.0). Você pode querer clicar sobre a linha abaixo e escolher o botão “Toggle RAW Code” para uma melhor experiência de visualização:

mauricio           965   0.0  0.1  4399224  19404   ??  S    10:32PM   0:00.32 com.docker.vpnkit --ethernet fd:3 --port vpnkit.port.sock --port hyperkit://:62373/./vms/0 --diagnostics fd:4 --pcap fd:5 --vsock-path vms/0/connect --host-names host.docker.internal,docker.for.mac.host.internal,docker.for.mac.localhost --gateway-names gateway.docker.internal,docker.for.mac.gateway.internal,docker.for.mac.http.internal --vm-names docker-for-desktop --listen-backlog 32 --mtu 1500 --allowed-bind-addresses 0.0.0.0 --http /Users/mauricio/Library/Group Containers/group.com.docker/http_proxy.json --dhcp /Users/mauricio/Library/Group Containers/group.com.docker/dhcp.json --port-max-idle-time 300 --max-connections 2000 --gateway-ip 192.168.65.1 --host-ip 192.168.65.2 --lowest-ip 192.168.65.3 --highest-ip 192.168.65.254 --log-destination asl --udpv4-forwards 123:127.0.0.1:59451 --gc-compact-interval 1800

Este segmento de rede é usado pelo Docker Server para fornecer endereços IP via DHCP, aos containers. O endereço IP do default gateway é sempre o primeiro endereço do bloco sendo entregue. No exemplo acima, é o 192.168.65.1. Observe que o host também assume um endereço IP na mesma rede (192.168.65.2), ficando o primeiro endereço disponível como sendo o 192.168.65.3. Este segmento é usado em redes host, que serão vistas mais abaixo.

Daqui para a frente, com o intuito de facilitar o entendimento completo do tema, darei preferência a mostrar as saídas dos comandos e explicar os conceitos, considerando que o Docker Server está executando num host Linux. Não se preocupe quanto a alguma falta de funcionalidade nos outros sistemas operacionais, pois ela não existe. Tudo o que será coberto aqui também se aplica à VM do Docker que executa no Windows e macOS.

 

Rede Bridge

No Docker, o funcionamento da rede bridge é similar ao modo “NAT” explicado acima no Virtualbox, só que através do adaptador docker0. Se uma rede não for especificada ao iniciar um container, o tipo/driver “bridge” será usado por padrão. Nela, todos os containers obtêm endereços IP de forma dinâmica, através de DHCP.

O objetivo dela é funcionar como um mini-switch lógico que permite a comunicação entre containers. Imagine que ela possui diversas portas/interfaces e que cada container é logicamente ligado a uma dessas interfaces. Se você conhece um pouco de virtualização, pode fazer um paralelo com os switches virtuais que produtos como o ESXi e o Hyper-V possuem. O intuito é exatamente o mesmo.

Esta rede permite tanto a comunicação entre containers como entre os containers e a Internet, através de um mecanismo de NAT implementado pelo host onde o Docker Server está executando. A figura abaixo mostra um diagrama de operação da rede bridge.

Figura 2: Docker Bridge – crédito: Docker.com

Na figura acima, o elemento “mybridge” corresponde a uma rede do tipo bridge criada pelo administrador. Observe que os containers “db” e “web” pertencem ao mesmo segmento de rede, e que o container “web” possui está expondo a porta 5000. Observe, ainda, a presença da porta 8000 conectada à interface física (eth0) do host. Isto correspondente a um mapeamento de portas. Ou seja, qualquer conexão externa tendo como destino a interface 192.168.1.2 do host e como porta a 8000 será redirecionada para o container web (172.19.0.3) na porta 5000.

O círculo com setas representa a interface lógica docker0 explicada anteriormente.

Um ponto importante é resolução de nomes (DNS). Quando containers são iniciados e conectados a uma rede do tipo bridge (com exceção da default), eles consegue se comunicar usando os nomes definidos durante sua criação. Ou seja, você cria uma rede do tipo bridge, inicia um container fazendo referência a essa rede e dá um nome a ele. Em seguida, inicia outro container conectado à mesma rede e dá outro nome. Feito. Os containers podem trocar pacotes entre si usando seus respectivos nomes. Isso facilita imensamente na hora de escrever o código de uma aplicação em três camadas, por exemplo. De fato, os containers herdam a configuração de DNS presente no host que executa o Docker Server, incluindo o /etc/hosts, no caso do Linux.

Numa rede do tipo bridge criada pelo usuário, também é possível conectar e desconectar containers dinamicamente a ela, ou seja, enquanto em funcionamento. Cada rede bridge cria seu próprio segmento de rede. Se dois containers pertencem a redes bridge diferentes, eles não se comunicam de forma automática.

 

Rede Host

A rede host tem o funcionamento semelhante à rede “bridge” do Virtualbox. Cuidado para não confundir as coisas! 🙂 Quando uma rede usa o driver “host”, significa que o container que for iniciado nela terá acesso direto à rede física, sem qualquer tipo de isolamento.

Como não existe o intermédio do docker0, o container usa o próprio endereço do host para se comunicar com a rede externa. Inclusive, se você executar o comando “ifconfig -a” em um container vinculado a uma rede com este driver, verá que tem o mesmo efeito de executá-lo no próprio host.

Portanto, atenção com portas em que a aplicação precisará executar. Como um container em uma rede do tipo host usará o mesmo endereço IP do próprio host, é preciso ter controle estrito sobre que portas serão acessíveis no container. De qualquer forma, em ambientes de produção, com muitos hosts e clusters swarm ou Kubernetes (veremos isso num próximo post), os servidores físicos usualmente não executam nada mais além do Docker Server e de algumas serviços de gerenciamento, liberando a maior quantidade de portas para as aplicações em execução nos containers.

 

Rede Overlay

Este driver é usado exclusivamente para comunicação de clusters de containers, como o Swarm, primeira solução de cluster disponivel para o Docker. Ele permite a conexão de vários Docker Servers em vários hosts diferentes. Tratarei do Swarm num próximo post, mas, em linhas gerais, a ideia é permitir a criação de uma entidade (cluster) para garantir a alta disponibilidade da aplicação. Com ele, é possível rodar containers semelhantes em hosts distintos, de forma que tanto a carga entre eles fica balanceada, quanto a eventual falha de um host não causa parada na aplicação.

O termo overlay, em tradução livre, significa sobreposição. Ou seja, este tipo de rede se sobrepõe sobre a rede física que conecta os hosts com Docker Server. É criada uma rede lógica por cima da rede física, de forma que a física nem tenha noção da existência da lógica.

Mesmo tendo sido projetada para uso no Swarm, também é possível usar a rede overlay para comunicar containers isolados situados em hosts distintos, ou mesmo um cluster Swarm com um container isolado num host que não faz parte desse cluster. A documentação oficial do Docker nomeia esse tipo de container isolado como.”standalone”.

Uma importante característica da rede overlay é a criptografia, de forma que a comunicação entre containers é protegida através de mecanismo de criptografia fornecido pela API do Docker. A figura abaixo mostra um exemplo de rede overlay.

Figura 3: Docker Overlay – crédito: Docker.com

 

Rede MACVLAN

É uma novidade trazida em versões mais recentes da Docker Engine. Ela permite a atribuição de um endereço MAC diretamente a um container, o que facilita o trabalho com aplicações legadas, que possam precisar ter acesso diretamente à rede local sem intervenção do Docker Server, ou qualquer tipo de mapeamento de portas ou mesmo pontes (bridges) lógicas do sistema operacional. Aplicações que fazem monitoramento de rede e que rodem em containers também se beneficiam deste tipo de rede, por lhe dar mais visibilidade do tráfego da rede física.

Toda rede MACVLAN precisa de uma interface “pai”, que pode ser a interface de rede física do host. Como o acesso é direto à rede física, um default gateway nesta rede também é necessário para que o container possa ter acesso ao mundo exterior.

Um caso de uso muito interessante para esse tipo de rede é a possibilidade de separação do tráfego em VLANs, como mostra a figura abaixo. Nela, você verá dois containers, cada um associado a uma sub-interface da interface física eth0 pertencente ao host. Cada sub-interface faz parte de uma VLAN. E a interface eth0 está conectada a um switch externo através de um trunk 802.1q, padrão criado pelo IEEE que permite o transporte de diversas VLANs através de um único cabo de rede ou fibra óptica.

Na situação apresentada pela figura para que o container na VLAN 10 possa se comunicar com o container da VLAN 20, é necessário que o tráfego saia do host, chegue até o gateway da VLAN 10, seja roteado e volte para o host, para ser entregue ao container na VLAN 20.

Daí, você pode perguntar: e qual a diferença para a rede Host. Simples: a rede host é normalmente usada apenas por containers isolados (standalone) para aplicações específicas que precisam ter conexão direta com a rede física.

Figura 4: Docker MACVLAN – crédito: Docker.com

 

Rede None

O único intuito é deixar o container completamente isolado, sem contato com qualquer tipo de rede. É normalmente usada quando o container precisa apenas manipular arquivos.

 

Alguns Comandos

Como você já sabe, o Docker vem com três redes por padrão. Para verificar a configuração e status da rede com nome “bridge”, basta rodar o comando abaixo:

$ docker network inspect bridge
[
    {
        "Name": "bridge",
        "Id": "d7bcfabd12664473b1d4c0186dd2bfe545c0920a280542bdac93b5bd7b39c418",
        "Created": "2018-12-02T12:12:26.654905552Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": null,
            "Config": [
                {
                    "Subnet": "172.17.0.0/16",
                    "Gateway": "172.17.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": 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": {}
    }
]

Observe acima o bloco que contém “Subnet” e “Gateway”. A subnet corresponde à rede com endereços IP a serem entregues aos containers. O gateway é o próprio host executando Docker Server. Muitos comandos no Docker possuem saídas na forma de um arquivo JSON. Você verá isto ao longo da nossa série, à medida que formos apresentando mais comandos.

Para mostrar a alocação automática de endereços IP, primeiro vamos criar uma rede do tipo bridge. Isto permitirá a resolução automática de nomes entre containers. Após criá-la, vou ver qual a lista de redes e os detalhes da nova rede criada. Observe o bloco de rede e o endereço do default gateway

$ docker network create minharede

$ docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
776921f5c937        bridge              bridge              local
cbd0c4ab9fee        host                host                local
39280012a4f6        minharede           bridge              local
3112416be1b4        none                null                local

$ docker network inspect minharede
[
    {
        "Name": "minharede",
        "Id": "39280012a4f6db4ad5edabf177e9951cf5603efebc26783bec54a6e3b2d16d6f",
        "Created": "2018-12-04T20:51:04.342211503Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        }
     }
...
saida suprimida
]

Na sequência, vamos iniciar dois containers com a distribuição Alpine usando esta rede:

$ docker run -d --network minharede --name c1 alpine /bin/ash -c "while true; do ls; done"
$ docker run -d --network minharede --name c2 alpine /bin/ash -c "while true; do ls; done"

Acima, iniciei dois containers (opção “run”), atribuí os nomes “c1” e “c2” (opção “name”), coloquei-os para rodar em segundo plano (opção “-d”) e conectados à rede “minharede”. Especifiquei a distribuição Alpine, e pus para rodar o Ash (shell do Alpine) passando como parâmetro um script simples que executa “ls” indefinidamente. Isso garantirá que os containers estarão ativos até que sejam paralisados.

Vamos agora inspecionar novamente a rede “minharede”.  Observe abaixo que os containers obtiveram seus endereços IP por meio do serviço DHCP do Docker Server. Cada container possui um ID, independentemente se você especificou ou não um nome para ele.

$ docker network inspect minharede
[
    {
        "Name": "minharede",
        "Id": "39280012a4f6db4ad5edabf177e9951cf5603efebc26783bec54a6e3b2d16d6f",
        "Created": "2018-12-04T20:51:04.342211503Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.18.0.0/16",
                    "Gateway": "172.18.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "c8600eba5c25852fd4044b3758aad404b46491ce1aca458c4d07e2de08f13664": {
                "Name": "c1",
                "EndpointID": "2b02822617e50be626ec8350db850eaf113352b5f09e894d2328289e1280fc9c",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/16",
                "IPv6Address": ""
            },
            "cbc45ba67436ce4c467dfbe1c9643a007cf483a789aff6ae5731ff3fd49955c5": {
                "Name": "c2",
                "EndpointID": "c2ebedced4b08300bb0b0cfab5cb79d1190354cfeb81c094dfeb073d42e0738b",
                "MacAddress": "02:42:ac:12:00:03",
                "IPv4Address": "172.18.0.3/16",
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

Por fim, vamos tentar uma conectividade simples entre eles para ver a resolução de nomes funcionando:

$ docker exec c1 ping c2
PING c2 (172.18.0.3): 56 data bytes
64 bytes from 172.18.0.3: seq=0 ttl=255 time=0.081 ms
64 bytes from 172.18.0.3: seq=1 ttl=255 time=0.076 ms
64 bytes from 172.18.0.3: seq=2 ttl=255 time=0.080 ms
64 bytes from 172.18.0.3: seq=3 ttl=255 time=0.079 ms
64 bytes from 172.18.0.3: seq=4 ttl=255 time=0.078 ms

$ docker exec c2 ping c1
PING c1 (172.18.0.2): 56 data bytes
64 bytes from 172.18.0.2: seq=0 ttl=255 time=0.080 ms
64 bytes from 172.18.0.2: seq=1 ttl=255 time=0.088 ms
64 bytes from 172.18.0.2: seq=2 ttl=255 time=0.081 ms
64 bytes from 172.18.0.2: seq=3 ttl=255 time=0.076 ms
64 bytes from 172.18.0.2: seq=4 ttl=255 time=0.098 ms

 

E por enquanto, é isso.

Assim, finalizamos nosso capítulo nossa rede. No próximo, vamos cobrir clusters em Docker, especialmente o Swarm, que foi citado aqui.

Encontro você lá! 🙂

Se quiser conferir os outros capítulos:

Compartilhe:

Maurício Harley

Olá! Meu nome é Maurício Harley. Tenho mais de 20 anos de experiência em Tecnologia da Informação. Durante minha carreira, trabalhei em setores diversos, como suporte a usuário final, manutenção de hardware, instalação e suporte a redes de computadores, programação, projetos avançados em Data Center, Cloud Computing, Cyber Security e Redes, incluindo Service Providers.

4 comentários

Sagara · 2018-12-05 às 08:55

Sensacional Harley!!!

Comecei a estudar docker por conta dos seus posts, e as portas que ele abre (e que abriu nas minhas ideias) são sensacionais!!

Meus parabéns!!

    Maurício Harley · 2018-12-05 às 16:03

    Olá, Thiago!

    Fico muito feliz que esteja gostando da série. Isto só me incentiva a continuar escrevendo e melhorando. Fique atento, que aparecerão mais novidades.

    Obrigado e abraço!

José Carlos · 2019-05-01 às 17:53

TOP mestre, muito bom. Depois que eu vi um lab todo em Docker, simulando switches, routers e serviços, fiquei muito empolgado. Agora deixa eu voltar aos meus labs, já que a parte de docker, que não era coberta pelo LAB, você já explicou de forma clara e objetiva. Abs e mais uma vez obrigado.

    Maurício Harley · 2019-05-01 às 18:18

    Ótimo!

    Estou escrevendo o capítulo 5 e devo publicar em breve. Fique ligado!

Deixe um comentário

Avatar placeholder

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Translate

You cannot copy content of this page.