Práticas recomendadas para trabalhar com contêineres

Neste artigo, há um conjunto de práticas recomendadas para facilitar o trabalho com contêineres. Essas práticas abrangem vários tópicos, incluindo segurança, monitoramento e geração de registros. O objetivo é facilitar a execução de aplicativos no Google Kubernetes Engine e em contêineres em geral. Muitas das práticas discutidas aqui foram inspiradas na metodologia 12 fatores, que é um ótimo recurso para criar aplicativos nativos da nuvem.

Essas práticas recomendadas não são de igual importância. Por exemplo, você pode executar com sucesso uma carga de trabalho de produção sem algumas delas, mas outras são fundamentais. Em especial, a importância das práticas recomendadas em relação à segurança é subjetiva. A implementação delas depende do ambiente e das restrições.

Para aproveitar ao máximo este artigo, você precisa ter conhecimento sobre o Docker e o Kubernetes. Algumas práticas recomendadas discutidas aqui também se aplicam a contêineres do Windows, mas a maioria presume que você esteja trabalhando com contêineres do Linux. Para orientações sobre como criar contêineres, consulte Práticas recomendadas para criar contêineres.

Usar mecanismos de geração de registro nativos de contêineres

Importância: ALTA

Como parte do gerenciamento de aplicativos, os registros têm informações valiosas sobre os eventos que ocorrem no aplicativo. O Docker e o Kubernetes se empenham para facilitar o gerenciamento de registros.

Em um servidor clássico, provavelmente é necessário gravar seus registros em um arquivo específico e processar a rotação de registros para evitar que os discos fiquem cheios. Se tiver um sistema de geração de registros avançado, você poderá encaminhar esses registros a um servidor remoto para centralizá-los.

Os contêineres oferecem uma maneira fácil e padronizada de lidar com registros, porque você pode gravá-los em stdout e stderr. O Docker captura essas linhas de registro e permite acessá-las usando o comando docker logs. Como desenvolvedor de aplicativos, você não precisa implementar mecanismos avançados de registro. Em vez disso, use os mecanismos nativos.

O operador da plataforma precisa fornecer um sistema para centralizar os registros e torná-los pesquisáveis. No GKE, esse serviço é fornecido pelo fluentd e pelo Stackdriver Logging. Outros métodos comuns incluem o uso de uma pilha EFK (Elasticsearch, Fluentd, Kibana).

Diagrama de um sistema clássico de gerenciamento de registro no Kubernetes.
Figura 1. Diagrama de um sistema típico de gerenciamento de registros no Kubernetes

Registros JSON

Na verdade, a maioria dos sistemas de gerenciamento de registros são bancos de dados de séries temporais que armazenam documentos indexados por tempo. Em geral, esses documentos podem ser fornecidos no formato JSON. No Stackdriver Logging e no EFK, uma única linha de registro é armazenada como um documento, junto com alguns metadados (informações sobre pod, contêiner, node e assim por diante).

Você pode aproveitar esse comportamento gerando o registro diretamente no formato JSON com diferentes campos. Pesquise seus registros com mais eficiência com base nesses campos.

Por exemplo, pense em transformar o seguinte registro em formato JSON:

[2018-01-01 01:01:01] foo - WARNING - foo.bar - There is something wrong.

Veja aqui o registro transformado:

{
  "date": "2018-01-01 01:01:01",
  "component": "foo",
  "subcomponent": "foo.bar",
  "level": "WARNING",
  "message": "There is something wrong."
}

Essa transformação permite que você pesquise facilmente nos seus registros todos os registros de nível WARNING ou todos os registros do subcomponente foo.bar.

Se decidir gravar registros formatados em JSON, esteja ciente de que é necessário gravar cada evento em uma única linha para que ele seja analisado corretamente. Na realidade, ele se parece com o seguinte:

{"date":"2018-01-01 01:01:01","component":"foo","subcomponent":"foo.bar","level": "WARNING","message": "There is something wrong."}

Como você pode ver, o resultado é muito menos legível do que a linha normal de um registro. Se decidir usar esse método, verifique se suas equipes não dependem muito da inspeção manual de registros.

Padrão de arquivo secundário do agregador de registro

Não é fácil configurar alguns aplicativos, como o Tomcat, para gravar registros em stdout e stderr. Como esses aplicativos gravam em diferentes arquivos de registro no disco, a maneira ideal de manipulá-los no Kubernetes é usar o padrão de arquivo secundário para criação de registro. Os arquivos secundários são pequenos contêineres executados no mesmo pod que seu aplicativo. Para ver mais detalhes sobre arquivos secundários, consulte a documentação oficial do Kubernetes.

