Usando Protobuf para criar APIs com múltiplos protocolos (gRPC + REST)
Com o constante crescimento da Hash, tornou-se necessário o desenvolvimento de novas ferramentas para facilitar as integrações entre nossos parceiros e nossos produtos. Com base nesse contexto, desenvolvemos uma plataforma white label de Aquisição. Esse produto possibilita a entrada de novos clientes e a compra de terminais PoS (Point of Sale) assim como em um e-commerce.
Aqui na Hash, serviços pertencentes à nossa service mesh se comunicam por padrão utilizando gRPC. No entanto, no serviço de Aquisição tivemos a necessidade de também expô-lo utilizando outra forma de transporte: a HTTP/1. Ela facilita a integração com esse serviço com parceiros ou mesmo com aplicações onde a implementação de um cliente gRPC não é viável ou conveniente. Nesse cenário, acabamos por estudar diferentes maneiras de disponibilizar essa API utilizando mais de um protocolo de transporte.
Como sugestão de implementação de diferentes interfaces em uma mesma aplicação, podemos citar a criação de diferentes handlers (gRPC e REST) de forma separada onde cada um possui um método para tratar os fluxos das requisições, como por exemplo:
REST
gRPC
A abordagem acima é totalmente válida e muito utilizada, porém, para expor um novo endpoint é preciso escrever os handlers para cada protocolo, tornando-se uma tarefa trabalhosa dependendo de sua complexidade.
Após pesquisar por alternativas para contornar este desconforto e tornar o desenvolvimento mais ágil, encontramos a biblioteca gRPC Gateway. Ela é um plug-in que, com base nas definições do Protocol Buffers (protobuf), gera um proxy reverso que converte uma API RESTful HTTP em gRPC.
Na integração com o gRPC Gateway, implementamos os contratos das APIs através do gRPC e de arquivos Protobuf. Assim, compilamos os arquivos com o objetivo de gerar os stubs que são utilizados nos handlers gRPC e REST por meio de um único método por endpoint. No tópico a seguir falaremos mais sobre como utilizamos o gRPC Gateway para obter o mesmo comportamento da abordagem citada acima onde criamos diferentes handlers.
gRPC + HTTP utilizando gRPC Gateway
Como estamos utilizando os mesmos arquivos protobuf para gerar o contrato das APIs, garantimos que todas as nossas interfaces possuem os mesmos contratos. Outra vantagem que temos é gerar as documentações das APIs REST utilizando o Swagger, pois ele possui integração para gerar as documentações com base em um arquivo protobuf.
Com a implementação do gRPC Gateway conseguimos expor a mesma API usando dois tipos de transporte diferentes mas com uma única fonte da verdade uma vez que disponibilizamos nosso serviço em gRPC para integrações internas e também disponibilizamos uma API REST para integrações externas. Assim, garantimos facilidade na manutenção e no desenvolvimento constante de novas features. Segue aqui um exemplo de como seria introduzida essa integração em uma API gRPC:
No proto teríamos que adicionar uma anotação gRPC dizendo qual o path HTTP que aquela chamada RPC corresponde:
Já no main de nossa aplicação, temos que dar start no servidor http:
Trade-offs
Com a proposta de solução na mesa, começamos a atacar os trade-offs desse caminho que no final somaram um saldo positivo o suficiente para não abandonarmos o gRPC Gateway. Procuramos alguns projetos que já utilizam essa biblioteca ou tiveram experiências com ela. Encontramos alguns e conseguimos entender bastante onde o gRPC Gateway ajudou-os e os cuidados que eles tomaram ao usar essa ferramenta. Os mais relevantes que podemos comentar seriam o etcd, que usa essa ferramenta, e, em contrapartida, usamos para comparação o dex que deixou de usar essa ferramenta. Reunimos todos os pontos que colhemos e disso levantamos os trade-offs que iriam afetar nossos projetos:
O primeiro ponto notável que discutimos foi como compilar os Protobufs em um cenário com múltiplos ambientes distintos (Linux flavors, MacOSs e Windows). Acabamos por optar pelo uso de uma imagem Docker, facilitando assim a reprodutibilidade nos diferentes ambientes.
Nesse script definimos onde está a pasta contendo os protocol buff e suas respectivas saídas, contendo os arquivos compilados e as configurações do swagger.
Um segundo ponto que colabora com a manutenção do projeto no longo prazo foi a definição da estrutura de pasta e separação de responsabilidades. Nela visamos isolar as responsabilidades e ter consistência para facilitar o desenvolvimento.
Nessa estrutura temos alguns pontos de destaque:
- mocks: Responsável por conter todos os mocks utilizados nos testes do sistema;
- internal/endpoints: Responsável por conter a implementação do handler gRPC gateway e realizar a abstração do protobuf passando adiante as entidades, validação dos dados de input;
- internal/store: Responsável pela interação com a camada de banco de dados;
- internal/services: Responsável por conter todas as regras de negócio da aplicação;
A estrutura definida, baseada no go-kit, nos obrigou a pensar numa estrutura mais robusta de mocks, pois as interfaces estão espalhadas em arquivos. Nossa estrutura tem como foco manter a nossa árvore de teste o mais saudável possível. Ou seja, garantir que tenhamos uma boa cobertura de testes de unidade, seguido de testes de integração. Para isso utilizamos o Mockery, que facilita a criação dos mocks, auxiliando na manutenção dos testes de unidade. Assim como no processo de compilação dos arquivos Protobuf, utilizamos uma imagem docker para criar os mocks de todas as interfaces de uma única vez.
Próximos passos
Tendo a especificação da API no arquivo protobuf e utilizando o gRPC gateway, adicionamos uma camada REST ao nosso serviço, ampliando as opções de integração por entidades externas. Isso gerou uma boa redução na duplicidade de código, o que favorece a manutenção e futuras adições novas funcionalidades a nossas aplicações.
Estamos empolgados com o futuro das decisões que tomamos ao longo desse projeto e estaremos de olho pra trazer outros desafios para esse canal. Nosso time está sempre aberto para receber pessoas que também se empolgam com desafios técnicos, por isso, dê uma olhada nas nossas vagas e nos nossos valores do time de tecnologia.
Autoria
Renan Palmeira, Ivan Micai, Medson Mendes, Deepak Vashist