// css

Contents

gRPC and gRPC Web on Google Cloud Run (serverless)


Serverless is taking the world. The same goes for gRPC. This article will show you how to deploy gRPC/gRPC Web API to a serverless environment on Google Cloud.

gRPC is a project created by Google. 31k stars project. It uses binary messages defined in Protobuf files. The main advantages over standard REST API are performance, bandwidth, strong typing, SDK for more than ten programming languages. There are, of course, some downsides - the binary messages might be problematic to process, especially in Cloud environments. Moreover, as for 2021, it is impossible to call the gRPC API directly from the browser. That’s why we need gRPC Web.

gRPC Web is a project that aims to deliver a gRPC-like experience in a browser.

Before we start

In this article, you must be aware that we use the experimental HTTP/2 Google Cloud feature (as for 9-07-2021). This tutorial has several chapters:

  1. Defining gRPC
  2. Implementing the backend in Golang
  3. Implementing the frontend in Typescript and React
  4. Deployment to Google Cloud

The tutorial repository is available on our GitHub.
The API Documentation with SDK is available here. There is also an article about deploying the same app (but with gRPC Web interface only) to AWS.

In case of any problems with this tutorial, contact us at support@gendocu.com

API Definition

You can skip this step and use the generated SDK.

In this article, we will use a simple gRPC API without streaming.

//from https://github.com/gendocu-com-examples/library-app/blob/master/proto/service.proto
syntax = "proto3";
package gendocu.sdk_generator.v3;
// GenDocu automatically add language-specific options 
// - if you generate it manually, you need to add those to this file
import "google/protobuf/empty.proto";

// BookService is a simple service that enables listing, creating, or removing the book.
service BookService {
   // ListBooks returns the list of all books available in the library without any pagination.
   rpc ListBooks(google.protobuf.Empty) returns (ListBookResponse);
   // DeleteBook deletes the book from the library. Warning! This action cannot be revert and doesn't require any confirmation.
   rpc DeleteBook(DeleteBookRequest) returns (Book);
   // CreateBook creates a book in the library. We do not de-duplicate the requests as this is the tutorial API.
   rpc CreateBook(Book) returns (Book);
}

message DeleteBookRequest {
   oneof selector {
      string isbn = 1;
      string title = 2;
   }
}

message ListBookResponse {
   repeated Book books = 1;
}

message Book {
   string isbn = 1;
   string title = 2;
   Author author = 3;
}

message Author {
   string first_name = 1;
   string last_name = 2;
}

The next step is the SDK generation. You can do that using the protoc compiler or using a cloud service like GenDocu. In this tutorial, we will go with the second option. First, log in to GenDocu Console and create a new project. Select project from public repository and input https://github.com/gendocu-com-examples/library-app/. Select master branch and proto directory as proto root. Click ‘Create project’ and wait for a build to complete. After the build completion, go to documentation - you can select your technology and download the SDK.

Last chance to try CI/CD for gRPC

On 1.11.2021, our gRPC CI/CD is starting to be paid service. It is the last chance to try all the features, including SDK generation, RBAC access, code snippets, and more, for free.

Backend implementation in Golang

You can skip this step and use our gRPC backend library-app-grpc-uqnits2f5q-uc.a.run.app:443 or gRPC Web backend https://library-app-grpcweb-uqnits2f5q-ey.a.run.app.

In the previous step, you have generated SDK. If you don’t have one, you can use the cloud-hosted one.

It is possible to expose a single port that would accept both gRPC and gRPC Web connections. To simplify the tutorial, we will create two executables from a single implementation - one for gRPC and one for gRPC Web.

Let’s start with simple gRPC server.

// file: cmd/grpc/main.go
package main

import (
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
	"log"
	"net"
	"os"
)

func main() {
	log.Println("starting container")
	port := os.Getenv("PORT") // Google Cloud sets this variable - we should expose our app on this port
	if port == "" {
		port = "8070"
		log.Printf("Defaulting to port %s", port)
	}
	lis, err := net.Listen("tcp", ":"+port)
	if err != nil {
		log.Fatalf("Got error: %+v", err)
	}
	grpcServer := grpc.NewServer()
	reflection.Register(grpcServer) // We add the reflection to be able to list all the available methods
	if err := grpcServer.Serve(lis); err != nil {
		log.Println("got an error", err)
	}
}

You can run it with go run command. You can test it using gRPC Curl command: grpcurl -plaintext localhost:8070 list. You should see output like this:

grpc.reflection.v1alpha.ServerReflection

This server hasn’t got any methods. Let’s create a service struct that implements our gRPC API.

// file: pkg/dummyservice.go
package pkg

