GoLang e Docker

Evandro F. Souza
Training Center
Published in
9 min readMay 3, 2018

Olá, este post faz parte de um série de três partes:

Parte 1: GoLang — Simplificando a complexidade

Parte 2: Goroutines e go channels

Parte 3: GoLang e Docker (você está aqui)

No post anterior, foi estudado os conceitos e vantagens do uso de goroutines e channels criando uma aplicação em Go. O objetivo deste post é containerizar esta aplicação e rodar múltiplos containers usando Docker compose.

Um novo modo de pensar

A conteinerização alterou não apenas a arquitetura de serviços, mas também a estrutura dos ambientes usados para desenvolvê-los. Agora, quando o software é distribuído em containers, os desenvolvedores têm total liberdade de decidir de quais aplicações eles precisam. Como resultado, até mesmo ambientes complexos podem ser instanciados em segundos. O desenvolvimento de software se tornou mais fácil e eficaz.

Com estas mudanças, novas questões surgem. Por exemplo, como desenvolvedor, como posso recriar facilmente uma arquitetura de microsserviços na minha máquina de desenvolvimento? E como posso ter certeza de que permanece inalterado à medida que se propaga através de um processo de entrega contínua(Continuous Delivery)? E por fim, como posso ter certeza de que um ambiente complexo de build e teste pode ser facilmente reproduzido?

Apresentando Docker Compose

A resposta para todas estas perguntas é o Docker Compose. O Docker Compose é uma ferramenta para definir e rodar aplicações complexas com Docker. Com ele, é possível definir uma aplicação multi-containers em um único arquivo e então subir todos os containers e recursos necessários utilizando um único comando.

Funcionalidades

A principal função do Docker Compose é a criação de uma arquitetura de microsserviços, ou seja, a criação de containers e a comunicação entre eles. Porém, é interessante falar que a ferramenta é capaz de muito mais:

Build de imagens:

docker-compose build

Escalar containers rodando determinado serviço:

docker-compose scale SERVICE=3

Recuperação de containers que pararam:

docker-compose up --no-recreate

Todas estas funcionalidades estão disponíveis através do command line docker-compose. Que possui um conjunto de comandos muito semelhante ao oferecido pelo Docker.

build    Build or rebuild services 
help Get help on a command
kill Kill containers
logs View output from containers
port Print the public port for a port binding
ps List containers
pull Pulls service images
rm Remove stopped containers
run Run a one-off command
scale Set number of containers for a service
start Start services
stop Stop services
restart Restart services
up Create and start containers

Eles não são apenas semelhantes, mas também se comportam como contrapartes do Docker. A única diferença é que eles afetam toda a arquitetura de vários contêineres definida no arquivo de configuração docker-compose.yml e não apenas em um único container.

Fluxo do Docker compose

Para implantar o Docker Compose na sua aplicação, é necessário passar por três etapas, são elas:

  1. Definir cada serviço em um dockerfile.
  2. Definir os serviços e sua relação entre eles no arquivo docker-compose.yml.
  3. Rodar o comando docker-compose up para iniciar o sistema.

No decorrer deste post vamos passar por estes três passos. Para isso vamos criar um container da aplicação que desenvolvemos no post anterior e rodar utilizando o docker-compose.

Mudanças na nossa aplicação

No post anterior foi desenvolvida uma aplicação console que efetua a extração de informações de posts do Medium utilizando a técnica webscraper. A solução que foi desenvolvida possui somente um serviço. Para conseguirmos simular uma arquitetura com microsserviços vamos implementar duas novas features na aplicação:

  1. Vamos transformar nossa aplicação console em um web server. Tornando possível rodar ela como serviço e comunicar via protocolo HTTP.
  2. Para simular a comunicação entre contêineres, vamos vincular algum serviço apartado, no caso do exemplo utilizarei o banco de dados em memória Redis.

Como o foco deste post é Docker e Docker Compose, eu não vou entrar no detalhe destas mudanças. Caso queira dar uma olhada acesse no Github. Na figura abaixo é possível observar uma visão macro da aplicação.

Mudanças na aplicação

Preparando o ambiente

Antes de começar, é necessário instalar as ferramentas que serão utilizadas. O Docker e o Docker Compose.

Dica: Não instale através de instaladores de pacotes ( como por exemplo o apt get). Pois há chances da versão do pacote estar desatualizada ( isso já aconteceu comigo).

