When GraphQL meets gRPC… 😍

GraphQL is a well-known language, used by many companies. gRPC is the (not so) new comer, and it has a key role to play in a micro services architecture with GraphQL on this side of course.

TL;DR

gRPC lacks maturity, but is a really promising language, and is the perfect companion for GraphQL in a micro-service architecture.

Mali is a great nodeJS framework to deal with gRPC. In particular, it supports interceptors (aka middlewares).

If you want to go fast and cross read this article, just fork the associated github project:


Context

At Aiptrade, we have a micro-service architecture, deployed on Kubernetes:

  • backend services discuss with each others via pub-sub,
  • and ONLY one frontend service to manage the API. Argh.

This frontend service has grown and is now fat, so it’s time to truncate it, and move to a Backend for Frontend (BFF) architecture.
At first, we tried GraphQL schema stitching, which sounds really awesome, but it comes with some problems: conflict management between types of the same name, heterogeneous error handling, and we were also worried about performance, since a graphQL request is parsed twice.
So we turned to gRPC. Very promising on paper, library available in several languages, with a typed contract similar to GraphQL, used for several years by Google internally, in short the Holy Grail! 🤩

I won’t explain GraphQL gRPC nor protobuffer, there are plenty of articles and the official documentation is good. So let’s dig into the project.

The sample project

Our PoC will allow us to create… well… blog posts as usual 😄, and is composed with:

  • a graphQL server, in front of the client as our unique BFF entry point. Its role is mainly to validate input data (required / optional fields, type, …) and filter output data. The validation here is important since required & optional fields have been dropped from proto3. It also acts as a client to the gRPC micro-service.
  • a gRPC server, to perform all functional operations. Here to create and list some blog posts.
  • those 2 micro-services will be hosted on k8, so we will also implement a Health check mechanism, in gRPC please!
Sample Architecture diagram with GraphQL & gRPC (cc cloudcraft.co)

Setup

For the GraphQL Server, apollo-server does the job perfectly. And it offers a very convenient graqhQL playground to run our queries.

For gRPC, We will use NodeJS for the server and client side, because gRPC libraries on NodeJS can dynamically generate the code at runtime, which is awesome to quickly do a PoC, unlike other libraries available on other platforms (Go, PHP, Java, Python,…), that require the protoc compiler to generate a stub at build time. If you prefer working with static generated code, here a really interesting article.

  • On the client side, the official gRPC client works like a charm. It can handle interceptors, but doesn’t support Promises. If you prefer working with async / await, grpc-caller is a good alternative.
  • On the server side, The official gRPC-node library doesn’t support interceptors, so we have chosen mali, a really simple and easy to use library for gRPC, which supports metadata and middlewares (aka interceptors).

To run the PoC:

git clone git@github.com:svengau/grpc-graphql-sample.git
cd post-api && npm i && npm start
cd graphql-api && npm i && npm start
🚀 Server ready at http://localhost:4000/graphql

and run the following query:

mutation {
addPost(data: { title: “helloooo” }) {
message
result { _id title body }
}
}

The wedding contract

The gRPC server exposes 2 proto3 contracts:

  • Post.proto, a sample contract to create and list blog posts.
  • Health.proto, a contract proposed by gRPC to check service Health from Kubernetes.

The proto3 contract, in the post.proto file:

message Post {
string _id = 1;
string title = 2;
string body = 3;
}
message Posts {
int32 page = 1;
int32 limit = 2;
int32 count = 3;
repeated Post nodes = 4;
}
message addPostRequest {
reserved 1; // _id
required string title = 2;
string body = 3;
}
message listPostRequest {
optional string page = 1 [default = 1];
optional string limit = 2;
optional string _id = 3;
}
service PostService {
rpc addPost (addPostRequest) returns (Post) {}
rpc listPosts (listPostRequest) returns (Posts) {}
}

And to serve the proto file, as simple as:

import Mali from "mali";
this.server = new Mali();
this.server.addService("[...]/Post.proto", "PostService");
this.server.use({ PostService: { addPost, listPost } })
this.server.start("0.0.0.0:50051");

Security

gRPC supports natively 2 mechanisms:

  • SSL/TLS: this will encrypt all the data exchanged between the client and the server, and works at the channel level.
  • Token-based authentication with Google: aka OAuth2 tokens, and must be used on an encrypted channel.

SSL/TLS is a must have since Google doesn’t allow any unencrypted connections with its services. We won’t use token-based authentication, but a simple mechanism based on an API key to illustrate how work interceptors. By the way, Google Cloud Endpoints uses also a similar mechanism to restrict access to their API.

