Docker Swarm e NFS: viabilizando uma infraestrutura clusterizada.

Sou cofundador e engenheiro de software na Pencil Labs, uma empresa de Brasília fundada em 2016. Esse é o primeiro conteúdo relacionado ao trabalho que temos desenvolvido ao longo desses anos . Esse artigo é um breve relato de como tem sido minha experiência em prover infraestrutura para os projetos da Pencil, utilizando um modo de execução do Docker chamado swarm, integrado à um servidor de arquivos NFS.

PencilLabs

A Pencil atualmente é composta por três frentes de trabalho:

Collectar: Sistema para gestão e divulgação de acervos. A stack do Collectar é composta por:

Empurrando Juntos (EJ): Plataforma de consultas de opinião desenvolvida em software livre com foco em Estado e Organizações. A stack da EJ é formada por:

Prestação de serviço de desenvolvimento e manutenção: Além dos projetos da casa, também prestamos serviço para empresas e pessoas físicas, que desejam desenvolver sites ou aplicativos para seus negócios. Trabalhamos também com migração de infraestrutura para ambientes Docker.

Pensar em uma infraestrutura que suportasse essas três frentes de trabalho, nos levou a escolher o modo swarm para orquestrar nossos containers na nuvem. Antes de apresentar o que foi feito para viabilizar tal infraestrutura devo dizer que consideramos utilizar o Kubernetes, projeto desenvolvido pelo Google e que também atua como um orquestrador de containers. O modo swarm já é nativo da engine do Docker, não havendo necessidade de nenhuma instalação ou configuração para iniciar o processo de clusterização da infraestrutura. Como já usávamos Docker em produção, essa facilidade em dar os primeiros passos com o modo swarm foi o principal fator para a sua adoção.

Docker Swarm

Segundo a Wikipedia, cluster é “um conjunto de computadores conectados que trabalham em conjunto, de modo que em vários aspectos podem ser vistos como um único sistema”. Cada máquina virtual que compõe um cluster é chamada de nó. Swarm, é um modo de execução do Docker para gestão e orquestração de um cluster, em que cada nó atua como um manager ou um worker.

No swarm, nós que atuam como managers possuem as seguintes responsabilidades:

  • Manter o estado do cluster: Os managers mantêm o estado do cluster e de todos os serviços rodando dentro dele. É recomendável ter um número impar de managers. Caso um manager caia, o estado do cluster é mantido pelos restantes, fazendo com que a infraestrutura seja tolerante a falhas.

Os nós que atuam como workers possuem uma única atribuição: executar as tarefas agendadas pelos managers. Um container que existe em um nó, é o resultado de uma tarefa disponibilizada por um manager e executada por um worker.

exemplo de criação de um serviço

Do ponto de vista do Docker cada “pedaço” da aplicação é considerado um serviço que será executado dentro do swarm. Por exemplo, uma aplicação composta por um servidor web e um banco de dados, irá possuir dois serviços, um para cada parte da aplicação. Vale ressaltar que um serviço não é o mesmo que um container. A documentação do Docker explica que serviços são “containers em produção”, mas como ambos os termos serão utilizados ao longo do texto, podemos considerar que um serviço define como os containers serão executados. A partir de qual imagem o container será criado, as portas que serão exportadas, o número de replicas que esse serviço possuirá, entre várias outras configurações que são definidas a nível de serviço mas que refletem diretamente nos containers. Todas essas configurações podem ser definidas por meio do arquivo docker-compose.yml.

version: "3"
services:
web:
# replace username/repo:tag with your name and image details
image: username/repo:tag
deploy:
replicas: 5
restart_policy:
condition: on-failure
resources:
limits:
cpus: "0.1"
memory: 50M
ports:
- "80:80"
networks:
- webnet
visualizer:
image: dockersamples/visualizer:stable
ports:
- "8080:8080"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
deploy:
placement:
constraints: [node.role == manager]
networks:
- webnet
networks:
webnet:

Docker stack

Para fazer o deploy dos serviços descritos no exemplo anterior, bastaria (fazendo as devidas adaptações ao exemplo) executar o comando docker-compose up . O problema dessa abordagem é que ela não escala ao longo dos nós da infraestrutura, ou seja, os serviços que são criados a partir do docker-compose up não são compatíveis com o modo swarm. Para realizar o deploy dos serviços no swarm é necessário utilizar o comando docker stack deploy. Uma stack descreve todos os serviços que compõe uma aplicação. Isso permite realizar o deploy destes serviços ao longo dos nós do swarm. Caso a aplicação precise de mais recurso, basta adicionar mais um nó ao swarm e aumentar o número de replicas. A figura a seguir demonstra como um mesmo serviço pode ser escalado ao longo de diferentes nós utilizando o conceito de replicas. Nesse caso my-web é o nome do serviço. Esse serviço possui duas replicas (containers), my-web.1 e my-web.2 . Cada replica é executada em um nó diferente e cada replica possui um ip único dentro do swarm.