Nesta solução, adicione um agente de geração de registro em um contêiner de arquivo secundário ao aplicativo (no mesmo pod) e compartilhe um volume emptyDir entre os dois contêineres, conforme mostrado neste exemplo YAML no GitHub. Em seguida, configure o aplicativo para gravar registros no volume compartilhado e configure o agente de geração de registro para ler e encaminhar esses arquivos quando necessário.

Nesse padrão, como você não está usando os mecanismos de geração de registro nativos do Docker e do Kubernetes, é necessário lidar com a rotação de registros. Se o agente de geração de registros não manipular a rotação de registros, outro contêiner de arquivo secundário no mesmo pod poderá manipular a rotação.

Padrão do arquivo secundário para gerenciamento de registros
Figura 2. Padrão do arquivo secundário para gerenciamento de registros

Garantir que seus contêineres estejam sem estado e imutáveis

Importância: ALTA

Se você estiver testando contêineres pela primeira vez, não os trate como servidores tradicionais. Por exemplo, você pode querer atualizar seu aplicativo dentro de um contêiner em execução ou aplicar patch em algum quando surgirem vulnerabilidades.

Os contêineres não são projetados para funcionar dessa maneira. Eles foram criados para serem sem estado e imutáveis.

Sem estado

Sem estado significa que qualquer estado (dados persistentes de qualquer tipo) é armazenado fora de um contêiner. Esse armazenamento externo pode assumir várias formas, dependendo do que você precisa:

  • Para armazenar arquivos, recomendamos o uso de um armazenamento de objetos, como o Cloud Storage.
  • Para armazenar informações como sessões de usuários, recomendamos o uso de um armazenamento de valor-chave externo de baixa latência, como Redis ou Memcached.
  • Para o armazenamento no nível de bloco, como para bancos de dados, use um disco externo conectado ao contêiner. No caso do GKE, recomendamos o uso de discos permanentes.

Com essas opções, é possível remover os dados do próprio contêiner, o que significa que ele pode ser encerrado e destruído de maneira correta e a qualquer momento sem receio de perder dados. Se um novo contêiner for criado para substituir o antigo, basta conectar o novo contêiner ao mesmo armazenamento de dados ou vinculá-lo ao mesmo disco.

Imutabilidade

Imutabilidade significa que um contêiner não será modificado durante sua vida útil: sem atualizações, sem aplicação de patches e sem alterações de configuração. Se precisar atualizar o código do aplicativo ou aplicar um patch, crie uma nova imagem e implante-a novamente. Essa imutabilidade torna as implantações mais seguras e mais repetíveis. Se precisar fazer alguma reversão, basta implantar a imagem antiga novamente. Essa abordagem permite que você implante a mesma imagem de contêiner em cada um de seus ambientes, tornando-os o mais idênticos possível.

Para usar a mesma imagem de contêiner em diferentes ambientes, recomendamos que você externalize a configuração do contêiner (porta de escuta, opções de tempo de execução e outros). Em geral, os contêineres são configurados com variáveis de ambiente ou arquivos de configuração ativados em um caminho específico. No Kubernetes, use Secrets e ConfigMaps para injetar configurações em contêineres como variáveis de ambiente ou arquivos.

Para atualizar uma configuração, implante um novo contêiner com base na mesma imagem usando a configuração atualizada.

Exemplo de como atualizar a configuração de uma implantação usando um ConfigMap ativado como um arquivo de configuração nos pods
Figura 3. Exemplo de como atualizar a configuração de uma implantação usando um ConfigMap ativado como um arquivo de configuração nos pods

A combinação de ausência de estado e imutabilidade é um dos argumentos de venda de infraestruturas baseadas em contêineres. Essa combinação permite automatizar implantações e aumentar a frequência e a confiabilidade delas.

Evitar contêineres privilegiados

Importância: ALTA

Em uma máquina virtual ou em um servidor bare-metal, evite executar seus aplicativos com o usuário root por um motivo simples: se o aplicativo for comprometido, o invasor terá acesso total ao servidor. Pelo mesmo motivo, evite usar contêineres privilegiados. Contêiner privilegiado é um que tem acesso a todos os dispositivos da máquina host, ignorando quase todos os recursos de segurança dos contêineres.

Se acreditar que precisa usar contêineres privilegiados, pense nestas alternativas:

  • Forneça recursos específicos ao contêiner por meio da opção securityContext do Kubernetes ou da sinalização --cap-add do Docker. A documentação do Docker lista os recursos habilitados por padrão e os que precisam ser habilitados de maneira explícita.
  • Se o aplicativo precisar modificar as configurações do host para ser executado, faça isso em um contêiner de arquivo secundário ou em um contêiner init. Ao contrário do seu aplicativo, esses contêineres não precisam ser expostos a tráfego interno ou externo, tornando-os mais isolados.
  • Se precisar modificar o sysctls no Kubernetes, use a anotação dedicada.

