How to Get a Browser Communicate with Containerd Using gRPC
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:
- 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…