import (
	"context"
	"fmt"
	sdk "git.gendocu.com/gendocu/LibraryApp.git/sdk/go" // This is the SDK Generated with GenDocu - you can replace it with your SDK
	"github.com/golang/protobuf/ptypes/empty"
)

type DummyService struct {
	books []*sdk.Book
}

func NewDummyService() *DummyService {
	return &DummyService{
		books: []*sdk.Book{{
			Isbn:   "0-670-81302-8",
			Title:  "It",
			Author: &sdk.Author{
				FirstName: "Stephen",
				LastName: "King",
			},
		}},
	}
}
func (s *DummyService) ListBooks(ctx context.Context, empty *empty.Empty) (*sdk.ListBookResponse, error) {
	return &sdk.ListBookResponse{
		Books: s.books,
	}, nil
}

func (s *DummyService) DeleteBook(ctx context.Context, request *sdk.DeleteBookRequest) (*sdk.Book, error) {
	for ind, book := range s.books {
		if book.Isbn == request.GetIsbn() || book.Title == request.GetTitle(){
			//removing the book
			s.books[ind] = s.books[len(s.books)-1]
			s.books = s.books[:len(s.books)-1]
			return book, nil
		}
	}
	return nil, fmt.Errorf("book not found %+v", request)
}

func (s *DummyService) CreateBook(ctx context.Context, book *sdk.Book) (*sdk.Book, error) {
	s.books = append(s.books, book)
	return book, nil
}

Having this, we can add this service to our gRPC Server:

