This article was originally written on Medium
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 graphQL 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](mailto:git@github.com):svengau/grpc-graphql-sample.gitcd post-api && npm i && npm start
cd graphql-api && npm i && npm start🚀 Server ready at [http://localhost:4000/graphql](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:
- a sample proto contract to implement, with a Check method.
- the tool grpc-health-probe to install in your docker image, which will call the Check method.
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 and GraphQL work great together:
- 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:
svengau / grpc-graphql-sample
grpc / graphql sample project
gRPC / graphQL sample project
this PoC will allow us to create blog posts, 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.
To run the PoC, install NodeJs, Mongodb and Git, and launch the following commands:
git clone git@github.com:svengau/grpc-graphql-sample.git
cd post-api && npm start
cd graphql-api && npm start
🚀 Server ready at http://localhost:4000/graphql
and run the following query:
mutation {
addPost(data: { title: "helloooo" }) {
message
result { _id title body }
}
}
```
And Happy coding ! 😎
Top comments (0)