Contents
How to use gRPC in Elixir
In today’s post, we’ll learn what gRPC is, when you should reach for such a tool, and some of the pros and cons of using it. After going over an introduction of gRPC, we’ll dive right into a sample application where we’ll build an Elixir backend API powered by gRPC.
What is gRPC and how does it work?
gRPC is a framework used to enable a remote procedure call (RPC) style of communication. RPC is a style of system communication where a client can directly invoke exposed methods on a server. From the client’s perspective, it feels no different than making a call to a local function or method as long as you provide the applicable parameters and handle the return type appropriately. gRPC facilitates this communication by providing 2 things for you:
- A client library which can be used to invoke allowed procedures
- A consistent data serialization standard via Protocol Buffers
Let’s break these two items down so we can appreciate the inner workings gRPC. When creating a gRPC server, you will have to define what procedures are invokable from the client, what inputs they accept, and what outputs they return. This interface specification is what allows client libraries (generally called gRPC stubs) to be automatically generated for various languages and runtimes as the contract for the remote procedure call is explicitly defined. This gRPC client library can then communicate with the server using Protocol Buffers. Protocol Buffers provide a mechanism for serializing and deserializing the payloads (both request and response) so that you can operate on the data with types native to your language. The diagram available in the gRPC documentation can help visualize this interaction [1]:
One bit that we haven’t yet discussed is how the data is transmitted between the client and the server. For this, gRPC leans on the HTTP/2 protocol. By using HTTP/2 as the underlying protocol, gRPC is able to support features such as bi-directional data streaming and several other features which are not available in HTTP/1.1.
When would you use gRPC over REST/GraphQL?
The obvious question that may come to mind is: “How does gRPC compare to REST/GraphQL and when do I used one over the other?”. In general, if you plan to use gRPC for a frontend application, there are a couple of caveats that you need to keep in mind. In order to serialize and deserialize Protocol Buffer payloads from your Javascript application, you’ll need to leverage grpc-web [2] in order to do that. In addition, you’ll also need to run a proxy on the backend (the default supported gRPC proxy is Envoy) since browsers cannot talk directly to gRPC servers. Depending on your resources and time constraints, this may be a show stopper, in which case REST and GraphQL will do just fine.
If frontend application communication is not a requirement, and instead what you require is inter-microservice communication from within your service cluster, then the barrier to entry for gRPC gets lowered considerably. At the time of writing, gRPC currently has client libraries and tooling for most mainstream languages including Elixir, Python, C++, Go, and Ruby to name a few. I would argue that the barrier to entry for consuming a RESTful API is still much lower than consuming a gRPC service given that all you need for the former is an HTTP client, which is baked into most languages and runtimes these days.
On the other hand, if you are willing to make the investment, you do get the added benefits of having your responses and requests checked against the Protocol Buffer specification that is used for code generation. This in turn provides you with some guarantees as to what you can expect on the client side and of the server side. This type guarantee and introspection is also something that you get with GraphQL when you define your server schemas. An added benefit with GraphQL over gRPC is that you are able to dynamically request embedded properties from within your schema depending on the query that you make to your backend server.
Like most things in the software engineering field, what technology you choose will largely depend on your application. Below are my personal TL;DR rules of thumb regarding the various technologies:
gRPC:
- Use: When communicating between microservices in my service cluster or if performance is a requirement
- Don’t use: When I need to transmit data from the browser to the backend
GraphQL:
- Use: When I need to aggregate data from multiple microservices for the purposes of streamlining frontend development or if my frontend data requirements are dynamic
- Don’t use: When communicating between microservices in my service cluster or if my API needs to be used by the lowest common denominator of consumers
REST:
- Use: When I need to put something together quickly or if I need to cater to the lowest common denominator of consumers
- Don’t use: If I require any kind of type checking or if I want to reduce payload sizes over the wire
Hands on project
With all the theory out of the way, it’s time to get our hands dirty and experiment with standing up a gRPC server. In order to keep us focused on the gRPC experience, we’ll opt for having a backend powered by an Agent versus an actual database, but all of the concepts should be easily transferable to an application backed by Postgres for example. Our gRPC application will be a simple user management service where we can create and fetch users. After we create our Elixir service, we’ll interact with it via grpcurl, which is effectively cURL…but for gRPC. With all that being said, let’s dive right in!
Before creating our Elixir project, there are a couple of things that we require on our machine in order to properly
develop and test our application. We’ll first need to install protoc
so that .proto
files can be compiled
appropriately. If you are on an OSX machine, you can run brew install protobuf
or see [3] for instructions specific to
your platform. With protoc
available on your machine now, you’ll also want to install grpcurl
so that we can
interact with our application. Once again, if you are on an OSX machine you can run brew install grpcurl
or see [4]
for instructions specific to your platform.
Lastly you’ll want to run mix escript.install hex protobuf
and ensure that protoc-gen-elixir
script is available on
your path (if you use ASDF as your runtime version manager this requires running asdf reshim elixir
). With all that
boilerplate done, you can run mix new sample_app --sup
to get a new application started.
Once inside your sample application directory, you’ll want to update your mix.exs
file to include our gRPC related
dependencies. For this application we will be leveraging https://github.com/elixir-grpc/grpc and
https://github.com/tony612/protobuf-elixir. In order to bring in these two dependencies into your project, ensure that
you deps/0
function looks like this:
defp deps do
[
{:grpc, "~> 0.5.0-beta"},
{:cowlib, "~> 2.8.0", hex: :grpc_cowlib, override: true}
]
end
With that in place, run mix deps.get
from the terminal in order to pull down all of the necessary project
dependencies. Next you’ll want to create a configuration file at config/config.exs
with the following contents:
use Mix.Config
# Configures Elixir's Logger
config :logger, :console, format: "$time $metadata[$level] $message\n"
config :grpc, start_server: true
Next, we’ll want to create the required Protocol Buffer definitions for our application. The Protocol Buffer
specification is fairly large [5] and we will only be using a small subset of it to keep things simple. Create a file
sample_app.proto
at the root of your project with the following contents:
syntax = "proto3";
package sample_app;
service User {
rpc Create (CreateRequest) returns (UserReply) {}
rpc Get (GetRequest) returns (UserReply) {}
}
message UserReply {
int32 id = 1;
string first_name = 2;
string last_name = 3;
int32 age = 4;
}
message CreateRequest {
string first_name = 1;
string last_name = 2;
int32 age = 3;
}
message GetRequest {
int32 id = 1;
}
As you can see our Protocol Buffer definition is fairly straightforward and easy to read. We define a service that
exposes two RPC methods (those being Create
and Get
). We also define the types that each of those RPC calls takes as
input and returns as a result. With the sample_app.proto
file in place, we’ll want to open up a terminal and run the
following:
$ protoc --elixir_out=plugins=grpc:./lib sample_app.proto
You’ll notice that this command produces a file lib/sample_app.pb.ex
with several modules within it. If you look
carefully, you’ll see that the code that was generated is the Elixir representation of the sample_app.proto
file that
we wrote. It contains all of the types that we defines along with the RPC method definitions. With our auto-generated
code in place, let’s get to work on the actual RPC handlers.
As previously mentioned, we’ll be using an Agent to persist state across gRPC calls instead of a database for the sake
of simplicity. Our agent will have the ability to lookup users via their ID, and will also be able to create new users.
Create a file lib/user_db.ex
with the following contents to provide that functionality:
defmodule UserDB do
use Agent
def start_link(_) do
Agent.start_link(
fn ->
{%{}, 1}
end,
name: __MODULE__
)
end
def add_user(user) do
Agent.get_and_update(__MODULE__, fn {users_map, next_id} ->
updated_users_map = Map.put(users_map, next_id, user)
{Map.put(user, :id, next_id), {updated_users_map, next_id + 1}}
end)
end
def get_user(id) do
Agent.get(__MODULE__, fn {users_map, _next_id} ->
Map.get(users_map, id)
end)
end
end
With that in place, we can create our RPC handlers for creating and getting users. Create a file lib/sample_app.ex
with the following contents:
defmodule SampleApp.Endpoint do
use GRPC.Endpoint
intercept GRPC.Logger.Server
run SampleApp.User.Server
end
defmodule SampleApp.User.Server do
use GRPC.Server, service: SampleApp.User.Service
def create(request, _stream) do
new_user =
UserDB.add_user(%{
first_name: request.first_name,
last_name: request.last_name,
age: request.age
})
SampleApp.UserReply.new(new_user)
end
def get(request, _stream) do
user = UserDB.get_user(request.id)
if user == nil do
raise GRPC.RPCError, status: :not_found
else
SampleApp.UserReply.new(user)
end
end
end
Our file defines two modules in this case. The SampleApp.Endpoint
module defines the gRPC server and provides the
handler module to service requests. The SampleApp.User.Server
module contains the actual implementations of the two
RPC calls that we defined. You’ll notice that for each of the handlers we provide the correct return type (as defined in
our Protocol Buffer file). When we encounter an error (in this case looking up a user that does not exist), we raise a
GRPC.RPCError
with the appropriate status code.
All that is left now is to start up our Agent and our gRPC server and we’re good to go. Open up
lib/sample_app/application.ex
and ensure that your process children
list looks like so:
children = [
UserDB,
{GRPC.Server.Supervisor, {SampleApp.Endpoint, 50051}}
]
With that in place, you should be able to run mix grpc.server
from the terminal and start your gRPC server. In another
terminal session (and from within the project directory), you should be able to grpcurl
commands and interact with
your application:
$ grpcurl -plaintext -proto sample_app.proto -d '{"first_name": "Bob", "last_name": "Smith", "age": 40}' localhost:50051 sample_app.User.Create
{
"id": 1,
"firstName": "Bob",
"lastName": "Smith",
"age": 40
}
$ grpcurl -plaintext -proto sample_app.proto -d '{"id": 1}' localhost:50051 sample_app.User.Get
{
"firstName": "Bob",
"lastName": "Smith",
"age": 40
}
$ grpcurl -plaintext -proto sample_app.proto -d '{"id": 2}' localhost:50051 sample_app.User.Get
ERROR:
Code: NotFound
Message: Some requested entity (e.g., file or directory) was not found
Conclusion
Thanks for sticking with me to the end and hopefully you learned a thing or two about gRPC and how to go about using it within an Elixir application. If you would like to learn more about gRPC or any of the tools that I mentioned, I suggest going through the following resources:
- [1] https://grpc.io/docs/guides/
- [2] https://github.com/grpc/grpc-web
- [3] https://github.com/protocolbuffers/protobuf/blob/master/src/README.md
- [4] https://github.com/fullstorydev/grpcurl#installation
- [5] https://developers.google.com/protocol-buffers/docs/proto3
- [6] https://github.com/elixir-grpc/grpc
- [7] https://github.com/tony612/protobuf-elixir
comments powered by Disqus