Guest Writer

Implementing gRPC-Web with Emissary-ingress

Anjul Sahu
Ambassador Labs
Published in
8 min readAug 23, 2022

--

gRPC is a high-performance and low-latency Remote Procedure Call framework created by Google. gRPC-Web was built on the foundation of the gRPC protocol to provide a high-performance architecture to implement services natively in the browser, using HTTP2 and gRPC to have a bi-directional stream and efficient multiplexing.

Envoy proxy is an important element when working with gRPC or gRPC-web because it helps to translate the gRPC Web calls over HTTP, and the impact of this on performance is phenomenal.

In this article, I’ll show you how to implement gRPC-Web with Emissary-ingress — an Envoy-based API gateway and Ingress controller.

gRPC in Kubernetes using Emissary-ingress

To understand how gRPC-Web works, we will set up a Greeter service demo that uses gRPC protocol to respond to the Hello request. This service will be deployed on a Kubernetes cluster and exposed using the Emissary-ingress API gateway.

I’ll be using Minikube to create a Kubernetes cluster, so if you don’t have a Kubernetes cluster yet, you can create one by following this guide. After setting up the Kubernetes cluster on your computer using Minikube, start the cluster using the minikube start command.

Minikube

The next thing we are going to do is install Emissary-ingress and it's custom CRDs (Mappings, Listeners, Hosts) by running the command below on our minikube cluster.

# Add the Repo:
helm repo add datawire https://app.getambassador.io
helm repo update

# Create Namespace and Install:
kubectl create namespace emissary && \
kubectl apply -f https://app.getambassador.io/yaml/emissary/2.2.2/emissary-crds.yaml
kubectl wait --timeout=90s --for=condition=available deployment emissary-apiext -n emissary-system
helm install emissary-ingress --namespace emissary datawire/emissary-ingress && \
kubectl -n emissary wait --for condition=available --timeout=90s deploy -lapp.kubernetes.io/instance=emissary-ingress

To route the traffic from the edge, we will create an HTTP listener on port 8080 by applying the YAML configuration below on our Kubernetes cluster.

kubectl apply -f - <<EOF
---
apiVersion: getambassador.io/v3alpha1
kind: Listener
metadata:
name: emissary-ingress-listener-8080
namespace: emissary
spec:
port: 8080
protocol: HTTP
securityModel: XFP
hostBinding:
namespace:
from: ALL
EOF

Hands-on with Emissary-ingress and gRPC-web

The Greeter service structure will be defined by a greeter.proto file using Protocol Buffers — a language-neutral, platform-neutral extensible mechanism for serializing structured data. So create a file named greeter.proto and add the contents below to it.

syntax = "proto3";
package greeter;
service Greeter {
rpc Hello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}

As you can see, the greeter.proto file has a gRPC service definition for the Greeter service called Hello, which will access a HelloRequest message and return a HelloReply message.

I’ve also added a diagrammatical representation of the flow of the greeter.proto service to give you a better perspective of how it works.

Generate the Stubs for the Service

Compiling the protobuf file with common.js style will generate the stubs needed for the gRPC service implementation. You can generate them using the protocol CLI.

protoc greeter.proto --js_out=import_style=commonjs:./ --grpc-web_out=import_style=commonjs,mode=grpcwebtext:./

The compilation will generate two stubs named greeter_pb.js and greeter_grpc_web_pb.js.

Implementing the gRPC Greeter Service

Now we can implement the gRPC service, which will have a function doHello that returns the message. First, create a file named server.js and add the contents below to it.

const PORT = 9090;
const PROTO_PATH = `${__dirname}/greeter.proto`;
const grpc = require("grpc");
const protoLoader = require("@grpc/proto-loader");
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition);
function doHello(call, callback) {
callback(null, {
message: 'Hello! ' + call.request.name
});
}
function getGrpcServer() {
const server = new grpc.Server();
server.addService(protoDescriptor.greeter.Greeter.service, {
Hello: doHello
});
return server;
}
if (require.main === module) {
var server = getGrpcServer();
server.bind(`0.0.0.0:${PORT}`, grpc.ServerCredentials.createInsecure());
server.start();
console.log(`Server started on port ${PORT}`);
}

Deploy the Greeter Service

Now, let’s containerize this application by creating a docker image. To do this, create a file named Dockerfile in the same directory of the server.js file and paste the content below into it.

FROM node:14.19
WORKDIR /app
COPY ["package.json", "server.js", "./"]
RUN npm install --no-cache --production
COPY . .
EXPOSE 9090
CMD [ "node", "server.js" ]

After creating the Dockerfile, go back to your Kubernetes cluster and run the command below to create a running container based on the docker image you created in the previous step.

docker build . -t grpc-server:1.0 

After doing this, you can then push it to DockerHub or Minikube. This is optional, as you can still go ahead with the article without completing this step.

# optional, push the image to minikube
minikube image load grpc-server:1.0

The next thing we are going to do is deploy the Greeter service. To do this, create a file named deploy.yaml and paste the content below into it.

apiVersion: apps/v1
kind: Deployment
metadata:
name: greeter
spec:
replicas: 1
selector:
matchLabels:
app: greeter
template:
metadata:
labels:
app: greeter
spec:
containers:
- name: greeter
image: grpc-server:1.0
ports:
- containerPort: 9090
name: http
protocol: TCP
---
kind: Service
apiVersion: v1
metadata:
name: greeter
spec:
selector:
app: greeter
type: ClusterIP
ports:
- name: http
port: 9090
targetPort: 9090
---

