Escrevendo um micro serviço gRPC em Go: estrutura, definição e deploy no Google Kubernetes Engine

Alexandre Miziara
Único
Published in
9 min readSep 11, 2020

Breves considerações

Você já deve ter se deparado com alguns tutoriais aqui no Medium que se tratam justamente da criação de micro serviços gRPC em Go. Eu mesmo já li alguns excelentes quando comecei a me aventurar por esse assunto, os quais me nortearam durante minha jornada inicial. Ainda assim, decidi escrever um guia mais prático com o propósito de compartilhar alguns dos aprendizados que obtive depois de quase um ano utilizando desta arquitetura em um dos nossos produtos na unico.

Não me aprofundarei nos detalhes mais técnicos de tecnologias específicas, como: gRPC, Protocol Buffers, HTTP/2.0 e Kubernetes. Muito menos em discussões conceituais sobre os desafios de uma arquitetura orientada a micro serviços e as vantagens/desvantagens do uso de gRPC sobre REST.

Assumo que você já possua algum conhecimento mínimo com relação à esses tópicos e que, portanto, já esteja decidido que essa é a stack/arquitetura que deseja utilizar no seu sistema. Se você ainda não está familiarizado com esses conceitos, recomendo estudá-los primeiro. Alguns artigos que sugiro:

Pré-requisitos

Nesse tutorial, faremos o deploy da nossa aplicação para um cluster Kubernetes do Google (GKE). Se você deseja seguir esse step, é importante que já tenha um cluster criado, pois não abordarei a criação do mesmo neste guia.

Estrutura e organização do projeto

Antes de começar propriamente a definição do micro serviço, vamos entender o layout do projeto. Utilizo como base um modelo mantido pela própria comunidade (não oficial), que define um conjunto de padrões fundamentados em algumas práticas comuns dentro do ecossistema Go.

O projeto vai ter a seguinte estrutura de diretórios e arquivos:

.
├── Dockerfile
├── Makefile
├── cmd
│ └── main.go
├── go.mod
├── go.sum
├── internal
│ └── svc
│ └── handlers.go
├── k8s
│ └── deploy.yaml
├── pkg
│ └── pb
└── proto
└── greeting
├── common
│ └── v1
│ └── message.proto
└── v1
└── service.proto

Sendo que:

  • cmd/ é o diretório que aloca o arquivo main.go, que idealmente deve possuir apenas a função principal para invocar o código contido nos pacotes internal/ ou pkg/.
  • internal/ é o diretório onde estarão definidos os pacotes do projeto que serão privados, ou seja, que não poderão ser importados como uma biblioteca por outras aplicações Go. Essa é uma regra imposta pelo próprio compilador.
  • k8s/: é o diretório onde definiremos os manifestos YAML necessários para fazermos o deploy da aplicação no Kubernetes.
  • pkg/: é o diretório onde deixaremos expostos os pacotes que podem ser importados como uma biblioteca por outras aplicações Go. Nesse exemplo: os stubs gerados pelos arquivos Protobuf.
  • proto/: é o diretório onde definiremos os arquivos Protobuf.

Naturalmente, conforme seu projeto for crescendo, você pode e deve adotar novos padrões que melhor se adaptam ao sistema que está construindo. Por exemplo, se sua aplicação trabalha com um banco de dados MySQL você pode decidir adicionar um diretório migrations/ para armazenar os seus scripts .sql. E por aí vai. Esse modelo é apenas uma sugestão, que não interfere de forma alguma no desenvolvimento do micro serviço em si.

DICA: Se estiver interessado em mais detalhes sobre esse layout, vale a pena checar o repositório golang-standards/project-layout.

Aplicação

A aplicação que iremos desenvolver é o habitual Hello World, para que não tenhamos que elaborar lógicas de negócio que não competem com o o foco deste artigo: demonstrar como podemos criar de forma simples um serviço gRPC.

Antes de prosseguir, certifique-se de que você já tenha criado a estrutura de diretórios apresentada no tópico anterior e que tenha iniciado seu projeto Go:

go mod init

Definição do serviço

Quando executamos uma requisição REST, a informação trafegada é um arquivo JSON. Não há restrições explícitas entre cliente e servidor de que informações deve conter o objeto e, portanto, cabe unicamente ao servidor validar se o objeto está estruturado da forma como espera receber.

Por outro lado, quando fazemos uso de gRPC, nós trafegamos binários que são serializados com base em uma estrutura de dados definida por arquivos Protobuf (sigla para Protocol Buffers). Ou seja, podemos pensar que esses arquivos representam um contrato explícito estabelecido entre cliente e servidor e que, portanto, quaisquer alterações nessa estrutura podem quebrar efetivamente a comunicação entre ambas as partes.