Abaixo segue o link de ambas instalações oficiais:

Dockerfile

Como foi visto no fluxo de trabalho do Docker compose, o primeiro passo de tudo é criar a definição do serviço no dockerfile.

Um Dockerfile é um arquivo de texto que contém todos os comandos de linha que um usuário chamaria para rodar seu sistema. Usando o comando docker build, o usuário executa um build automático que roda vários comandos de linha em sucessão. O resultado de todos esses comandos de linha será uma imagem do serviço( futuro container). Caso você queira entender mais detalhes sobre o DockerFile, acesse este link.

Mãos a obra

Vamos montar o DockerFile do nosso projeto:

  1. Na pasta raiz do projeto crie um arquivo chamado Dockerfile.
  2. Cole o conteúdo abaixo no arquivo

Vamos explicar cada uma das linhas:

FROM golang

Sempre o primeiro comando de um DockerFile, o FROM configura a imagem base(Localizada no DockerHub) do nosso container. Neste caso esta sendo utilizada uma imagem contendo o Debian com o Go instalado.

ADD . /go/src/wscraper

O ADD simplesmente copia arquivos do sistema de arquivos locais para a imagem criada. No exemplo todos os arquivos da pasta raiz serão copiados para o caminho /go/src/wscraper .

RUN go get golang.org/x/net/html
RUN go get github.com/go-redis/redis
RUN go install wscraper

O RUN executará qualquer comando em uma nova camada sobre a imagem atual e irá consolidar os resultados na imagem final. No exemplo, as duas primeiras linhas estão buscando os pacotes que precisamos para nossa aplicação rodar. Na terceira linha é efetuado o build da aplicação.

ENTRYPOINT /go/bin/wscraper

O ENTRYPOINT permite preparar o container para rodar como um executável. Permitindo assim passar argumentos para nossa aplicação via docker run <image> -d args . No exemplo, está sendo executado o binário gerado no comando RUN go install wscraper .

EXPOSE 8080

O EXPOSE informa ao Docker que o container escuta na porta especificada.

Utilizando imagens sucintas

Os passos ADD, RUN e ENTRYPOINT são tarefas comuns em qualquer projeto Go. Para simplificar, existe uma variante onbuild da imagem do Go. Ela automaticamente copia o código fonte, busca todas as dependências, faz build da aplicação e configura para rodar.

Observe abaixo como ficou simples o nosso Dockerfile utilizando o :onbuild:

Rodando o Docker

Agora que estamos com o nosso arquivo Dockerfile criado, vamos testar se está tudo funcionando.

Primeiramente vamos rodar o comando para efetuar o build da imagem. Navegue até o diretório que está o seu Dockerfile e rode o seguinte comando:

docker build -t webscraper .

Este comando buscará a imagem base do golang diretamente do Docker Hub, copiará os fontes do nosso sistema para dentro, fará o build do sistema e ao termino irá etiquetar a imagem com a tag webscraper ( é isso que o -t webscraper faz).

A primeira vez que o comando é rodado pode demorar, pois ele irá buscar todas as dependências e montar a imagem. Nas próximas vezes algumas imagens já estarão no repositório local do seu Docker. Para visualizar a lista de imagens baixadas rode o comando abaixo:

docker images

No resultado do comando é possível observar a imagem que montamos, assim como aquelas que foram buscadas do Docker Hub.

Agora que já possuímos a imagem da nossa aplicação, vamos rodar ela e ver tudo funcionando.

docker run --publish 6060:8080 --name webscraper  --rm webscraper

A flag --publish informa ao Docker para o redirecionar a porta 8080 do Container para a externa 6060.

A flag --name fornece um nome para o nosso container, facilitando o trabalho de manutenção.

A flag --rm avisa ao Docker que a imagem do container deve ser excluída quando o server sair.

Agora com o container rodando, abra o web browser e acesse http://localhost:6060/ . Deverá aparecer algo assim:

Está funcionando! Porém se tentarmos acessar a /process-urls/ irá ocorrer um erro avisando sobre a falta de um servidor Redis. Para resolver, precisamos subir outro container rodando o Redis e fazer ambos de comunicarem. Vamos fazer isso utilizando o Docker Compose.

Configurando o docker-compose.yml

No inicio inicio do post foi feita uma introdução do Docker Compose e suas funcionalidades. Foi visto também que a definição dos serviços é configurada no arquivo docker-compose.yml . Vamos ver agora como ficaria o arquivo de configuração da nossa aplicação.