exemplo de replicas de um serviço

Proxy Reverso

O fato de uma aplicação poder ser executada em qualquer nó do swarm e ainda assim ser acessada pelo cliente de modo transparente, impacta como o servidor http interage com os serviços. O nginx, quando configurado como um proxy reverso, irá redirecionar requisições externas ao servidor para as aplicações que são executadas dentro do servidor. Isso irá acontecer independente de se utilizar o modo swarm ou não, o que muda é de que forma esse redirecionamento interno irá acontecer.

Uma aplicação que é executada fora do swarm, e utiliza nginx como servidor http, irá ser servida conforme o esquemático a seguir. As requisições irão chegar na porta 80 ou 443, e a partir do domínio da requisição o nginx irá redirecionar para a porta e ip correspondentes ao domínio requisitado. Esse é o modus operandi de uma aplicação que não é servida de maneira clusterizada.

nginx atuando como proxy reverso fora do swarm

Por outro lado, uma aplicação que é executada no modo swarm, e utiliza nginx como servidor http, irá ser servida conforme o próximo esquemático. As requisições irão chegar na porta 80 ou 443, e a partir do domínio da requisição o nginx irá redirecionar para o serviço correspondente ao domínio requisitado. Note que, assim como as aplicações, o nginx também é executado como um container dentro do cluster. O nginx utiliza o nome do serviço para realizar o proxy reverso. O servidor DNS que existe dentro do swarm resolve o nome do serviço definido na diretiva proxy_pass para o ip de algum container que pertença ao serviço.

nginx atuando como proxy reverso dentro do swarm

Se tivermos, por exemplo, cinquenta replicas do serviço do EJ sendo executadas no swarm, nada muda para o nginx. É papel do swarm realizar tanto o load balancing quanto a resolução do nome do serviço para o ip do container.

Arquivos Estáticos e NFS

Devido a natureza clusterizada do modo swarm, os containers dos serviços podem rodar em qualquer nó do cluster. Isso é muito importante pois nos permite escalar horizontalmente (adicionando mais nós ao swarm). Como os containers podem rodar em qualquer nó, os arquivos estáticos (html,css,js) das aplicações vão existir apenas no nó em que o container estiver rodando, e isso é um problema para o nginx. Para ser capaz de servir os arquivos estáticos das aplicações o nginx precisa ter acesso de leitura aos arquivos, mas se o nginx estiver rodando no nó A e a aplicação estiver rodando no nó B, esse acesso não será possível. Para contornar esse problema nossa infraestrutura conta com um servidor NFS, que permite que o nginx acesse os estáticos de uma aplicação mesmo que ambos estejam rodando em nós diferentes. Isso é possível pois o proposito de um servidor NFS é permitir que o cliente acesse via rede, diretórios e arquivos existentes no servidor, como se esses arquivos existissem também no cliente.

Analisando brevemente um dos nós da infraestrutura, temos a seguinte organização dos discos. Em vermelho temos o disco da máquina virtual. Ele possui 241 gigas. Em verde temos diretórios montados via NFS. A configuração do servidor ocorre da seguinte forma: No servidor você define as regras de quem irá poder montar as partições via rede. Essas partições são os diretórios que existem na máquina virtual em que o servidor NFS está sendo executado. No cliente, via comando mount, você monta um diretório existente no servidor.

lista de partições do cliente NFS

Os arquivos existentes no servidor estarão disponíveis no cliente, e este poderá alterar, modificar e até criar novos arquivos, que automaticamente serão sincronizados no servidor. Note que, os diretórios são montados como uma partição da máquina. A partição vai ter o tamanho da máquina virtual em que o servidor NFS está sendo executado. De maneira prática os arquivos existem apenas no servidor, se um cliente desmonta uma das partições os arquivos deixam de existir apenas no cliente.