Sendo assim, é muito propício que comecemos a definição do nosso serviço pela escrita dos nossos “contratos” (arquivos .proto). Comece criando um arquivo message.proto dentro de proto/greeting/common/v1:

Nele, definimos a mensagem Message que contém dois campos:

  • greeting: é a representação do cumprimento que enviaremos nas requisições, sendo definida por um enum Greeter, que especifica uma lista pré-definida de constantes que podem ser utilizadas.
  • name: é uma string que representa o nome da pessoa que iremos cumprimentar.

Agora vamos definir propriamente o serviço gRPC. Para isso, crie um arquivo service.proto dentro do diretório proto/greeting/v1:

Onde:

  • GreeterService: é o nome que determinamos para o serviço. Ele possui apenas um método RPC chamado Greet, que recebe como parâmetro uma mensagem do tipo GreetRequest e retorna uma resposta GreetResponse.
  • GreetRequest: estabelece a estrutura da requisição, possuindo apenas um campo msg do tipo Message (struct definida no pacote common/v1 que acabamos de escrever).
  • GreetResponse: representa a resposta enviada pelo método, contendo apenas uma string, que concatenará a mensagem de cumprimento que enviarmos na requisição.

Note que configuramos o parâmetro go_package em ambos os arquivos .proto, o qual nos permite especificar o caminho onde os stubs gerados em Go (arquivos .pb.go) serão criados e o nome do pacote que queremos usar.

O caminho dos arquivos .proto deve coincidir com o nome especificado pelo package, caso contrário você receberá um erro de compilação dos stubs. É uma boa prática utilizar versões, como por exemplo v1, para que possamos manter outros contratos ainda válidos em uma eventual quebra de compatibilidade.

Supondo que você esteja rodando seu server local na porta 50051, a URL do seu endpoint teria o seguinte formato:

http://localhost:50051/greeting.v1.GreeterService/Greet

⚠️ Não esqueça de alterar a variável {{YOUR_REPO}} nos gists acima para o caminho do seu repositório no GitHub (ou qualquer outra plataforma que utilize para versionar seu código).

Agora que definimos como será a estrutura do nosso micro serviço, precisamos gerar os stubs em Go, que serão usado na implementação do server. Para isso, você precisará ter o protoc-gen-go instalado. Eu recomendo a utilização de uma imagem Docker que já possua todas as dependências instaladas.

Utilizaremos nesse exemplo a imagem thethingsindustries/protoc, que oferece suporte à uma série de linguagens e inclui alguns plugins interessantes do ecossistema Go. Você pode checar o repositório pra visualizar a lista completa suportada por essa imagem. Se preferir optar pela instalação na sua máquina, vale checar a documentação oficial do Google.

Preparei um arquivo Makefile contendo um comando para geração dos stubs. É uma boa prática tentarmos automatizar esse tipo de tarefa, tornando sustentável operações que serão habituais no nosso fluxo de desenvolvimento:

O que o comando compile-proto-go faz basicamente é: executar o protoc na imagem Docker para todos os arquivos .proto do diretório proto. Além disso, configuramos o output para compilar os stubs em Go com suporte a gRPC. Sendo assim, basta rodar:

make compile-proto-go

Se você não recebeu nenhuma mensagem no bash, significa que a compilação terminou com sucesso. Você pode conferir o diretório pkg/pb para visualizar os stubs que foram gerados, respeitando a mesma hierarquia de diretórios definida nos arquivos Protobuf:

pkg/pb
└── greeting
├── common
│ └── v1
│ └── message.pb.go
└── v1
└── service.pb.go

Implementação do servidor

A implementação do servidor é bem simples. O ideal é começarmos escrevendo os handlers do serviço. Para isso, crie um arquivo handlers.go no diretório internal/svc:

O que nós fizemos foi:

  1. Criar uma struct GreeterService que representa a interface do serviço nos stubs que foram compilados a partir das especificações dos arquivos Protobuf.
  2. Implementar todas as funções que representam os endpoints do serviço. Nesse exemplo, temos apenas o método Greet.

No método Greet existe primeiramente uma validação do campo Msg. Caso este valor seja nulo, retornamos um erro específico existente no gRPC para argumentos inválidos (lista completa de status aqui). Caso contrário, retornamos a resposta com a mensagem de cumprimento formatada com o nome da pessoa enviado na requisição.

Uma vez que todos os handlers estejam implementados, podemos configurar o servidor. Crie um arquivo main.go no diretório cmd:

