Featured image of post How to deploy a Go based gRPC Service on Google Cloud Run

How to deploy a Go based gRPC Service on Google Cloud Run

In this Post I show you how to deploy a gRPC Service written in Go to Google Cloud Run

Home / Posts / How to deploy a Go based gRPC Service on Google Cloud Run

Why gRPC and what are the benefits of it?

gRPC is a modern, high-performance, open-source remote procedure call (RPC) framework that can run anywhere. It enables client and server applications to communicate transparently, and makes it easier to build connected systems. Some of the benefits of using gRPC include:

  1. High performance: gRPC uses a compact binary format for data serialization, which is faster and more efficient than text-based formats like JSON or XML. It also uses HTTP/2 as the underlying transport protocol, which allows for efficient multiplexing of requests, streaming of data, and low-latency communication.
  2. Cross-language compatibility: gRPC uses protocol buffers as the interface definition language (IDL), which allows for language-agnostic contract definitions. This means that you can use any supported language to define the service API, and the generated code will be compatible with any other supported language.
  3. Simplified API development: gRPC provides a simple, modern API for building distributed systems, which makes it easier to develop and maintain complex, connected systems. It also provides tools and libraries for automatically generating API client and server code from the service definition, which can save a lot of time and effort.
  4. Improved reliability: gRPC includes features like automatic retries and backoff, flow control, and deadlines to help ensure that your system is reliable and responsive. It also supports bi-directional streaming and cancellation, which can be useful for building real-time, interactive applications.

Overall, the use of gRPC can help improve the performance, reliability, and simplicity of your distributed systems.

Building the Server Application

Requirements for the Guide

For our setup we have multiple requirements, make sure you have the following installed:

You further need a Google Cloud Account with Billing enabled. The whole deployment should cost anything, as we are staying within the Free Limits of the Various products.

Create the Protocol Buffers

I want to keep the Setup as simple as possible, therefore we build a really basic Ping Service which provides two Methods.

  • Respond to a Ping an include the provide Name
  • Respond with a Version of our Service

We put the Protocol Buffer definition in it’s own Folder called ping and create a file named ping.proto in there.

1
2
3
.
└── ping
    └── ping.proto

In there we define our services & types. If you want to learn more about the details of the Protocol Buffers, check our the official guide.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
syntax = "proto3";

option go_package = "grpc-gcloud/ping";

package ping;

service Pinger {
  rpc Ping (PingRequest) returns (PingReply) {}
  rpc GetVersion (VersionRequest) returns (VersionReply) {}
}

message PingRequest {
  string message = 1;
}

message PingReply {
  string message = 1;
}

message VersionRequest {
}

message VersionReply {
  string message = 1;
}

We are defining our Service Pinger which has the two Methods we outlined above. One important thing is that you always need to define a Received type, even if it’s empty as in our case with the VersionRequest.

Now we can build our Protocol Buffer file into Go code, which we then can use in our program.

1
2
3
protoc --go_out=. --go_opt=paths=source_relative \
    --go-grpc_out=. --go-grpc_opt=paths=source_relative \
    ping/ping.proto;

This creates two additional files in the ping folder

1
2
3
4
5
.
└── ping
    ├── ping.pb.go
    ├── ping.proto
    └── ping_grpc.pb.go

When you inspect these files, you see that the contain a lot of Go Boilerplate.

Our next step is to implement the logic to handle the defined Methods.

Create the Server

To separate our code a bit, I put the server code into it’s own folder called server. There I created a simple main.go file, which will run as server on Google Cloud.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import (
	"context"
	"log"
	"net"

	pb "grpc-gcloud/ping"

	"google.golang.org/grpc"
)

type server struct {
	pb.UnimplementedPingerServer
}

func (s *server) Ping(ctx context.Context, in *pb.PingRequest) (*pb.PingReply, error) {
	log.Printf("Received: %v", in.Message)
	return &pb.PingReply{Message: "Hello " + in.Message}, nil
}

func (s *server) GetVersion(ctx context.Context, in *pb.VersionRequest) (*pb.VersionReply, error) {
	return &pb.VersionReply{Message: "0.0.1"}, nil
}

