Envoy had grpc-web support ealy on. It’s used in the official grpc-web
tutorial docs.
In the envoy
config
of this tutorial, both grpc & grpcWeb are exposed via a single listener port.
This is nice model when you have a production deployment in which some clients
(mobile apps) want to speak to the GRPC protocol directly, but web apps want to
go down to http/1.1 to work with browsers’ xhr or fetch requests.
In production, we use istio as our gateway, but we have always struggled to utilise one single port to proxy both GRPC and GRPC-Web requests, even though envoy - the underlying proxy supports it very well. I suppose it’s partially due to the lack of documentation out there and the GRPC-Web community is still relatively small.
As we adopted Istio & GRPC-Web quite early on, we used what some other bloggers suggested: using a sidecar to convert HTTP1.1 request to GRPC and use a separate gateway dedicated to GRPC. The solution roughly looks like this:
For grpc
gateway:31400 -> app
For grpc-web
gateway:443 -> pod sidecar -> app
This has a few issues:
- We’re adding an extra open port to outside world: more attacking surface
- The new grpc-only port doesn’t have TLS or needs a separate TLS config
- We added a sidecar pod for every grpc service that we want to expose as GRPC-Web: wasted resources when we don’t need service mesh
This is definitely not right when evnoy supports the use case so well, but why does it feels so difficult on Istio’s side?
I finally sat down today and look at this from scratch: We have an effective envoy config, so we just need to configure istio such that the gateway envoy of istio has the most important elements that supports both GRPC & GRPC-Web. Namely:
- on the listener config we need the filter envoy.filters.http.grpc_web
- on the clusters config we need the http2_protocol_options
And we definitely don’t need the sidecar for this: sidecar is for east-west traffic, not north-south traffic. So the filter needs to be on the gateway, not the sidecar.
Well, it turns out it’s still in the same
EnvoyFilter
documentation. We just need to 1) configure the filter on the gateway; 2) use
grpc-web as the protocol in the service. (I believe grpc works just as
well)
The solution Link to heading
gateway:443 -> app for both grpc & grpc-web
envoy filter
apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: gateway-grpc-web-filter
  namespace: istio-system
spec:
  workloadSelector:
    labels:
      istio: ingressgateway
  configPatches:
    - applyTo: HTTP_FILTER
      match:
	  	# importantly we're patching to the GATEWAY envoy, not sidecar
        context: GATEWAY
        listener:
          filterChain:
            filter:
              name: "envoy.filters.network.http_connection_manager"
              subFilter:
				# apply the patch before the cors filter, just like the one in
				# grpc-web example
                name: "envoy.filters.http.cors"
      patch:
        operation: INSERT_BEFORE
        value:
          name: envoy.filters.http.grpc_web
virtual service
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: hello-grpc
  namespace: default
spec:
  hosts:
    - "*"
  gateways:
    - httpbin-gateway
  http:
    - route:
        - destination:
            host: hello-grpc
            port:
              number: 12345
      match:
        - uri:
            prefix: /main.HelloService/
And importantly the service needs to have grpc in the service name or use
appProtocol:
ref
apiVersion: v1
kind: Service
metadata:
  name: hello-grpc
  namespace: default
spec:
  selector:
    app: hello-grpc
  type: LoadBalancer
  ports:
    - name: grpc # name has to start with grpc, alternatively use appProtocol below
      port: 12345
      # or use appProtocol for Kubernetes 1.18+
      # appProtocol: grpc
Full example on github. There are some useful debug commands that helped.