Boas práticas para conteinerizar aplicações Go com Docker
Jekayin-Oluwa Olabemiwo
23 de março de 2022
0 minutos de leituraAplicações Go e contêineres foram feitos um para o outro. O tamanho reduzido dos binários de aplicações Go combina perfeitamente com as estratégias de implantação de microsserviços que o Docker e o Kubernetes oferecem. Mas essa sinergia também traz desafios. Por isso, é importante conhecer as boas práticas e os conceitos fundamentais de contêineres para evitar problemas de segurança que podem surgir facilmente nas imagens.
Neste artigo, você vai programar uma aplicação Go de exemplo e aprender a conteinerizá-la da melhor forma e executá-la com segurança.
Pré-requisitos
Para acompanhar este tutorial, você precisa ter o Docker e o Go instalados na sua máquina e conhecer o básico de ambos. Você encontra instruções introdutórias e downloads do Docker na página de orientação e configuração da documentação do Docker. Há bons tutoriais de Go na documentação oficial do Go ou, se preferir, acesse diretamente a página de instalação do Go para Linux.
Crie uma aplicação Go de exemplo
Para começar, vamos criar nossa API em Go. Primeiro, navegue até o diretório da sua máquina onde você quer armazenar a aplicação. Em seguida, crie um diretório chamado “godocker”. Nesse diretório godocker/, execute o comando a seguir para definir seu módulo Go:
Em seguida, crie um arquivo chamado “main.go” dentro do diretório “godocker”. Esse arquivo conterá o código da API. Agora, você pode adicionar a lógica para fornecer e processar a hora atual pela API.
Insira o código a seguir em godocker/main.go:
Este código fornece a hora atual pela API usando a variável TimeFormat do pacote http. Em seguida, definimos a biblioteca NewServeMux como mux para configurar o servidor HTTP. Por fim, registramos o endpoint /time para retornar a hora atual na API e codificamos a resposta no endpoint /time como JSON.
Vamos fazer um teste rápido. Execute a aplicação no terminal com o comando a seguir:
A saída deve ser parecida com esta:
Agora, vamos testar a API em outra janela do terminal usando cURL. Insira o seguinte comando no terminal:
A saída deve ser parecida com esta:
Prepare o Dockerfile
Um Dockerfile contém uma série de instruções para empacotar e implantar uma aplicação como contêiner. Nesta seção, vamos criar um Dockerfile e analisar algumas instruções que podemos usar para empacotar nossa aplicação de exemplo como contêiner.
Especifique a versão da sintaxe do Docker
Primeiro, adicionamos a diretiva de sintaxe. No diretório godocker/, crie um arquivo chamado "Dockerfile" e insira o código a seguir na primeira linha:
A diretiva de sintaxe especifica o local da sintaxe do Dockerfile que usaremos para criar nosso Dockerfile. Esta linha de código define o local da sintaxe como docker/dockerfile:1, que é a versão mais recente. Antes de usar o backend Buildkit para criar o Dockerfile, o Docker verifica a versão da sintaxe.
Comente esta linha no início do arquivo. Depois da diretiva de sintaxe, a convenção é deixar uma linha em branco.
Use uma imagem base com pouco consumo de memória
Em seguida, especificamos a imagem base do Docker que queremos herdar com a instrução FROM.
Adicione o código a seguir a godocker/Dockerfil`:
Com essa instrução, não precisamos criar nossa própria imagem base do Docker. Em vez disso, herdamos a imagem oficial do Docker para aplicações Go, na variante Alpine Linux. A versão do Go na imagem base é 1.17. A imagem alpine é muito pequena em comparação com uma variante como a imagem ubuntu.
Agora, vamos completar o restante do Dockerfile, incluindo comentários no código para explicar brevemente a finalidade de cada linha.
Atualize godocker/Dockerfile para que contenha o seguinte código:
Crie a imagem
Com o Dockerfile pronto, vamos criar nossa imagem Docker a partir dele usando o comando docker build. O Docker usa o daemon do Docker para criar imagens. Usamos a opção --tag — que pode ser abreviada para -t — com o comando docker build para definir um nome personalizado para a imagem Docker.
Insira o seguinte comando no terminal:
Acrescentamos . ao comando docker build para criar a imagem no diretório atual, que funciona como contexto de build. Evite usar o caminho / como contexto de build, pois isso pode transferir todo o código-fonte para o daemon do Docker.
A saída do build deve conter a linha FINISHED e ser parecida com esta:
Essa saída significa que a imagem Docker foi criada por completo com a tag godocker.
Para ver a lista de imagens locais, insira o seguinte comando no terminal:
A saída deve ser parecida com esta:
Na nossa saída, a imagem godocker recém-criada tem 319 MB, o que é muito para uma aplicação de API simples. Por isso, precisamos otimizar o build para criar uma imagem mais enxuta. Na próxima seção, vamos implementar o conceito de builds em vários estágios para obter um build mais leve.
Use builds em vários estágios
A abordagem de builds em vários estágios ajuda a criar imagens muito menores do que as produzidas pela abordagem de estágio único que mostramos na seção anterior. Um build em vários estágios usa uma imagem para criar componentes, que são empacotados em uma imagem menor contendo apenas o essencial para executá-los. Ao reduzir nossas imagens ao mínimo necessário para executar a aplicação, diminuímos o potencial de vulnerabilidades de segurança. Para isso, precisamos usar várias instruções FROM no Dockerfile.
Use a imagem oficial scratch
Podemos começar o build com uma imagem vazia, herdando a imagem oficial do Docker scratch. Nesta seção, vamos mostrar como usar a imagem scratch em builds de vários estágios.
Primeiro, navegue até o diretório raiz da sua aplicação. Em seguida, crie um arquivo chamado “Dockerfile.multistage” e insira o seguinte código:
Este código especifica a imagem base herdada da imagem oficial golang:1.17-alpine, com o nome de estágio build. Em seguida, usamos outra instrução FROM para implementar o conceito de vários estágios, copiando o binário criado no primeiro estágio para a imagem vazia do segundo estágio.
Em seguida, precisamos criar uma nova imagem com o novo arquivo Dockerfile.multistage. Também precisamos dar a ela a tag “multistage”. Assim, fica mais fácil diferenciá-la da imagem criada anteriormente.
Insira o seguinte comando no terminal:
Depois que o build for concluído com sucesso, confira a lista de imagens inserindo o seguinte comando no terminal:
A saída deve ser parecida com esta:
Essa saída mostra a grande diferença de tamanho entre as imagens godocker:multistage e godocker:latest. Houve uma melhora evidente: a imagem de estágio único, com 319 MB, foi reduzida para 6,1 MB na imagem de vários estágios. Como dependemos de contêineres que iniciem rapidamente, otimizações como essa são essenciais ao conteinerizar aplicações Go.
Implante o contêiner
Além de otimizar o desempenho e a eficiência, também precisamos pensar na melhor forma de implantar nossos contêineres para que sejam executados com segurança. Nesta etapa do tutorial, vamos aplicar algumas boas práticas.
Execute como usuário não root
O princípio do menor privilégio exige que limitemos o acesso aos recursos do sistema. Nossos contêineres Docker para Go são contêineres de aplicação e não precisam ser executados com privilégios de root. Por isso, devemos criar um novo usuário e grupo com acesso limitado no Dockerfile para aumentar a segurança.
Para criar um usuário não root, adicione as linhas a seguir logo depois da primeira instrução FROM em godocker/Dockerfile.multistage:
Esta instrução define os argumentos USERNAME e PASSWORD com a palavra-chave ARG e, em seguida, cria um usuário com a instrução RUN adduser.
Em seguida, vamos implementar as instruções para copiar os dados do usuário do primeiro estágio e aplicá-los ao segundo.
Adicione o código em negrito no exemplo a seguir ao segundo estágio de godocker/Dockerfile.multistage, que agora deve terminar com o seguinte código:
No Kubernetes, você pode especificar runAsuser: UID no campo securityContext. Consulte a documentação do Kubernetes para saber como definir o contexto de segurança de um Pod.
Use um sistema de arquivos raiz somente leitura
Outra forma de aumentar a segurança da aplicação é executar o contêiner com um sistema de arquivos somente leitura. Para aplicar essa restrição ao sistema de arquivos do contêiner, vamos passar a flag de somente leitura com o comando docker run.
Insira o seguinte comando no terminal:
Remova ou negue recursos do Linux
Os recursos do Linux são um conjunto de privilégios que podemos ativar ou desativar ao usar o Linux. Como nosso contêiner é baseado em uma variante do Linux, também podemos usar esses recursos para aumentar a segurança da aplicação. Remover alguns deles reduz os riscos para o contêiner.
Neste tutorial, vamos remover todos os recursos, exceto setuid.
Insira o seguinte comando no terminal:
Limite o uso de CPU e memória
O comando docker run permite definir limites de uso dos recursos da máquina host para o contêiner Docker. O Docker usa a flag --cpus.
Por exemplo, para impedir que o contêiner use mais de 50% de uma única CPU, insira o seguinte comando na CLI do Docker:
Se precisar usar 2 CPUs, você pode limitar o uso com o seguinte comando na CLI do Docker:
Para limitar o uso de memória de um contêiner Docker a 1024 MB, você pode usar o comando docker run, assim:
Este comando também define um limite de memória de 256 MB, aplicado quando o Docker detecta que a memória do host está ficando baixa.
Conclusão
Neste artigo, mostramos como configurar uma aplicação Go e conteinerizá-la com o Docker. Também aplicamos o conceito de builds em vários estágios para otimizar o desempenho, criamos a partir do zero usando uma imagem oficial vazia do Docker e discutimos algumas boas práticas que podem ser úteis ao criar e implantar contêineres. Estas orientações são um ponto de partida para garantir eficiência, segurança e gerenciamento de memória robustos ao conteinerizar aplicações web em Go com o Docker.
Para saber mais sobre boas práticas de segurança, acesse a central de recursos Snyk Learn.