No Kubernetes, os contêineres privilegiados podem ser proibidos por uma Política de segurança de pods específica. Essa política é um objeto do Kubernetes que o administrador do cluster configura e gerencia e que impõe requisitos específicos para pods. No cluster do Kubernetes, não é possível criar pods que violem esses requisitos.

Facilitar o monitoramento do aplicativo

Importância: ALTA

Assim como a geração de registros, o monitoramento é uma parte importante do gerenciamento de aplicativos. De muitas maneiras, o monitoramento de aplicativos em contêineres segue os mesmos princípios dos que não estão em contêineres. No entanto, como as infraestruturas em contêiner tendem a ser altamente dinâmicas, com contêineres que são criados ou excluídos com frequência, não é possível reconfigurar seu sistema de monitoramento sempre que isso acontece.

É possível distinguir duas classes principais de monitoramento: de caixa preta e de caixa branca. O monitoramento de caixa preta refere-se à análise de seu aplicativo pelo lado de fora, como se você fosse um usuário final. Ele será útil se o serviço final que você quer oferecer estiver disponível e funcionando. Por ser externo à infraestrutura, o monitoramento de caixa preta não é diferente entre infraestruturas tradicionais e em contêineres.

O monitoramento de caixa branca refere-se à análise de seu aplicativo com algum tipo de acesso privilegiado e à coleta de métricas sobre seu comportamento que um usuário final não pode ver. Como esse monitoramento precisa examinar as camadas mais profundas de sua infraestrutura, ele é muito diferente para infraestruturas tradicionais e em contêiner.

Uma opção conhecida na comunidade do Kubernetes para monitoramento de caixa branca é o Prometheus, um sistema que pode descobrir automaticamente os pods a serem monitorados. O Prometheus copia os pods de métricas. Ele espera um formato específico para eles. O Stackdriver pode monitorar os clusters do Kubernetes e os aplicativos que eles executam com uma versão própria do Prometheus. Saiba como ativar o Stackdriver Kubernetes Monitoring no GKE.

Veja aqui uma demonstração do Stackdriver Kubernetes Monitoring em ação. Clique na imagem para ver uma versão ampliada:

Painel do Stackdriver Kubernetes Monitoring
Figura 4. Painel do Stackdriver Kubernetes Monitoring

Para se beneficiar do Prometheus ou do Stackdriver Kubernetes Monitoring, seus aplicativos precisam expor métricas no formato do Prometheus. Os dois métodos a seguir mostram isso.

Ponto de extremidade HTTP de métricas

O ponto de extremidade HTTP de métricas funciona de maneira semelhante aos que serão mencionados posteriormente em expor a integridade de seu aplicativo. Ele expõe as métricas internas do aplicativo, geralmente em um URI de /metrics. A resposta é semelhante a esta:

http_requests_total{method="post",code="200"} 1027
http_requests_total{method="post",code="400"}    3
http_requests_total{method="get",code="200"} 10892
http_requests_total{method="get",code="400"}    97

Neste exemplo, http_requests_total é a métrica, method e code são rótulos e o número à extrema direita é o valor dessa métrica para esses rótulos. Desde sua inicialização, o aplicativo respondeu a uma solicitação HTTP GET 97 vezes com o código do erro 400.

A geração desse endpoint HTTP é facilitada pelas bibliotecas de cliente do Prometheus que existem em várias linguagens. O OpenCensus também pode exportar métricas usando esse formato, entre muitos outros recursos. Não exponha esse endpoint à Internet pública.

Esse assunto é abordado em detalhes na documentação oficial do Prometheus (conteúdo em inglês). Leia também o Capítulo 6 (em inglês) da Engenharia de Confiabilidade do Site para saber mais sobre o monitoramento de caixa branca e de caixa preta.

Padrão de arquivo secundário para monitoramento

Nem todos os aplicativos podem ser instrumentados com um endpoint HTTP /metrics. Para manter o monitoramento padronizado, recomendamos usar o padrão de arquivo secundário para exportar as métricas no formato correto.

A seção Padrão de arquivo secundário do agregador de registro explica como usar um contêiner de arquivo secundário para gerenciar os registros do aplicativo. É possível usar o mesmo padrão de monitoramento. O contêiner do arquivo secundário hospeda um agente de monitoramento que traduz as métricas à medida que são expostas pelo aplicativo para um formato e protocolo que o sistema de monitoramento global entende.

