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.