SSL

To generate SSL certificates for CA, client and server, just launch in post-api:

src/cert/generate_using_openssl.sh

I’ve also put another sample script which uses certrap, a convenient tool provided by Foursquare.

Once generated, the certificates use is well-documented in the official gRPC site, basically:

const credentials = grpc.ServerCredentials.createSsl(
fs.readFileSync(__dirname + ‘/cert/ca.crt’),
[{
cert_chain: fs.readFileSync(__dirname + ‘/cert/server.crt’),
private_key: fs.readFileSync(__dirname + ‘/cert/server.key’)
}], true);
this.server.start("0.0.0.0:50051", credentials);

And on the client side:

const packageDefinition = protoLoader.loadSync(... + '/Post.proto');
const  proto = grpc.loadPackageDefinition(packageDefinition);
let credentials = grpc.credentials.createSsl(
fs.readFileSync(__dirname + ‘/../cert/ca.crt’),
fs.readFileSync(__dirname + ‘/../cert/client.key’),
fs.readFileSync(__dirname + ‘/../cert/client.crt’)
);
const options = {
"grpc.ssl_target_name_override": "localhost",
};
const client = proto.sample.PostService(host, credentials, options);

Small tip 💡: the certificates have been generated to use with localhost, so the option grpc.ssl_target_name_override allows us to reuse the same certificates with a remote gRPC server. Without this option, you have to generate new certificate with the right domain name.

Sample authorization mechanism using interceptors

Mali offers a great mechanism to intercept calls done to the API. You can intercept globally, at the service level, or at the operation level. In our case, Health service stays insecure, and we secure the PostService like this:

function auth(apiKey: string) {
return async function(ctx: any, next: any) {
const apiKeyProvided:string = ctx.request.get("x-api-key");
if (!apiKeyProvided || apiKeyProvided !== apiKey) {
throw new Error(‘invalid.apiKey’);
}
await next();
}
}
this.server = new Mali()
this.server.addService("[...]/Post.proto", "PostService");
this.server.use("PostService", auth("myapikey"));
this.server.use({ PostService: { addPost, listPost } });

On the client side, you can pass the API key, through metadata, globally:

import * as grpc from 'grpc';
const interceptorAuth:any = (options:any, nextCall:any) =>
new grpc.InterceptingCall(nextCall(options), {
start: function(metadata, listener, next) {
metadata.add(‘x-api-key’, API_KEY);
next(metadata, listener);
}
});
const client = new proto.sample.PostService(
host, credentials, {interceptors: [interceptorAuth]}
);

Or you can pass metadata at the operation level:

import * as grpc from 'grpc';
const metadata = new grpc.Metadata();
metadata.add(‘x-api-key’, API_KEY);
client.listPosts({page: 1}, metadata, (err:any, response:any) => {
console.log(‘Post list’, err, response);
});

Error handling

With grpc-node, you have to catch all exceptions, which could quickly become a pain …. Fortunately, Mali catches exceptions for you and sends back to the graphQL server as an error.

Error thrown by gRPC and send back using GraphQL API

Deployment on K8

Kubernetes doesn’t support gRPC health checks natively, but gRPC comes with:

grpc-health-probe needs to be configured to run with SSL and the command line is a bit long:

/bin/grpc_health_probe 
-tls
-tls-server-name localhost
-tls-ca-cert cert/ca.crt
-tls-client-cert cert/client.crt
-tls-client-key cert/client.key
-addr=:4000

So I’ve put it in a script calledbin/grpc-health-probe.sh

Once the Check service up and running, you just need to configure k8 with:

spec:
containers:
- name: server
image: "[YOUR-DOCKER-IMAGE]"
ports:
- containerPort: 4000
readinessProbe:
exec:
command: ["/usr/src/app/src/bin/grpc_health_probe.sh", ":4000"]
initialDelaySeconds: 5
livenessProbe:
exec:
command: ["/usr/src/app/src/bin/grpc_health_probe.sh", ":4000"]
initialDelaySeconds: 10

Conclusion

gRPC is not yet mature, but very promising, and a good complement to GraphQL:

  • GraphQL is in charge of setting up a contract with the outside world,
  • while gRPC is in charge of communication between micro-services within the company.

Not all gRPC libraries are at the same level, both in terms of documentation and code. Better reading the doc carefully (proto version, authentication, interceptor support) before starting with a library.
For Nodejs, server-side interceptors are sorely lacking in the official library.

Fortunately, Mali offers a very good alternative.

Retrieve the project on Github:

And Happy coding ! 😎

References