Pense em um exemplo concreto: aplicativos Java e JMX (Java Management Extensions). Muitos aplicativos Java expõem métricas usando o JMX. Em vez de gravar novamente um aplicativo para expor métricas no formato do Prometheus, aproveite o jmx_exporter. O jmx_exporter reúne métricas de um aplicativo por meio do JMX e as expõe por meio de um ponto de extremidade /metrics que o Prometheus pode ler. Essa abordagem também tem a vantagem de limitar a exposição do ponto de extremidade JMX, que pode ser usada para modificar as configurações do aplicativo.

Padrão de arquivo secundário para monitoramento
Figura 5. Padrão de arquivo secundário para monitoramento

Expor a integridade de seu aplicativo

Importância: MÉDIA

Para facilitar seu gerenciamento na produção, o aplicativo precisa comunicar seu estado ao sistema em geral. O aplicativo está em execução? É íntegro? Está pronto para receber tráfego? Como ele está se comportando?

O Kubernetes tem dois tipos de verificações de integridade: sondagens de atividade e de prontidão. Conforme descrito nesta seção, cada um tem um uso específico. Você pode implementá-los de várias maneiras (incluindo a execução de um comando dentro do contêiner ou a verificação de uma porta TCP), mas o método preferido é usar os pontos de extremidade HTTP descritos nesta prática recomendada. Para mais informações sobre este tópico, consulte a documentação do Kubernetes.

Sondagem de ativação

A maneira recomendada de implementar a sondagem de ativação é que seu aplicativo exponha um endpoint HTTP /health. Ao receber uma solicitação nesse endpoint, o aplicativo precisará enviar a resposta "200 OK" se for considerado íntegro. No Kubernetes, "íntegro" significa que o contêiner não precisa ser eliminado ou reiniciado. A integridade varia de um aplicativo para outro, mas geralmente significa o seguinte:

  • O aplicativo está em execução.
  • Suas principais dependências são atendidas (por exemplo, é possível acessar seu banco de dados).

Sondagem de preparo

A maneira recomendada de implementar a sondagem de preparo é que seu aplicativo exponha um endpoint HTTP /ready. Ao receber uma solicitação nesse endpoint, o aplicativo precisará enviar a resposta "200 OK" se estiver pronto para receber tráfego. Isso quer dizer o seguinte:

  • O aplicativo é íntegro.
  • Quaisquer etapas potenciais de inicialização foram concluídas.
  • Qualquer solicitação válida enviada ao aplicativo não resulta em erro.

O Kubernetes usa a sondagem de prontidão para orquestrar a implantação de seu aplicativo. Se você atualizar uma implantação, o Kubernetes fará uma atualização contínua dos pods pertencentes a essa implantação. A política padrão é atualizar um pod de cada vez. O Kubernetes espera que o novo pod esteja pronto, conforme indicado pela sondagem de preparo, antes de atualizar o próximo.

Evitar a execução como raiz

Importância: MÉDIA

Os contêineres oferecem isolamento. Com configurações padrão, um processo dentro de um contêiner Docker não pode acessar informações da máquina host ou de outros contêineres colocalizados. No entanto, como os contêineres compartilham o kernel da máquina host, o isolamento não é tão completo como no caso de máquinas virtuais, conforme explica esta postagem do blog. Um invasor pode encontrar vulnerabilidades ainda desconhecidas, no Docker ou no próprio kernel do Linux, que permitiriam a ele escapar do contêiner. Se o invasor encontrar uma vulnerabilidade e seu processo estiver sendo executado como raiz dentro do contêiner, ele terá acesso raiz à máquina host.

À esquerda: máquinas virtuais usam hardware virtualizado. À direita: os aplicativos em contêineres usam o kernel do host.
Figura 6. À esquerda: máquinas virtuais usam hardware virtualizado. À direita: os aplicativos em contêineres usam o kernel do host.

Para evitar essa possibilidade, é uma prática recomendada não executar processos como raiz dentro de contêineres. Para aplicar esse comportamento no Kubernetes, use um PodSecurityPolicy. Ao criar um pod no Kubernetes, use a opção runAsUser para especificar o usuário do Linux que está executando o processo. Essa abordagem modifica a instrução USER do Dockerfile.

Na realidade, existem desafios. Muitos pacotes de software conhecidos têm o processo principal deles executado como raiz. Se quiser evitar a execução como raiz, crie seu contêiner para que ele possa ser executado com um usuário desconhecido e sem privilégios. Em geral, essa prática significa que você precisa ajustar as permissões em várias pastas. Ao seguir a prática recomendada de um único aplicativo por contêiner e executar um único aplicativo com um único usuário, preferencialmente não raiz, é possível conceder a todos os usuários permissões de gravação para as pastas e arquivos que precisam ser gravados e tornar todas as outras pastas e arquivos graváveis apenas por raiz.