Esse arquivo contém a lógica “principal” da aplicação. A função main será a primeira a ser executada, na goroutine principal. Os passos que executamos são:

  1. Inicializamos um listener TCP no endereço onde serviremos o serviço gRPC. Nesse exemplo, adotamos localhost:50051.
  2. Instanciamos o servidor gRPC. Nessa etapa, existe uma série de configurações extras que podem ser feitas de acordo com as necessidades de sua aplicação, como por exemplo: middlewares, health probe, tracing, gateway, etc. Vale a pena conferir os repositórios open-source disponíveis no github.com/grpc-ecosystem.
  3. Registramos os handlers implementados no pacote internal/svc.
  4. Iniciamos uma goroutine que escuta em um channel algum evento de interrupção da aplicação. Caso isso ocorra, conseguimos parar o servidor em modo graceful.
  5. Por fim, servimos o micro serviço no endereço especificado no listener.

Build e deploy no GKE

Estamos prontos para buildar a aplicação e fazermos o deploy no Google Kubernetes Engine. Faremos isso utilizando o seguinte Dockerfile:

Não há muito segredo nas instruções necessárias para gerar a imagem da aplicação, mas você pode criar seu próprio arquivo Dockerfile com as instruções que atendem ao seu projeto.

Gere a imagem usando o comando:

docker build -t gcr.io/{{PATH_OF_YOUR_IMAGE}}:{{VERSION}} .

Onde:

  • PATH_OF_YOUR_IMAGE: é o caminho no qual você deseja salvar sua imagem dentro do Google Container Registry (uma vez que faremos o deploy no GKE). Caso você esteja utilizando outro provedor de Cloud (AWS, Azure, etc.), basta adaptar esse pedaço do comando.
  • VERSION: é a versão da imagem. Por exemplo: v0.0.1.

Em seguida, faça o upload da imagem no GCR:

docker push gcr.io/{{PATH_OF_YOUR_IMAGE}}:{{VERSION}}

Para finalizar, precisamos do manifesto que usaremos no deploy do serviço para o Kubernetes.

⚠️ Você precisa ter um cluster criado no GKE e autenticação já configurada para poder prosseguir com os próximos passos.

Crie um arquivo deploy.yaml no diretório k8s:

Em seguida, crie um namespace chamado greeting no cluster do Kubernetes:

kubectl create ns greeting

Certifique-se de que você tenha setado o parâmetro image do seu manifesto apontando para o caminho correto da imagem no GCR do seu projeto. Feito isso, aplique as configurações:

kubectl apply -f k8s/deploy.yaml

Você pode verificar se o seu micro serviço está rodando nos logs do pod, através dos comandos:

Testando o serviço

Nosso micro serviço está no ar! Como podemos testá-lo agora? Existem algumas maneiras de fazermos isso e apresentarei nesse artigo uma delas.

Como nossa aplicação está rodando em um cluster remoto, uma das opções é fazermos o port-forward (roteamento de porta) do pod para a nossa máquina. Esse é o método que considero mais fácil, já que requer o mínimo de configurações na arquitetura do cluster. Que fique claro que esta não é a solução ideal para um ambiente de produção, onde você precisaria configurar um DNS e no mínimo um Load Balancer para rotear a carga de requisições externas para os pods dos seus micro serviços. Mas isto é assunto para um próximo artigo.

Para fazer esse roteamento, basta executar:

O que esse comando faz é: rotear a porta 50051 do pod onde está rodando o micro serviço para a porta 50051 local da nossa máquina.

Feito isso, podemos fazer uma requisição para o serviço através do grpcurl: um CLI bastante similar ao conhecido curl, mas que tem o propósito de interagir com servidores gRPC. Para macOS, você pode instalar essa ferramenta utilizando o próprio brew:

brew install grpcurl

Se você está utilizando outro SO, basta seguir os passos para instalação na documentação oficial.

Uma vez instalado, você está pronto para realizar requisições no seu serviço. No diretório do seu projeto, execute o comando:

Algumas informações importantes sobre os parâmetros que utilizamos nesse exemplo:

  • import-path: como não habilitamos reflection no nosso servidor, é necessário especificarmos o diretório onde nossos arquivos Protobuf estão localizados.
  • proto: o caminho relativo ao import-path do serviço RPC que faremos a requisição.
  • plaintext: necessário uma vez que não configuramos TLS.
  • d: necessário quando não enviamos uma requisição vazia.

Existem outras ferramentas muito interessantes, como o bloomrpc, que oferecem uma UI interativa para realizar requisições gRPC. Vale a pena conferir.

Conclusão

Chegamos ao fim! Espero que este artigo tenha servido como um guia no desenvolvimento do seu micro serviço gRPC e que tenha aberto sua mente para algumas boas práticas e novas possibilidades. Nós abordamos pontos desde a organização do projeto e concepção de um serviço até o deploy em uma plataforma Cloud.

Caso você tenha se interessado por esse conteúdo e seja apaixonado por tecnologia, a unico está contratando engenheir@s. Basta conferir as vagas aqui. 😄 🚀

Alexandre Miziara é engenheiro na unico.

--

--