func main() {
	lis, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	s := grpc.NewServer()
	pb.RegisterPingerServer(s, &server{})
	log.Printf("server listening at %v", lis.Addr())
	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

Now we need to add the generated Protocol Buffer files into the Server folder as well, this makes it easier to let Google Cloud build our code later on.

Create a ping folder inside of server and copy the two *.go files into this newly created folder. Then we init the go module with go mod init grpc-gcloud and then follow up with a go mod tidy to download the required dependencies.

Now we should have the following structure.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
.
├── ping
│   ├── ping.pb.go
│   ├── ping.proto
│   └── ping_grpc.pb.go
└── server
    ├── main.go
    └── ping
        ├── ping.pb.go
        └── ping_grpc.pb.go

If you now go into the server folder, you can launch your Server and should see the following message.

1
2
go run main.go
2022/12/04 12:07:39 server listening at [::]:8080

Testing the Server with gRPCurl

I was curious if the server is already working, there are multiple tools that can be used to invoke gRPC Services. One is gRPCurl which is, you guessed it, a curl like implementation for gRPC.

This step is optional, but if you are curious go ahead and install gRPCurl.

Then you can invoke your service, directly from the root of your project:

1
2
3
4
5
6
grpcurl \
  -import-path ping \
  -plaintext \
  -d '{"message": "gRPCurl"}' \
  -proto ping.proto \
  localhost:8080 ping.Pinger.Ping

And should get the matching output:

1
2
3
{
  "message": "Hello gRPCurl"
}

This looks already good! Another option is to use Insomnia or or Postman

Testing the Server with Insomnia

In Insomnia you have to first create a new gRPC Request, make sure to select the gRPC option, otherwise it wont work. Create a new gRPC Request in Insomnia

The next prompt asks for the proto file to use, here you can already see the benefit of the whole gRPC story :)

Select the ping.proto file from the ping folder

Now you only have to fill out the server address localhost:8080 and select the Method you want to use. The Body is optional, it can be left blank or you can specify a message.

Successful request from Insomnia

Deploy the Server to Google Cloud

Our new server is now ready to be deployed to Google Cloud. For this we create a new project (feel free to use an existing one, if you are familiar with google cloud).

Setup of the Google Cloud project

1
gcloud projects create grpc-guide

This take a little bit of time, but then you should see the new project in the projects list.

1
2
gcloud projects list | grep grpc-guide
grpc-guide        grpc-guide     191926065871

This means we are good to go, now let’s deploy our code to a google cloud run instance, it’s the serverless offering from google cloud. First we need to enable billing for the newly created project, if you don’t have a billing account you need to create one first

Then you can grab the ID via

1
2
3
gcloud beta billing accounts list
ACCOUNT_ID            NAME  OPEN  MASTER_ACCOUNT_ID
X_Y_Z                 bill  True

And then link it with the new project

1
gcloud beta billing projects link grpc-guide --billing-account=X_Y_Z

After that we can enable the required services for our new project.

1
2
3
gcloud services enable artifactregistry.googleapis.com --project grpc-guide
gcloud services enable run.googleapis.com --project grpc-guide
gcloud services enable cloudbuild.googleapis.com --project grpc-guide

Now is a great time to grab a coffee, as you have to wait a couple minutes before the services are ready to be used. If you try it too early, you will get a fault along the lines of:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
ERROR: (gcloud.run.deploy) PERMISSION_DENIED: Cloud Build API has not been used in project X before or it is disabled. Enable it by visiting https://console.developers.google.com/apis/api/cloudbuild.googleapis.com/overview?project=191926065871 then retry. If you enabled this API recently, wait a few minutes for the action to propagate to our systems and retry.
- '@type': type.googleapis.com/google.rpc.Help
  links:
  - description: Google developers console API activation
    url: https://console.developers.google.com/apis/api/cloudbuild.googleapis.com/overview?project=X
- '@type': type.googleapis.com/google.rpc.ErrorInfo
  domain: googleapis.com
  metadata:
    consumer: projects/X
    service: cloudbuild.googleapis.com
  reason: SERVICE_DISABLED

Deploying the code to Google Cloud

We directly deploy two variations of our service, one without authentication and one with.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
export GOOGLE_CLOUD_PROJECT=grpc-guide

# deploy version without auth
gcloud run deploy ping  \
  --project $GOOGLE_CLOUD_PROJECT \
  --region us-central1 \
  --platform=managed \
  --source ./server/ \
  --allow-unauthenticated \
  --quiet

# deploy version without auth
gcloud run deploy ping-auth \
  --project $GOOGLE_CLOUD_PROJECT \
  --region us-central1 \
  --platform=managed \
  --source ./server/ \
  --quiet

There are different options for the deployment:

  • You can build locally and upload the container images
  • You can define a Dockerfile and let Google build based on that
  • You can use the –source variant (Which we use here), Google then uses a magic template to deploy your code :)

After you run above command, you will have twice the url information in your output.