Uma maneira simples de verificar se o contêiner está em conformidade com essa prática recomendada é executá-lo no local com um usuário aleatório e testar se ele funciona corretamente. Substitua [YOUR_CONTAINER] pelo nome do contêiner.

docker run --user $((RANDOM+1)) [YOUR_CONTAINER]

Se o contêiner precisar de um volume externo, configure a opção fsGroup do Kubernetes para conceder a propriedade desse volume a um grupo específico do Linux. Essa configuração soluciona o problema da propriedade de arquivos externos.

Se o processo for executado por um usuário sem privilégios, ele não poderá se vincular a portas abaixo da 1024. Geralmente isso não é problema, porque você pode configurar os Serviços do Kubernetes para rotear o tráfego de uma porta para outra. Por exemplo, configure um servidor HTTP para ligar-se à porta 8080 e redirecionar o tráfego da porta 80 com um Serviço Kubernetes.

Escolher com cuidado a versão da imagem

Importância: MÉDIA

Quando você usa uma imagem do Docker, seja como uma imagem de base em um Dockerfile ou como uma imagem implantada no Kubernetes, escolha a tag da imagem que está sendo usada.

A maioria das imagens públicas e privadas segue um sistema de marcação semelhante ao descrito em Práticas recomendadas para criar contêineres. Se a imagem usar um sistema próximo ao Controle de versão semântico, pense em alguns detalhes de marcação.

Mais importante, a tag "mais recente" pode ser movida com frequência de imagem para imagem. A consequência é que não é possível confiar nessa tag para versões previsíveis ou reproduzíveis. Por exemplo, veja o Dockerfile a seguir:

FROM debian:latest
RUN apt-get -y update && \ apt-get -y install nginx

Se você criar uma imagem a partir desse Dockerfile duas vezes, em diferentes momentos, é possível ficar com duas versões diferentes do Debian e do NGINX. Em vez disso, examine esta versão revisada:

FROM debian:9.4
RUN apt-get -y update && \ apt-get -y install nginx

Com uma tag mais precisa, você garante que a imagem resultante sempre será baseada em uma versão secundária específica do Debian. Como a versão específica do Debian também é fornecida com uma versão específica do NGINX, você tem muito mais controle sobre a imagem que está sendo criada.

Esse resultado ocorre não só no momento da criação, mas também no tempo de execução. Se referenciar a tag "latest" em um manifesto do Kubernetes, você não terá garantia da versão que o Kubernetes usará. Diferentes nós do cluster podem usar a mesma tag "latest" em momentos diferentes. Se a tag foi atualizada em um ponto entre os pulls, será possível ter nós diferentes executando imagens diferentes (todas marcadas como "latest" em um ponto).

O ideal é que você sempre use uma tag imutável na sua linha FROM. Essa tag permite que você tenha versões que possam ser reproduzidas. No entanto, existem algumas vantagens e desvantagens de segurança: quanto mais você fixar a versão que quer usar, menos automatizados serão os patches de segurança em suas imagens. Se a imagem usada estiver com o controle de versão semântico apropriado, a versão do patch (ou seja, o "Z" em "X.Y.Z") não poderá ter alterações incompatíveis com versões anteriores. Você poderá usar a tag "X.Y" e receber correções de bugs automaticamente.

Imagine um software chamado "SuperSoft". Suponha que o processo de segurança para o SuperSoft corrija as vulnerabilidades por meio de uma nova versão do patch. Você quer personalizar o SuperSoft e escreveu o seguinte Dockerfile:

FROM supersoft:1.2.3
RUN a-command

Após um tempo, o fornecedor descobre uma vulnerabilidade e lança a versão 1.2.4 do SuperSoft para corrigir o problema. Nesse caso, é sua responsabilidade ficar informado sobre os patches da SuperSoft e atualizar seu Dockerfile de acordo. Em vez disso, se você usar FROM supersoft:1.2 em seu Dockerfile, a nova versão será extraída automaticamente.

No final, você precisa examinar cuidadosamente o sistema de tag de cada imagem externa que está usando, decidir o quanto você confia nas pessoas que criam essas imagens e decidir qual tag você usará.

Próximas etapas

Teste outros recursos do Google Cloud Platform. Veja nossos tutoriais.

Esta página foi útil? Conte sua opinião sobre:

Enviar comentários sobre…