// file: cmd/grpc/main.go
   //...
	grpcServer := grpc.NewServer()
	srvc := pkg.NewDummyService() // <-- Create the Service
	sdk.RegisterBookServiceServer(grpcServer, srvc) // <-- Register the Service
	if err := grpcServer.Serve(lis); err != nil {
   //...

Now, we can list again available services grpcurl -plaintext localhost:8070 list:

gendocu.sdk_generator.v3.BookService
grpc.reflection.v1alpha.ServerReflection

And the methods grpcurl -plaintext localhost:8070 list gendocu.sdk_generator.v3.BookService:

gendocu.sdk_generator.v3.BookService.CreateBook
gendocu.sdk_generator.v3.BookService.DeleteBook
gendocu.sdk_generator.v3.BookService.ListBooks

To create a gRPC Web API, we can use the same service. Let’s start with creating another main file:

// file: cmd/grpcweb/main.go
package main

import (
	sdk "git.gendocu.com/gendocu/LibraryApp.git/sdk/go" // You can replace this sdk with your own for LibaryApp
	"github.com/gendocu-com-examples/library-app/backend/pkg"
	"github.com/improbable-eng/grpc-web/go/grpcweb"
	"google.golang.org/grpc"
	"net/http"
	"os"
)

func main() {
	port := "8083"
	if p := os.Getenv("PORT"); p != "" {
		port = p
	}
	grpcServer := grpc.NewServer()
	srvc := pkg.NewDummyService() // Instantiate our service
	sdk.RegisterBookServiceServer(grpcServer, srvc) // Register in the exactly the same way as in the standard gRPC server
	wrappedGrpc := grpcweb.WrapServer(grpcServer) // This is where it differs - we need to wrap the gRPC server wit gRPC Web server
	if err := http.ListenAndServe(":"+port, http.HandlerFunc(func(w http.ResponseWriter, req *http.Request){ 
		w.Header().Set("Access-Control-Allow-Origin", "*") // The CORS header to make it work also on our custom domain in the GCP environment
		w.Header().Set("Access-Control-Allow-Methods", "*")
		w.Header().Set("Access-Control-Allow-Headers", "*")
		wrappedGrpc.ServeHTTP(w, req) // gRPC Web server handling the request
	})); err != nil {
		panic(err)
	}
}

To test gRPC Web we need to implement the frontend. It is the same frontend as from our previous AWS article.

Frontend App in Typescript and React

You can use the Typescript snippet from the API documentation. You can skip this part and use the ready frontend app from our GitHub repository.

We will bootstrap our solution from the React template npx create-react-app my-app --template typescript. The next step is to replace the App.tsx file.

import React, {useEffect, useState} from 'react';

import {BookServiceClient} from "LibraryApp/sdk/ts/service_pb_service";
import {Empty} from "google-protobuf/google/protobuf/empty_pb";
import {DeleteBookRequest} from "LibraryApp/sdk/ts/service_pb";

var service0 = new BookServiceClient('http://localhost:8083')
function App(){
  const [msgs, setMsgs] = useState<string[]>([])
  useEffect(() => {
    const req1 = new Empty();

    service0.listBooks(req1, (err, value) => {
      const msg = `listBooks() response err:${JSON.stringify(err)}, msg:${value}`
      setMsgs(old => !!old ? [...old, msg] : [msg])
    })
    const req2 = new DeleteBookRequest();

    service0.deleteBook(req2, (err, value) => {
      const msg = `deleteBook() response err:${JSON.stringify(err)}, msg:${value}`
      setMsgs(old => !!old ? [...old, msg] : [msg])
    })
  }, [])
  return <>
    <ol>
      {msgs.map(el => <li>{el}</li>)}
    </ol>
  </>
}
export default App;

To make it work, we need to install the generated Typescript SDK:

npm install git+https://git.gendocu.com/gendocu/LibraryApp.git#fd131dbbf77e73de26aed805930d6c2615ece495

After that, we need to add only the gRPC dependencies to allow us to use the protobuf types

npm install @improbable-eng/grpc-web google-protobuf @types/google-protobuf

Voilà! We should be able to React app that communicates using gRPC Web. Run npm start to start the app.

Let’s add some form to add some books.

import {Book, Author} from "LibraryApp/sdk/ts/service_pb";
//...
  const addBook = () => {
    const req = new Book()
    req.setTitle(window.prompt("Title") || "")
    req.setIsbn(window.prompt("ISBN") || "")
    const author = new Author()
    author.setFirstName(window.prompt("Author's first name") || "")
    author.setLastName(window.prompt("Author's last name") || "")
    req.setAuthor(author)
    service0.createBook(req, (err, value) => {
    })
  }
return <>
    <ol>
        {msgs.map(el => <li>{el}</li>)}
    </ol>
    <input type="button" onClick={addBook} value="Add book"/>
</>

We also need to implement the book list functionality.

import {Empty} from "google-protobuf/google/protobuf/empty_pb";
//....
const listBooks = () => {
    service0.listBooks(new Empty(), (err, resp)=>{
        if(err !== null || resp === null) {
            console.error(err || "received empty response")
        } else {
            setBooks(resp.getBooksList())
        }
    })
}
useEffect(listBooks, [])
//...
return <>
    <ol>
        {books.map((el, key) => <li key={key}>{el.getTitle()} ({el.getIsbn()})</li>)}
    </ol>
    <input type="button" onClick={addBook} value="Add book"/>
</>

Now we can list all books, right after adding a new one:

  const addBook = () => {
    //.....
    service0.createBook(req, (err, value) => {
        listBooks()
    })
  }

You should be able to add a new book to our list. We can add “1984” with ISBN “9780151660346” by “George Orwell”.

Last but not least - deleting the book from the directory.

import {DeleteBookRequest} from "LibraryApp/sdk/ts/service_pb";
//...
const removeBook = (isbn:string) => {
    const req = new DeleteBookRequest()
    req.setIsbn(isbn)
    service0.deleteBook(req, (err, value)=>{
        if(err !== null || value === null){
            console.error("received error: ", (err || "got empty response"))
        }
        listBooks()
    })
}
//...
return <>
    <ol>
        {books.map((el, key) => <li key={key} onClick={() => removeBook(el.getIsbn())}>{el.getTitle()} ({el.getIsbn()})</li>)}
    </ol>
    <input type="button" onClick={addBook} value="Add book"/>
</>

Now, after clicking on the book title, we will remove it. It is not the best UX decision but should be fine for our example. Congratulations! You have now completely working local gRPC Web app. There is only one thing that we need to do to make it work with AWS Lambda backend - we need to obtain the backend address from the env variable:

var service0 = new BookServiceClient(process.env.REACT_APP_BACKEND || 'http://localhost:8003')

Deployment to Google Cloud Run

Google Cloud Run is a cloud environment that deploys the Docker images serverless manner - GCP starts your image on request and closes it shortly after. You pay for what you use. Moreover, it can scale those Docker instances horizontally almost infinitely.

The whole Google Cloud Platform has a great CLI tool - gcloud. We will use it in this tutorial.

Let’s start with creating the Dockerfile that builds two binaries. We will enhance the Docker’s multi-stage build to keep our final Docker images small.

# deployments/gcp/dockers/base.Dockerfile
FROM golang:1.16-buster as builder 
ENV GO111MODULE on
RUN apt-get update && apt-get install -y make git ca-certificates

WORKDIR /app
COPY ./backend/ ./
RUN go build -o grpc-server -ldflags="-s -w" cmd/grpc/main.go
RUN go build -o grpcweb-server -ldflags="-s -w" cmd/grpcweb/main.go

Now, we can build the image and tag it as builder docker build -f base.Dockerfile -t builder. The last thing we need are two docker images that simply copy the binary:

# grpcweb.Dockerfile
FROM debian:buster-slim
RUN apt-get update && apt-get install -y ca-certificates
COPY --from=builder /app/grpcweb-server /app/grpcweb-server
CMD ["/app/grpcweb-server"]
# grpc.Dockerfile
FROM debian:buster-slim
RUN apt-get update && apt-get install -y ca-certificates
COPY --from=builder /app/grpc-server /app/grpc-server
CMD ["/app/grpc-server"]

We can build the with commands docker build -f grpcweb.Dockerfile -t grpcweb and docker build -f grpcweb.Dockerfile -t grpc. You can also test those images using command docker run --network host grpc or docker run --network host grpcweb.

The last part of this chapter is to upload those images to GCP and run them using Google Cloud Run. Run the command gcloud init in the root directory. Create a new project. We have created the project gendocu-example - let’s use it as an example. After the project creation, you can upload your docker images to the image registry with url: gcr.io/gendocu-example/<any-image-name>. Add the tag to images grpcweb and grpc to upload the to docker registry - please remember to replace gendocu-example with your project name:

docker tag grpc gcr.io/gendocu-example/library-app-grpc
docker push gcr.io/gendocu-example/library-app-grpc
docker tag grpcweb gcr.io/gendocu-example/library-app-grpcweb
docker push gcr.io/gendocu-example/library-app-grpcweb

Now, we can deploy these images to Google Cloud Run with these commands:

gcloud run deploy --project gendocu-example --image gcr.io/gendocu-example/library-app-grpc --platform managed grpc-server
gcloud run deploy --project gendocu-example --image gcr.io/gendocu-example/library-app-grpcweb --platform managed grpcweb-server

The output of those commands looks like this:

$ gcloud run deploy --project gendocu-example --image gcr.io/gendocu-example/library-app-grpc --platform managed  grpc-server

Allow unauthenticated invocations to [library-app-grpc] (y/N)?  y
Deploying container to Cloud Run service [library-app-grpc] in project [gendocu-example] region [us-central1]
✓ Deploying... Done.                                                                                                                                                                                                                          
  ✓ Creating Revision...                                                                                                                                                                                                                      
  ✓ Routing traffic...                                                                                                                                                                                                                        
Done.                                                                                                                                                                                                                                         
Service [library-app-grpc] revision [library-app-grpc-00005-dit] has been deployed and is serving 100 percent of traffic.
Service URL: https://library-app-grpc-uqnits2f5q-uc.a.run.app

$ gcloud run deploy --project gendocu-example --image gcr.io/gendocu-example/library-app-grpcweb --platform managed  grpcweb-server

Allow unauthenticated invocations to [library-app-grpcweb] (y/N)? 
Deploying container to Cloud Run service [library-app-grpcweb] in project [gendocu-example] region [us-central1]
✓ Deploying... Done.                                                                                                                                                                                                                          
  ✓ Creating Revision...                                                                                                                                                                                                                      
  ✓ Routing traffic...                                                                                                                                                                                                                        
Done.                                            
Service [library-app-grpcweb] revision [library-app-grpcweb-00003-fes] has been deployed and is serving 100 percent of traffic.
Service URL: https://library-app-grpcweb-uqnits2f5q-ey.a.run.app

The gRPC Web endpoint works - you can test it with the frontend: REACT_APP_BACKEND=https://<your-grpc-web-url> yarn start.

The gRPC endpoint is more problematic. It requires HTTP/2, which is still an experimental feature on Google Cloud. The easiest way to turn it on is to go to your Cloud Run Project, select your grpc function, click edit, and then in the Connections tab, enable Enable http/2 connections. Then click deploy.

/gcp_http2_enable.png

You can test gRPC using the gRPC Curl grpcurl library-app-grpc-uqnits2f5q-uc.a.run.app:443 list

Do you like this article? Subscribe newsletter to get more.

We use Mailchimp as our marketing platform. By clicking above to subscribe, you acknowledge that your information will be transferred to Mailchimp for processing. You can unsubscribe anytime. Learn more about Mailchimp's privacy practices here.

Troubleshooting

  1. Failed to dial target host "https://<url>": dial tcp: lookup tcp///<url>: Servname not supported for ai_socktype - the gRPC url can’t start with http or https as gRPC uses raw tcp instead of http. Remove the URL Scheme, e.g. https://library-app-grpc-uqnits2f5q-uc.a.run.app -> library-app-grpc-uqnits2f5q-uc.a.run.app:443

  2. Failed to dial target host "<url>": context deadline exceeded - please check whether you have entered the correct port. The gRPC server might expose any arbitrary port (like 8080), but the Google Cloud Run exposes it as a 443 for HTTPS and 80 for HTTP.

  3. The gRPC Curl hangs - please check whether the HTTP/2 connections are available. You can find a guide at the end of the ‘Deployment to Google Cloud Run’ section in this article.