Por padrão o Docker sempre monta os volumes na máquina local. Para que o Docker monte os volumes no servidor NFS temos que, via arquivo de configuração, informar algumas opções como o ip do servidor, o modo de acesso (escrita e leitura) e qual o device (diretório) será montado. Note que o device deve existir no servidor NFS antes de tentarmos fazer o deploy da stack. Isto é uma limitação do drive do Docker, pois, caso o diretório informado não exista, o serviço não vai inicializar corretamente.

volumes:
backups:
driver: “local”
driver_opts:
type: nfs
device: :/var/nfs-statics/dev/ej
o: addr=192.168.0.231,rw

O esquemático a seguir demonstra de que forma ocorre a relação entre uma instância do nginx, uma instância de uma aplicação com arquivos estáticos e o NFS. Inicialmente o nginx irá montar no servidor NFS o diretório /var/nfs-statics/dev/, que irá conter subdiretórios que serão montados pelas aplicações. Quando for feito o deploy do EJ, o volume ej_dev_statics será montado no NFS, sincronizando o diretório /ejserver/local com o diretório /var/nfs-statics/dev/ej/ . Nesse momento vale ressaltar uma particularidade do NFS: Um cliente pode acessar todos os subdiretórios do diretório montado, mas os diretórios acima da raiz não são acessíveis. O volume do EJ não tem acesso ao diretório /var/nfs-statics/dev/, já que esse diretório está acima do diretório montado pelo Docker. Por outro lado, qualquer diretório criado dentro de /var/nfs-statics/dev/, será acessível pelo nginx.

relação entre nginx, ej e o servidor NFS

Logo que os arquivos estáticos forem gerados no container do EJ, o Docker irá monta-los no NFS dentro de /var/nfs-statics/dev/ej/ (até então esse diretório estava vazio). Ao montar os arquivos estáticos no NFS, automaticamente eles também existirão dentro do container do nginx, já que o diretório /var/nfs-statics/dev/ej é um subdiretório de /var/nfs-statics/dev/. Resumindo, qualquer arquivo ou diretório criado dentro de /ejserver/local também existirá dentro de /etc/nginx/sites/ej/. Ao final do deploy da stack do EJ, teríamos os seguintes diretórios dentro do container do nginx e do servidor do EJ:

# dentro do container do EJ
$ ls /ejserver/local/
html css js
# dentro do diretório do ej no NFS
$ ls /var/nfs-statics/dev/ej/
html css js
# dentro do container do nginx
$ ls /etc/nginx/sites/
ej
$ ls /etc/nginx/sites/ej/
html css js

A partir do momento que o nginx consegue acessar os arquivos estáticos das aplicações por meio de um volume que é montado no servidor NFS, nosso problema está resolvido. Agora que sabemos como um servidor NFS funciona e de que forma podemos montar os volumes via rede, o esquemático final da nossa infraestrutura é o seguinte:

esquemático final da infraestrutura

As requisições irão chegar ao swarm nas portas 80 e 443 (produção) e 8080 (homologação). Uma instância do nginx irá responder à essas requisições, identificando o domínio e, em seguida, redirecionando para o devido serviço. Cada aplicação com arquivos estáticos irá montá-los no NFS, em subdiretórios dos diretórios montados pelo nginx. Dessa forma as aplicações são executadas em qualquer um dos nós, e ainda assim o proxy reverso será feito e os arquivos estáticos serão servidos.

Conclusão

Primeiramente gostaria de agradecer por ter nos acompanhado até aqui, essa foi nossa primeira publicação técnica então obrigado por ter dedicado parte do seu tempo para essa leitura. A arquitetura que apresentamos nos permitiu colocar em produção, para cada um dos nosso projetos, dois ambientes totalmente isolados. Tanto EJ quanto Collectar possuem ambientes de produção e homologação. Além de atender EJ e Collectar, nossa infraestrutura também possibilita criar ambientes de produção e homologação para os projetos que somos contratados para desenvolver.

Apesar de resolver alguns problemas, esse arranjo da infraestrutura trouxe outras questões como:

Qual o impacto do modo swarm integrado à um servidor NFS na performance das aplicações?

Uma análise de performance nessa arquitetura é necessária, uma vez que não sabemos se essa abordagem aumentou o tempo de resposta das aplicações. Tal análise ficará para uma próxima conversa.

Se interessou pelo nosso trabalho e tem desejo de desenvolver produtos digitais como aplicativos e sites? Entre em contato pelo nosso site, ou nos envie um email em contato@pencillabs.com.br. Somos Engenheiros e Designers que tiram seu projeto digital do papel.

https://davidcarlos.me/