How to Setup a gRPC-Web Backend on Google Cloud Run with Envoy Proxy
So far we used Cloud Run at Ackee only for smaller projects with REST backends, but since January 2021 the situation changed as Google added support for gRPC streams - it became a serious option for gRPC.
A great starting point is this example where you can find everything you need to set up a gRPC server on Cloud Run... but everything? Only if you don't plan to use web clients. As current browsers don't fully support HTTP/2 gRPC spec there must be a backend that provides gRPC-web - a standard usable by current browsers. And this is something that Cloud Run doesn't provide, so we need to create own proxy.
We use envoy for that - a bit too robust solution, but so far I'm not aware of anything better for a production environment. Envoy is deployed to Cloud Run as a service that provides HTTP/1 endpoint for gRPC-web clients and proxies traffic to gRPC services (such as Calculator from the example above). A nice thing about Cloud Run is that it runs out of the box, but until things go wrong... and when I tried to implement envoy things went wrong. The problem is that there are not enough logs from the load balancer of Cloud Run that is responsible for TLS negotiation and envoy failed exactly on this.
After hours I solved this issue so I will share my configuration as I haven't found any other working example.
We have this Dockerfile container with envoy
FROM envoyproxy/envoy-alpine:v1.17.0
COPY envoy.yaml /etc/envoy/envoy.yaml
RUN apk --no-cache add ca-certificates
And now the whole envoy.yaml config file
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
cluster: api-gateway-proxy
auto_host_rewrite: true
max_stream_duration:
grpc_timeout_header_max: 0s
cors:
allow_origin_string_match:
- prefix: "*"
allow_methods: GET, PUT, DELETE, POST, OPTIONS
allow_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
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
http_filters:
- name: envoy.filters.http.grpc_web
- name: envoy.filters.http.cors
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: api-gateway-proxy
type: strict_dns
connect_timeout: 20s
http2_protocol_options: {}
lb_policy: round_robin
dns_refresh_rate: 90s
load_assignment:
cluster_name: api-gateway-proxy
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: YOUR_GRPC_APP_URL.a.run.app
port_value: 443
dns_lookup_family: V4_ONLY
transport_socket:
name: envoy.transport_sockets.tls
typed_config:
"@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext
common_tls_context:
alpn_protocols: h2
validation_context:
trusted_ca:
filename: /etc/ssl/certs/ca-certificates.crt
sni: YOUR_GRPC_APP_URL.a.run.app
Replace YOUR_GRPC_APP_URL with an URL of your gRPC service (on Cloud Run or anywhere else) and deploy a container with envoy to Cloud Run. Then you should be able to start accepting gRPC-web connections from the new endpoint this newly deployed service creates.