Then go to your Kubernetes cluster and run this command kubectl apply -f deploy.yaml to apply/create the Kubernetes resources inside your cluster.

Configure Emissary-ingress resources to expose the Greeter Service

Let’s create Module, Mapping, and Host resources to expose the Greeter service. After reading the content of each section, copy the YAML content into a file name of your choice and apply them to your Kubernetes cluster using the kubectl apply -f filename.yaml command.

Module

A module defines the system-wide configuration of Emissary-Ingress. To use the ambassador module, it must be named as ambassador. There are two items in the ambassador module named module — enable_grpc_web and enable_grpc_http11_bridge. Both must be set to true to enable the gRPC Web bridge.

This bridge translates the HTTP/1.1 calls with the header Content-Type: application/grpc to gRPC calls. You can find more details about this in the official documentation.

apiVersion: getambassador.io/v2
kind: Module
metadata:
name: ambassador
spec:
# Use ambassador_id only if you are using multiple instances of Emissary-ingress in the same cluster.
# See below for more information.
config:
# Use the items below for config fields
use_proxy_proto: true
enable_grpc_web: true
enable_grpc_http11_bridge: true
diagnostics:
enabled: true

Mapping

The ambassador Mapping resource can be used to map a resource to a service. Here the /greeter.Greeter/would route to greeter service on port 9090 using gRPC. We have defined other things, particularly headers, to enable it for gRPC Web. More details in the reference.

apiVersion: getambassador.io/v3alpha1
kind: Mapping
metadata:
name: grpc-greeter
spec:
hostname: "*"
grpc: True
prefix: /greeter.Greeter/
rewrite: /greeter.Greeter/
service: greeter.default:9090
timeout_ms: 4000
idle_timeout_ms: 500000
connect_timeout_ms: 2000
bypass_auth: true
dns_type: logical_dns
cors:
origins:
- "*"
methods:
- POST
- GET
- OPTIONS
- PUT
- DELETE
headers:
- keep-alive
- user-agent
- cache-control
- content-type
- content-transfer-encoding
- custom-header-1
- x-accept-content-transfer-encoding
- x-accept-response-streaming
- x-user-agent
- x-grpc-web
- grpc-timeout
exposed_headers:
- custom-header-1
- grpc-status
- grpc-message

Host

A Host resource describes how you want Emissary-Ingress to look externally. The YAML configuration file of a Host resource usually contains a hostname, how to handle insecure connections, the TLS configuration, and the Mappings linked to the Host.

apiVersion: getambassador.io/v3alpha1
kind: Host
metadata:
name: example-host
spec:
hostname: localhost
acmeProvider:
authority: none
requestPolicy:
insecure:
action: Route

Deploying Client Side Stubs

At this point, you should have a gRPC server running in the Kubernetes cluster and exposed via Emissary-Ingress. We will now deploy the client-side stubs we created earlier using the Protobuf compiler in common.js format.

The next thing we need to do is import the stubs and call the Greeter service. To do this, create a file namedclient.js and add the content below.

const  { HelloRequest, HelloReply} = require('./greeter_pb.js');
const { GreeterClient } = require('./greeter_grpc_web_pb.js');
const client = new GreeterClient('http://localhost');
// console.log(client)
var request = new HelloRequest();
request.setName('Ambassador!');
client.hello(request, {}, (err, response) => {
console.log(response.getMessage());
});

I have created a tunnel using the kubectl port-forward command to reach the Emissary-Ingress service running inside the Minikube cluster on local port 80.

Now, let’s package our client’s code and generate a distribution to run in the browser using a Webpack. Run the below commands in your terminal to generate the distribution.

npm install 
npx webpack client.js

This will generate ./dist/main.js distribution, which we will call on the web page. Next, create a file named index.html and add the content below to return a sample web page.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>gRPC-Web Example</title>
<script src="./dist/main.js"></script>
</head>
<body>
<p>open developer console of the browser.</p>
</body>
</html>

At this point, we should host the script and static page in some webserver. We will use python’s HTTP server module in this demo to start the webserver at port 8081.

python3 -m http.server 8081 &

Now open the browser and point it to http://localhost:8081. This will load the page below and call the main.js client-side javascript. You can see the response in the console coming from the gRPC service.

Conclusion

To recap, we created a Greeter gRPC service that greets you — it uses gRPC end-to-end, from the browser to the Kubernetes service via the Envoy proxy-based API gateway, Emissary-Ingress. Here’s a GitHub repository containing everything we’ve done so far.

Thanks for reading. If you have any questions or concerns, do well to leave a comment on this article, and I’ll respond to it as soon as I can.

Are you looking for the right tool to route traffic to your Kubernetes services?

Routing traffic into your Kubernetes cluster requires modern traffic management. And that’s why we built Ambassador Edge Stack (the commercial version of Emissary Ingress) to contain a modern Kubernetes ingress controller that supports a broad range of protocols, including HTTP/3, gRPC, gRPC-Web, and TLS termination.

Try Ambassador Edge Stack today or learn more about Ambassador Edge Stack.

--

--

CEO, Cloudraft (www.cloudraft.io) . Writes a newsletter Cloud-Native Weekly (anjulsahu.substack.com) and supports the community through mentoring.