How to Get a Browser Communicate with Containerd Using gRPC

Murat Kilic
6 min readJun 18, 2019

--

In the series about exploring containerd, after exercising communicating with containerd using Java, this time I wanted to dive into getting my browser talk to containerd client (not using the Java code from before). For this, I will use the containerd setup I had from previous exercises, with the examplectr namespace and the images I already pulled. I will try to get a list of all images inside the examplectr namespace by sending a gRPC request from the browser.

GRPC uses protocol buffers as language and toolset for defining RPC service and HTTP/2 for transport by default. Since browsers do not have support for any (Newer browser versions support HTTP/2 but do not expose it to be used by client side applications), we need a way to translate from browsers HTTP/1.1 requests to gRPC endpoints. gRPC-Web project aims to do that by providing a set of tools and libraries to be used by browser and then sent over to a Proxy which transcodes the request(HTTP/JSON to gRPC), then transfers to a gRPC server and when server responds, transcodes it back(gRPC to HTTP/JSON) and returns the response to the browser.

We start by following the instructions at gRPC-Web project site:

  1. If NodeJS is not installed we install that, npm and npx first. We need that to generate the grpc-web stubs and browser JavaScript files.
sudo yum install nodejs npm

After npm is installed we need to install npx-

npm install -g npx

2. Let’s get the grpc-web library

npm i grpc-web

3. If you don’t already have protoc installed, you will have to download it first from here

Assuming we already have protoc already , we need the protoc plugin for JS. Download the protoc-gen-grpc-web protoc plugin from here. Then we move it to /usr/local/bin

$ sudo mv ~/{Download Location}/protoc-gen-grpc-web-1.0.4-darwin-x86_64 \
/usr/local/bin/protoc-gen-grpc-web
$ chmod +x /usr/local/bin/protoc-gen-grpc-web

Now we can generate the Javascript files from containerd provided proto files:

protoc --proto_path=/tmp/containerd_protos  --js_out=import_style=commonjs:. --grpc-web_out=import_style=commonjs,mode=grpcwebtext:. /tmp/containerd_protos/github.com/containerd/containerd/api/services/images/v1/*.proto /tmp/containerd_protos/github.com/containerd/containerd/api/types/*.proto /tmp/containerd_protos/gogoproto/gogo.proto

4. We need a package.json file. This is needed for both client.js file. If we were defining our own gRPC service and implementing the server side, we would need the same package.json for this file as well.

{
“name”: “containerd-grpc-web”,
“version”: “0.1.0”,
“description”: “containerd gRPC-Web example”,
“devDependencies”: {
@grpc/proto-loader”: “⁰.3.0”,
“google-protobuf”: “³.6.1”,
“grpc”: “¹.15.0”,
“grpc-web”: “¹.0.0”,
“webpack”: “⁴.16.5”,
“webpack-cli”: “³.3.4”
}
}

5. Write client.js javascript code to send RPC from browser

const {ListImagesRequest, ListImagesResponse} = require(‘./github.com/containerd/containerd/api/services/images/v1/images_pb.js’);
const {ImagesClient} = require(‘./github.com/containerd/containerd/api/services/images/v1/images_grpc_web_pb.js’);
var client = new ImagesClient(‘http://{Our Server Public IP}:8080');var request = new ListImagesRequest();
request.setFiltersList([]);
// We need to set containerd-namespace header namespace we are inquiring so containerd will understand and execute the RPC properlyclient.list(request, {“containerd-namespace”:”examplectr”}, (err, response) => {
if (err != null ) {
console.log(“Error:”+err.message);
} else {
var imageDiv = document.getElementById("containerd_image_list");
imageDiv.innerHTML="Container Images List in examplectr namespace<ul>";
imageList=response.getImagesList();
for (var i = 0; i < imageList.length; i++) {
//console.log("Image:"+imageList[i].getName());
imageDiv.innerHTML+="<li>"+imageList[i].getName()+"</li>";
const {ListImagesRequest, ListImagesResponse} = require('./github.com/containerd/containerd/api/services/images/v1/images_pb.js');
const {ImagesClient} = require('./github.com/containerd/containerd/api/services/images/v1/images_grpc_web_pb.js');
var client = new ImagesClient('http:{Our Server Public IP}:8080');var request = new ListImagesRequest();
request.setFiltersList([]);
// We need to set containerd-namespace header namespace we are inquiring so containerd will understand and execute the RPC properlyclient.list(request, {"containerd-namespace":"examplectr"}, (err, response) => {
if (err != null ) {
console.log("Error:"+err.message);
} else {
var imageDiv = document.getElementById("containerd_image_list");
imageDiv.innerHTML="Container Images List in examplectr namespace<ul>";
imageList=response.getImagesList();
for (var i = 0; i < imageList.length; i++) {
//console.log("Image:"+imageList[i].getName());
imageDiv.innerHTML+="<li>"+imageList[i].getName()+"</li>";
}
imageDiv.innerHTML+="</ul>";
}
});

6. Let’s package the JS files

$ npm install
$ npx webpack client.js
……….
Entrypoint main [big] = main.js
[1] (webpack)/buildin/global.js 472 bytes {0} [built]
[2] ./gogoproto/gogo_pb.js 66.9 KiB {0} [built]
[3] ./github.com/containerd/containerd/api/services/images/v1/images_pb.js 63.7 KiB {0} [built]
[7] ./github.com/containerd/containerd/api/types/descriptor_pb.js 7.41 KiB {0} [built]
[8] ./client.js 796 bytes {0} [built]
[14] ./github.com/containerd/containerd/api/services/images/v1/images_grpc_web_pb.js 11.7 KiB {0} [built]
+ 10 hidden modules

At this point, webpack generates ‘main.js’ file under ‘dist’ directory

ls -l dist/
total 416
-rw-rw-r — . 1 centos centos 423781 Jun 18 20:10 main.js

7. Let’s create a simple index.html file to include this main.js file. As you can see it incorporates the JS file created. I added a empty div block here for the response from containerd.

<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8">
<title>containerd gRPC-Web Example</title>
<script src=”./dist/main.js”></script>
</head>
<body>
<div id=”containerd_image_list”></div>
</body>
</html>

8. Let’s start a simple HTTP server here for just serving the html and JS created above. Of course if you have an existing web server, you can add these files to a directory under document root and serve from there. I’m assuming there is python on the server, so it can function as a minimal HTTP server as

python2 -m SimpleHTTPServer 8081

We are telling our HTTP server to listen on port 8081. We can test this setup by going to

http://{Our Server Public IP}:8081/

We will be served the index.html and main.js file but fail on the List Request at this point. Chrome Network Trace shows this:

9. At this point we are ready to setup Envoy proxy which we will use as a gateway to containerd.

We need Envoy configuration file which I copied from grpc-web site and customized it to use containerd’s unix socket and also use as a reverse proxy to the simple HTTP server I started above.

$ sudo mkdir /etc/envoy
$ sudo vi /etc/envoy

Here is the content of the envoy.yaml file I used:

admin:
access_log_path: /tmp/admin_access.log
address:
socket_address: { address: 0.0.0.0, port_value: 9901 }
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.http_connection_manager
config:
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/containerd.services" }
route:
cluster: containerd_service
max_grpc_timeout: 0s
- match: { prefix: "/" }
route:
cluster: default_service
cors:
allow_origin:
- "*"
allow_methods: GET, PUT, DELETE, POST, OPTIONS
allow_headers: containerd-namespace,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
max_age: "1728000"
expose_headers: containerd-namespace,custom-header-1,grpc-status,grpc-message
enabled: true
http_filters:
- name: envoy.grpc_web
- name: envoy.cors
- name: envoy.router
clusters:
- name: containerd_service
connect_timeout: 0.25s
type: static
http2_protocol_options: {}
lb_policy: round_robin
hosts: [{"pipe": {"path": "/run/containerd/containerd.sock"}}]
- name: default_service
connect_timeout: 0.25s
type: static
lb_policy: round_robin
hosts: [{ socket_address: { address: 127.0.0.1, port_value: 8081 }}]

We can install Envoy here on our server, but to do it the easy way, I’ll just start a container from envoy image using containerd(another benefit of working with containerd !). We mount 2 directories /etc/envoy and /run/contaianerd to use the envoy.yaml configuration (default location) and the containerd unix socker file /run/containerd/containerd.sock

sudo /usr/local/bin/ctr -namespace examplectr image pull docker.io/envoyproxy/envoy:latest
sudo /usr/local/bin/ctr -namespace examplectr container create --net-host --mount type=bind,src=/run/containerd,dst=/run/containerd,options=rbind:ro --mount type=bind,src=/etc/envoy,dst=/etc/envoy,options=rbind:ro docker.io/envoyproxy/envoy:latest myenvoy
sudo /usr/local/bin/ctr -namespace examplectr task start myenvoy

This starts the container, with the initial command running envoy .

[2019–06–18 21:08:43.909][1][info][main] [source/server/server.cc:205] initializing epoch 0 (hot restart version=10.200.16384.127.options=capacity=16384, num_slots=8209 hash=228984379728933363 size=2654312)
[2019–06–18 21:08:43.909][1][info][main] [source/server/server.cc:207] statically linked extensions:
[2019–06–18 21:08:43.909][1][info][main] [source/server/server.cc:209] access_loggers: envoy.file_access_log,envoy.http_grpc_access_log
……
[2019-06-18 21:08:43.920][1][info][config] [source/server/listener_manager_impl.cc:1006] all dependencies initialized. starting workers
[2019-06-18 21:08:43.920][1][info][main] [source/server/server.cc:478] starting main dispatch loop

At this point our Envoy proxy is ready and we can try it.

Let’s go to the browser page again hit out server, this time on port 8080 which Envoy is listening to. If this setup is successful, we should see the list of images inside the examplectr namespace

http://{Our Server Public IP}:8080/

Voilà !

Happy Containerizing…

--

--

Murat Kilic

Tech enthusiast and leader. Love inspiring people to follow their dreams in tech. Coded all the way from BASIC to Go.