HTTP, gRPC, and websocket on Google Cloud Run
When you run services on Google Cloud Run you don’t have the luxury of choosing your port or use multiple ports, and if your requirements are to expose multiple services HTTP, gRPC, and websocket you’re facing a big challenge which may force you choose other solutions and products.
It looks like it is possible to have all three protocols on the same port on Cloud Run with authentication.
TL;DR
You can find the complete implementation and the documentation on github.com/gabihodoroaga/http-grpc-websocket
The solution
The solution to make it work is to write your own HTTP/2 server (using h2c protocol) and instruct Cloud Run to send us all requests (even HTTP/1 ones over HTTP/2).
addr := resolveAddress()
listener, err := net.Listen("tcp", addr)
if err != nil {
logger.Sugar().Errorf("error listen tcp listen on %s, %v", addr, err)
os.Exit(1)
}
grpcServer := grpc.NewServer(appgrpc.ServerOptions()...)
pb.RegisterEchoServer(grpcServer, appgrpc.NewServer())
mixedHandler := newHTTPAndGRPCMux(r, grpcServer)
http2Server := &http2.Server{}
http1Server := &http.Server{Handler: h2c.NewHandler(mixedHandler, http2Server)}
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
go func(l net.Listener) {
logger.Sugar().Infof("httpServer: listen on address %s", addr)
if err := http1Server.Serve(l); err != nil && err != http.ErrServerClosed {
logger.Sugar().Errorf("http1Server exit with error %v", err)
os.Exit(1)
}
}(listener)
<-ctx.Done()
stop()
fmt.Println("httpServer: shutting down gracefully...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := http1Server.Shutdown(ctx); err != nil {
logger.Sugar().Errorf("forced to shutdown, error %v", err)
os.Exit(1)
}
fmt.Println("httpServer: server exiting...")
and the implementation for the mixed handler is
func newHTTPAndGRPCMux(httpHand http.Handler, grpcHandler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.ProtoMajor == 2 && strings.HasPrefix(r.Header.Get("content-type"), "application/grpc") {
grpcHandler.ServeHTTP(w, r)
return
}
httpHand.ServeHTTP(w, r)
})
}
For the websocket all you have to do is to handle the http request and accept the connection
func ServeHTTP(w http.ResponseWriter, r *http.Request) {
zap.L().Sugar().Debug("ws/server: received connect request")
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{})
if err != nil {
zap.L().Error("ws/server: error accept request", zap.Error(err))
return
}
defer c.Close(websocket.StatusNormalClosure, "websocket server exit")
handleRequest(r.Context(), c)
}
This post does not contain all the source code because is too big to show it here.
If you want to checkout the full source code and to deploy a working example in GCP follow these steps.
Clone the repository.
git clone https://github.com/gabihodoroaga/http-grpc-websocket.git
cd http-grpc-websocket
Set your variables.
PROJECT_ID=$(gcloud config list project --format='value(core.project)')
REGION=us-central1
Create a service account.
gcloud iam service-accounts create demo-grpc-sa \
--display-name "Demo service account for gRPC on Cloud Run"
Create a new key for this service account.
gcloud iam service-accounts keys create key.json \
--iam-account demo-grpc-sa@${PROJECT_ID}.iam.gserviceaccount.com
Build the image.
gcloud builds submit \
--tag gcr.io/$PROJECT_ID/grpcwebapp \
--project $PROJECT_ID .
Deploy the service.
gcloud run deploy grpcwebapp \
--image gcr.io/$PROJECT_ID/grpcwebapp \
--set-env-vars=AUTH_SERVICE_ACCOUNTS="demo-grpc-sa@${PROJECT_ID}.iam.gserviceaccount.com",AUTH_AUDIENCE=webapp \
--allow-unauthenticated \
--timeout=10m \
--project $PROJECT_ID \
--region $REGION
The timeout option here is important because for gRPC and websocket you need the connection to be always on. Keep in mind that for Cloud Run the maximum timeout is 60 minutes and you need to implement your clients to auto reconnect if the connection drops.
Get the public URL.
SERVICE_URL=$(gcloud run services describe grpcwebapp \
--platform managed --region $REGION --format 'value(status.url)')
echo $SERVICE_URL
SERVICE_HOST=$(echo "$SERVICE_URL" | awk -F/ '{print $3}')
echo $SERVICE_HOST
Test HTTPS.
curl -v $SERVICE_URL/ping
Test gRPC.
go run clients/grpc/main.go --server "$SERVICE_HOST:443" --key key.json --insecure=false
Test Websocket.
go run clients/ws/main.go --server "wss://$SERVICE_HOST/ws" --key key.json
Cleaning up
In order to remove all the resources created and avoid unnecessary charges, run the following commands:
gcloud run services delete grpcwebapp \
--project $PROJECT_ID \
--region $REGION
Thanks to / acknowledgments
This example was based on this post Serving gRPC+HTTP/2 from the same Cloud Run container written by Ahmet Alp Balkan (https://github.com/ahmetb).