How to Use gRPC in Elixir

Posted by Alex Koutmos on Tuesday, March 31, 2020

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:

  1. A client library which can be used to invoke allowed procedures
  2. 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]:

gRPC Diagram

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:


comments powered by Disqus