Agora vamos comentar linha por linha:

version: '3'

O version é onde declaramos a versão do arquivo de configuração. No momento que este post é escrito, a versão mais recente é a major 3. A cada mudança de versão ocorrem algumas mudanças de sintaxe. A boa prática é sempre utilizar a mais atual.

services: 
web:
build: .
ports:
- "6060:8080"
depends_on:
- redis
redis:
image: "redis:alpine"

O services contém a lista de serviços(containers) que serão inicializados na nossa aplicação. No exemplo acima, nossa aplicação possui dois serviços web e o redis.

O serviço web é a nossa aplicação, vamos falar de cada comando:

build: .

O build contém o diretório no qual está localizado o arquivo Dockerfile. Existem outras maneiras de utilizar este comando, passando argumentos ou até especificando um Dockerfile com nome alternativo. Acesse este link para mais detalhes.

ports:     
- "6060:8080"

Semelhante a flag --publish do Docker, o ports para a porta configurada. O exemplo a porta 8080 do Container está sendo exporta para a 6060.

depends_on:
- redis

O depends_on serve para expressar dependências entre serviços. Desta maneira, os serviços serão inicializado na ordem especificada. No exemplo, o serviço web depende do redis. Desta maneira, o redis será iniciado antes do serviço web.

redis:    
image: "redis:alpine"

E por fim, o image, neste comando é possível especificar a imagem que o container utilizará. Caso a imagem não esteja no repositório, será buscada do DockerHub. No nosso exemplo, estamos inicializando um contêiner com a imagem Redis alpine.

Agora com o arquivo salvo docker-compose.yml na pasta raiz da aplicação, rode o comando:

docker-compose up

Deverá aparacer algo como na imagem abaixo:

Agora tente acessa o http://localhost:6060/process-urls.

Perfeito!

De fato está funcionando! Conectou no Redis corretamente. Apensas mais um teste, vamos acessar a rota /last-process/ , ela vai retornar os últimos dados salvos no Redis.

Utilize o comando docker-compose down para parar e destruir todos os containers.

Agora que o nosso ambiente de desenvolvimento está configurado. Podemos rodar localmente nossa aplicação utilizando o redis e sem a necessidade de um servidor redis configurado externamente. Com apenas um comando, inicializamos ou paramos toda a nossa aplicação.

[BONUS] Ainda pode melhorar

Da maneira que está atualmente, não está muito prático para o desenvolvedor. Pois sempre que for modificada a aplicação, será necessário rodar dois comandos para as alterações refletirem:

docker-compose build
docker-compose up

O docker-compose buildirá reconstruir o container todo — inclusive buscar novamente os pacotes e efetuar o build da aplicação — e isso deve demorar alguns segundos. Realmente nada prático.

Para aqueles que já trabalharam com Django( ou outras ferramentas que agora não me recordo). Certamente gostam da funcionalidade de reload automático que elas possuem, no qual a aplicação recarrega assim que algum arquivo é modificado. Com Go também é possível fazer isso, utilizando o Fresh.

Vamos efetuar algumas mudanças para adicionar o Fresh na nossa aplicação.

Primeiro vamos modificar o Dockerfile para adicionar o pacote Fresh como dependência:

Agora vamos modificar o docker-compose.yml e adicionar três linhas( destacadas abaixo):

version: '3'
services:
web:
build: .
ports:
- "6060:8080"
command: fresh
volumes:
- .:/go/src/app

depends_on:
- redis
redis:
image: "redis:alpine"

O command serve para substituir o comando que será rodado quando o container for inicializado.

O volumes serve para mapear um diretório entre o hospedeiro( máquina localhost) e o container. No exemplo, a pasta local está sendo mapeada para a pasta /go/src/app, que é a pasta que roda a aplicação. Desta maneira, sempre que os arquivos são modificados localmente, eles são refletidos na aplicação rodando dentro do container.

Assim como muitos outros comandos aqui citados, o volumes serve para muitas outras coisas. Caso queira saber mais detalhes acesse aqui.

Após salvo o arquivo, rode novamente a sua aplicação e modifique algum arquivo localmente, atualize o seu browser e as mudanças deverão refletir.

Se quiser trocar uma ideia ou entrar em contato comigo, pode me achar no Twitter (@e_ferreirasouza) , Linkedin ou deixar um comentário aqui.

Grande abraço e até a próxima!

--

--