Boas Práticas com Docker

No post sobre DevOps foram citadas inúmeras ferramentas que facilitam e permitem cultivar uma cultura DevOps. Minha escolha para iniciar esta jornada foi o Docker. Em vez de convencer que o Docker é bom ou explicar como instalar e subir o primeiro container, vou focar em questões da nossa experiência com o Docker. Questões simples mas importantes acabam passando desapercebidas e geram problemas em produção.

Se você está iniciando em Docker, recomendo começar pela documentação oficial, que melhorou bastante nos últimos tempos. Se você ainda está em dúvida para onde as coisas caminham, dê uma olhada neste gráfico. Containers já são uma realidade.

Entendendo Problemas de Consistência

Com todo o hype em torno de containers, é natural que existam sentimentos do tipo “precisamos usar Docker porque todo mundo está usando” ou “vamos usar Docker porque é legal”. Docker, como qualquer ferramenta, tem seus principais valores. E no caso, posso resumir o valor do Docker em uma palavra: consistência. É importante entender o que isso significa para implantar a ferramenta de forma adequada em seu ciclo produtivo.

Pré-Puppet (ou Chef, Ansible…)

Vamos supor que você precisa subir uma aplicação nova. Máquina provisionada, SO instalado, você loga na máquina e:

  • Atualiza o sistema operacional.
  • Instala os pacotes da sua aplicação (apt, yum, zip (!!) etc).
  • Edita os arquivos de configuração.
  • Coloca o serviço no boot.
  • Reboot.

Este fluxo representa a maioria dos casos, e é um bom exemplo.

Agora solicitaram subir mais máquinas idênticas… será tedioso.

Mecanizando o Processo

De posse de ferramentas de gestão de configuração, você escreve o código que mecaniza todo o processo. Maravilha! Máquinas novas provisionadas para produção em segundos!

Porém, saem novas versões da aplicação o tempo todo, problemas aparecem em produção e a únia solução é “formatar” a máquina…

Gerenciando Alterações

Vamos entender onde o problema ocorre. Você recebe a máquina M1, apenas com o SO (BareMetal):

M1 > BareMetal

Então, aplica a sua automação, v1, em cima:

M1 > BareMetal > v1

E leva a máquina para produção, na versão v1. Mas claro, vem a v2 da automação, e você aplica:

M1 > BareMetal > v1 > v2

Agora você adiciona uma nova máquina M2, para suportar a v3:

M1 > BareMetal > v1 > v2 > v3

M2 > BareMetal > v3

E de repente, a M1 funciona, mas a M2 não! Claramente, M1 foi submetida a um fluxo (v1 > v2 > v3) totalmente diferente da M2 (v3), e fatalmente o estado real e final da M2 não é o mesmo da M1, mesmo que ambas foram submetidas à mesma receita v3. Uma causa comum por exemplo, é que a v2 instala uma dependência, que a v3 não pede.

Para resolver o problema, só começando do zero.

Entregando Com Consistência

Achado o bug, criamos uma v4, e entregamos em uma máquina M3:

M3 > v4

Isso funciona porque o fluxo é mais consistente. Pra fechar a questão, matamos as máquinas M1 e M2, e provisionamos as máquinas M4 e M5, no mesmo fluxo da M3:

M4 > v4

M5 > v4

Maravilha! Um mês depois, você provisiona a M6, para suportar a carga crescente:

M6 > v4

E novamente, problemas! Como pode? M4 e M5 foram submetidas ao mesmo fluxo do M6! Fatalmente é algum fator externo, como por exemplo updates do SO que estão na receita. Alguma versão mais nova de dependência que está presente somente na M6 está gerando problemas.

“Que maravilha!”, você pensa. Agora vamos à estaca zero para versionar TODOS os pacotes do sistema operacional, repositórios, etc…. não vai ter fim.

Containers Para a Salvação!

Bem, é exatamente todo esse stress que containers evitam. Fazendo uma analogia, containers são como um snapshot de uma VM em um estado confiável. Este snapshot pode ser utilizado para instanciar quantas VMs forem preciso, de forma confiável. Na terminologia do Docker, os snapshots se chamam imagens e as VMs containers:

Dockerfile > Image > Containers

Uma vez com a imagem pronta, você pode instanciar quantos containers forem precisos, onde for preciso, rapidamente e com confiança!

Além do Ambiente de Produção

 

Um container é algo tão leve e rápido, que consegue ser utilizado na máquina do desenvolvedor e também no servidor em produção, com fidelidade altíssima entre os ambientes. Além disso, o Docker Registry resolve o problema de se compartilhar imagens. Imaginem que eu como desenvolvedor preciso fazer o QA do meu sistema, que tem dependência de um sistema terceiro. Eu posso simplesmente utilizar a imagem do container de produção deste sistema! Nada mais de “o QA está fora”, ou “o orçamento para máquinas de QA está alto”.

 

Docker traz assertividade, confiabilidade e redução de custos, levando para o início do ciclo de desenvolvimento a mesma tecnologia e performance que existem no ambiente de produção.

Pontos de Atenção Com Docker

Criar um Dockerfile e subir o primeiro container é quase trivial. Entretanto,alguns pontos importantes, triviais de se implementar, muitas vezes são esquecidos. Em especial, no Docker Hub, há imagens prontas para praticamente qualquer coisa. Entretanto, a maturidade e qualidade delas varia grotescamente. Olhe as imagens e valide se os Dockerfiles seguem bons princípios. Somente então as utilize.

 

Alguns dos pontos levantados aqui são parte do http://12factor.net/, vale a leitura complementar.

Não Executar Como root

Um dos anti-patterns mais comuns com o Docker é executar os processos do container como root. Há uma certa argumentação válida de que a kernel já garante isolamento entre containers, mesmo eles sendo executados como root. Vou dar crédito para isso. Porém, há uma série de abusos que podem ser feitos dentro do container, como sobrescrever binários, arquivos de configuração etc… Pense assim: para quê rodar como root? O único caso que consigo pensar é executar um container privilegiado, que por si só é algo tão fora do padrão, que pode ser até considerado um anti-pattern.

Logs Fora do Container

Apesar de não haver nada proibindo você de gravar logs dentro do container, isso fatalmente irá te causar problemas. Na próxima atualização, quando for entregue um novo container, TODO o conteúdo dos logs antigos será perdido. As opções são:

  • Mapear o diretório de logs para um volume que será persistente, e anexado a novas versões do container.
  • Enviar os logs para outra ferramenta externa (ex: Elastic Search, Splunk).
  • Enviar logs para o STDOUT / STDERR.

Este último é a boa prática com Docker, utilize se possível. É trivial configurar o Docker Engine para que os logs de TODOS os containers sejam redirecionados para algum local externo. Cuidado com esta configuração e garanta que os logs estão sendo devidamente rotacionados, e os antigos apagados.

Volumes de Dados

Pelo mesmo motivo dos logs, dados importantes persistidos pela aplicação devem sair do container (ex: /var/lib/mysql). Caso contrário serão perdidos quando o container for destruído.

 

Este ponto é melhor compreendido se olharmos para uma arquitetura completa:

Web Tier > App Tier > BD

Apenas a camada de BD persiste dados, logo deve ser a única elegível para utilizar volume de dados (sujeito a backup). Pense nas demais camadas como “imutáveis”: os containers nela devem poder ser destruídos sem qualquer tipo de prejuízo, a qualquer momento. Isso facilita bastante a administração e acaba sendo um requisito importante se você está pensando em qualquer tipo de auto-scaling.

Configurações Através de Variáveis de Ambiente

Outro anti-pattern comum é colocar um grande volume de configurações no container, em arquivos. Não há nada de errado nisso, mas você tem dois pontos a considerar:

  • O que muda do container de produção para o de QA?
  • A mesma imagem pode ser utilizada para criar containers em ambientes distintos (eg: Apache como proxy para 2 sistemas)?

O que for identificado como necessário para que a mesma imagem atenda mais de um cenário deve ser informado via variáveis de ambiente na criação do container (ver –env)! O seu ENTRYPOINT deve ser responsável por interpretar estas variáveis e fazer os ajustes necessários. Uma opção comum é colocar no ENTRYPOINT um shell script que lê as variáveis de ambiente (tipicamente usuário e senha), gera o arquivo de configuração adequado e depois executa o processo do container.

 

Permitir “reciclar” a mesma imagem em mais de um cenário tem sempre que estar em mente. Minimamente produção e QA acabarão existindo. Tome cuidado para não abusar deste modelo. Uma coisa é colocar os certificados HTTPS em variável (um pattern que já vi algumas vezes) e outra é colocar TODO o httpd.conf do Apache em uma variável de ambiente. Ambos funcionam, mas claramente há um pouco de bom senso nisso. A regra do dia a dia é: se é algo que muda entre cada ambiente, vai para variável de ambiente, caso contrário vai para o container diretamente.

Operating System updates

Já pensou o como corrigir o próximo bug nojento do OpenSSL em todos os seus containers em produção? A resposta é simples: não se corrige. Pense em containers como algo imutável. Se você precisa atualizar algo, inclua no seu Dockerfile um comando pra atualizar o SO e o que mais for necessário e entregue a nova versão do container. Tecnicamente é possível entrar em um container e atualizar o SO, mas faz pouco sentido pois viola o princípio da imutabilidade.

Nomes das Tags

Uma consequência de atualizar o SO diretamente do Dockerfile (ex: apt-get upgrade) é que um mesmo Dockerfile, utilizado para criar imagens em dois momentos distintos, gera duas imagens distintas. Isso ocorre devido a dependências externas na criação do container, que mudam com o tempo (ex: versão do OpenSSL). Uma opção seria controlar também a versão dos pacotes no repositório do SO porém uma solução mais simples, seria adotar um padrão para a tag de cada imagem:

[git tag]_[data]

Ex:

V1.0.10_2016-07-07_18-36

Apesar de um pouco extenso, o node permitirá melhor gestão. Ex: você só precisa atualizar o SO do container, mas a versão da aplicação é a mesma. Sem problemas. Fica claro pelo nome que é a mesma versão da aplicação, mas dependências externas são diferentes.

 

Utilizar a tag “latest”, além da tag específica, também é uma prática comum para indicar qual a versão de produção.

Pacotes de SO / Prateleira

Se você vai executar uma aplicação que não foi feita para ser executada em container, tenha os seguintes pontos em mente:

  • Os scripts de start / stop do SO não foram feitos para serem executados em container.
    • Eles irão fazer fork() do daemon, e você verá uma morte prematura do container.
    • Você deve invocar o binário do daemon diretamente no ENTRYPOINT do container.
  • Rotação e purga de logs geralmente funcionam via cron, porém não há cron no container.
    • Tente redirecionar os logs do daemon para stdout / stderr.
    • Se não por possível, a alternativa é usar um entrypoint que suba o cron no container, e depois execute o daemon.

Um Processo por Container

Esse é um mantra comum na comunidade Docker, mas é bastante mal compreendido. Pense em um Apache MPM, que abre diversos processos para atender requisições. Ele viola o princípio do Docker? Bem… não. Uma melhor formulação do mantra seria “uma aplicação por container”. Exemplo: se você tem um Apache e um Jetty para subir, coloque cada um em seu container. Este artigo explora o cenário de mais de um processo por container.

Tecnicamente, você pode rodar o init em um container, e “subir o SO do zero”. Nada te impede disso. Porém isso vai contra a mentalidade de containers serem leves, auto-contidos e descartáveis. Um SO completo sobe uma variedade de serviços que sua aplicação no container não precisa… inclusive o próprio init.

Conclusão

Não pense em Docker / containers como uma opção, mas sim como algo inevitável. Quanto antes você estiver nesta onda, melhor. Mas compreenda bem a mentalidade por trás e use a ferramenta certa, da maneira certa, para o problema certo. Não somente “siga o hype” cegamente.