1
2
Service URL: https://ping-leipieppba-uc.a.run.app
Service URL: https://ping-auth-leipieppba-uc.a.run.app

This can now be plugged into the same gRPCurl and Insomnia commands that we used before. But you need to change it a little bit, so for gRPCurl the command would be.

1
2
3
4
5
grpcurl \
  -import-path ping \
  -d '{"message": "gcloud"}' \
  -proto ping.proto \
  ping-leipieppba-uc.a.run.app:443 ping.Pinger.Ping

Main difference is that we remove the -plaintext flag, as google does by default tls for us. We also have to add the port to the url. This command should work and return you a message from the service that runs on Google Cloud!

For Insomnia you have to prefix the url with grpcs:// that the call works again. Otherwise you will get an error “13 Received RST_STREAM with code 2 (Internal server error)” which doesn’t really help…

On the other hand, the -auth version should throw a 403 error. Let’s fix this now and write our go based gRPC client.

Building the gRPC Client Application

Alright, so far we created and deployed our server. We did some validation with Insomnia and the gRPCurl tools. Now it’s time to tackle a client in Go, which can talk to our server.

Client code

We put our client in its own directory. There I have a client.go file, which contains the go code. I also put the two *.go files from the ping folder into a ping subfolder inside of the client folder.

1
2
3
4
5
6
.
└── client
    ├── client.go
    └── ping
        ├── ping.pb.go
        └── ping_grpc.pb.go

Then we also initialize this subfolder as go module. I used the same name, but this is probably not best practice :)

1
2
go mod init grpc-gcloud
go mod tidy

Service Account

To allow access to the Authenticated version of our gRPC Service, we need a service account which has the rights to invoke the service.

This can be done in the portal or directly through the gcloud CLI.

1
gcloud iam service-accounts create grpc-gcloud --display-name="grpc-gcloud" --project grpc-guide

After we created the service account, we can fetch the service-account.json for it.

1
gcloud iam service-accounts keys create service-account.json [email protected]

You have to enter the IAM account in the format @.iam.gserviceaccount.com The output is now the service-account.json file, which we put into the client folder.

Then we need to allow the user to invoke our Google Cloud Run Service

1
2
3
4
5
gcloud run services add-iam-policy-binding ping-auth \
  --member=serviceAccount:[email protected] \
  --role=roles/run.invoker \
  --project grpc-guide \
  --region us-central1

Go code

Now you are ready to let the client run, this is the sample code we are using.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package main

import (
	"context"
	"crypto/x509"
	"fmt"
	"log"

	pb "grpc-gcloud/ping"

	"google.golang.org/api/idtoken"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials"
	"google.golang.org/grpc/credentials/oauth"
)

var (
	server     = "ping-leipieppba-uc.a.run.app"
	serverAuth = "ping-auth-leipieppba-uc.a.run.app"
)

func auth(ctx context.Context) oauth.TokenSource {
	audience := fmt.Sprintf("https://%s", server)
	// We load the service account from the json file
	idTokenSource, err := idtoken.NewTokenSource(ctx, audience, idtoken.WithCredentialsFile("service-account.json"))
	if err != nil {
		log.Fatalf("unable to create TokenSource: %v", err)
	}
	return oauth.TokenSource{idTokenSource}
}

func main() {
	ctx := context.Background()
	perRpc := auth(ctx)

	pool, _ := x509.SystemCertPool()
	creds := credentials.NewClientTLSFromCert(pool, "")

	// Set up a connection to the server. Without auth.
	conn, err := grpc.Dial(
		server+":443",
		grpc.WithTransportCredentials(creds),
	)
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}

	// Set up a connection to the server. With auth.
	connAuth, err := grpc.Dial(
		serverAuth+":443",
		grpc.WithTransportCredentials(creds),
		grpc.WithPerRPCCredentials(perRpc),
	)
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}

	defer conn.Close()
	defer connAuth.Close()

	c := pb.NewPingerClient(conn)
	cAuth := pb.NewPingerClient(connAuth)

	r, err := c.Ping(ctx, &pb.PingRequest{Message: "Go Client"})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Greeting: %s", r.GetMessage())

	rAuth, err := cAuth.Ping(ctx, &pb.PingRequest{Message: "Go Client"})
	if err != nil {
		log.Fatalf("could not greet: %v", err)
	}
	log.Printf("Greeting: %s", rAuth.GetMessage())
}
1
2
3
go run client.go
2022/12/04 19:16:59 Greeting: Hello Go Client
2022/12/04 19:17:00 Greeting: Hello Go Client

Github Repo

I put the whole code into a Github Repo check it out and let me know if there are any questions!

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy