Golang gRPC Cloud Run
- 4 minutes read - 843 wordsUpdate: 2020-03-24: Since writing this post, I’ve contributed Golang and Rust samples to Google’s project. I recommend you start there.
Google explained how to run gRPC servers with Cloud Run. The examples are good but only Python and Node.JS:
Missing Golang…. until now ;-)
I had problems with 1.14 and so I’m using 1.13.
Project structure
I’ll tidy up my repo but the code may be found:
https://github.com/DazWilkin/Golang-gRPC-CloudRun
tree -L 1
.
├── client
├── cloudbuild.yaml
├── deployment
├── go.mod
├── go.sum
├── protoc
├── protos
├── README.md
└── server
Protobuf
One minor tweak (for my preference) to the probuf file with the addition of option go_package = "protos";
.
This defines the package name (protos
) in the Go-generated code.
The protobuf file is placed in ./protos
syntax = "proto3";
option go_package = "protos";
enum Operation {
ADD = 0;
SUBTRACT = 1;
}
message BinaryOperation {
float first_operand = 1;
float second_operand = 2;
Operation operation = 3;
};
message CalculationResult {
float result = 1;
};
service Calculator {
rpc Calculate (BinaryOperation) returns (CalculationResult);
};
I’m using protoc 3.11.4 (with linux-x86_64
), revise VERS
and ARCH
as appropriate:
FROM golang:1.13 as build
WORKDIR /grpc-cloudrun
ARG REPO="github.com/DazWilkin/Golang-gRPC-CloudRun.git"
RUN git clone https://${REPO} .
# Installs protoc and plugins: protoc-gen-go
ARG VERS="3.11.4"
ARG ARCH="linux-x86_64"
RUN wget https://github.com/protocolbuffers/protobuf/releases/download/v${VERS}/protoc-${VERS}-${ARCH}.zip --output-document=./protoc-${VERS}-${ARCH}.zip && \
apt update && apt install -y unzip && \
unzip -o protoc-${VERS}-${ARCH}.zip -d protoc-${VERS}-${ARCH} && \
mv protoc-${VERS}-${ARCH}/bin/* /usr/local/bin && \
mv protoc-${VERS}-${ARCH}/include/* /usr/local/include && \
go get -u github.com/golang/protobuf/protoc-gen-go
# Generates the Golang protobuf files
RUN protoc \
--proto_path=. \
--go_out=plugins=grpc:. \
./protos/*.proto
RUN CGO_ENABLED=0 GOOS=linux \
go build -a -installsuffix cgo \
-o /go/bin/server \
github.com/DazWilkin/Golang-gRPC-CloudRun/server
ENTRYPOINT ["/go/bin/server"]
The GitHub repo includes a cloudbuild.yaml
file that you may use with Google Cloud Build:
gcloud services enable cloudbuild.googleapis.com --project=${PROJECT}
gcloud builds submit . \
--config=./cloudbuild.yaml \
--substitutions=COMMIT_SHA=${TAG} \
--project=${PROJECT}
NB You’ll need to build a local copy of the Cloud Builder
protoc
to use Cloud Build.
Server
I’ve attempted to keep this close to the Python and Node.JS examples.
main.go:
package main
import (
"context"
"fmt"
"log"
"net"
"os"
"sync"
pb "github.com/DazWilkin/Golang-gRPC-CloudRun/protos"
"google.golang.org/grpc"
)
const (
serviceName = "grpc-cloudrun-server"
)
var (
port = os.Getenv("PORT")
)
func main() {
log.Printf("Starting: %s", serviceName)
defer func() {
log.Printf("Stopping:%s", serviceName)
}()
if port == "" {
port = "8080"
}
grpcEndpoint := fmt.Sprintf(":%s", port)
log.Printf("gRPC endpoint [%s]", grpcEndpoint)
grpcServer := grpc.NewServer()
pb.RegisterCalculatorServer(grpcServer, NewServer())
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
var wg sync.WaitGroup
// gRPC Server
wg.Add(1)
go func() {
defer wg.Done()
listen, err := net.Listen("tcp", grpcEndpoint)
if err != nil {
log.Fatal(err)
}
log.Printf("Starting: gRPC Listener [%s]\n", grpcEndpoint)
log.Fatal(grpcServer.Serve(listen))
}()
wg.Wait()
}
and:
server.go:
package main
import (
"context"
"fmt"
"log"
pb "github.com/DazWilkin/Golang-gRPC-CloudRun/protos"
"go.opencensus.io/trace"
)
// Prove that Server implements pb.CalculatorServer by instantiating a Server
var _ pb.CalculatorServer = (*Server)(nil)
// Server is a struct implements the pb.CalculatorServer
type Server struct {
}
func NewServer() *Server {
return &Server{}
}
// Calculate performs an operation on operands defined by pb.BinaryOperation returning pb.CalculationResult
func (s *Server) Calculate(ctx context.Context, r *pb.BinaryOperation) (*pb.CalculationResult, error) {
log.Println("[server:Calculate] Started")
if ctx.Err() == context.Canceled {
return &pb.CalculationResult{}, fmt.Errorf("client cancelled: abandoning")
}
// TODO(dazwilkin) Low-value but required to capture a trace at all
ctx, span := trace.StartSpan(ctx, "Calculate")
defer span.End()
switch r.GetOperation() {
case pb.Operation_ADD:
return &pb.CalculationResult{
Result: r.GetFirstOperand() + r.GetSecondOperand(),
}, nil
case pb.Operation_SUBTRACT:
return &pb.CalculationResult{
Result: r.GetFirstOperand() - r.GetSecondOperand(),
}, nil
default:
return &pb.CalculationResult{}, fmt.Errorf("undefined operation")
}
}
If you clone the repo, you’ll have ./protos/calculator.pb.go
.
If not, you’ll need to grab a release of protoc
and the Go plugin protoc-gen-go
.
And generate the Go stubs with:
protoc \
--proto_path=. \
--go_out=plugins=grpc:. \
./protos/*.proto
NB The Dockerfile handles this for you.
You may test this by running the server in one shell:
PORT=50051
go run server/*.go
and accessing it with grpcurl
from another:
grpcurl \
-plaintext \
-proto protos/calculator.proto \
-d '{"first_operand": 2.0, "second_operand": 3.0, "operation": "ADD"}' \
localhost:${PORT} Calculator.Calculate
all being well, of course:
{
"result": 5
}
Containerize
TAG=$(git rev-parse HEAD) # Or your preference
docker build \
--tag=gcr.io/${PROJECT}/server:${TAG} \
--file=./deployment/Dockerfile.server \
.
docker push gcr.io/golang-grpc-cloudrun/server:${TAG}
You may then test this, by using the same grpcurl
command but, running the container image as:
PORT=50051 # On Cloud Run this will be 8080
docker run \
--interactive --tty \
--publish=50051:${PORT} \
--env=PORT=${PORT} \
gcr.io/${PROJECT}/server:${TAG}
The result should (!) be the same as before ;-)
Cloud Run:
SERVICE="server"
REGION=us-west1 # Or ...
gcloud beta run deploy ${SERVICE} \
--image=gcr.io/${PROJECT}/server:${TAG} \
--project=${PROJECT} \
--region=${REGION} \
--platform=managed \
--allow-unauthenticated
This should complete (successfully) by outputting the service’s endpoint:
...
Service [${SERVICE}] revision [...] has been deployed and is serving 100 percent of traffic at https://${ENDPOINT}
You could:
ENDPOINT=$(\
gcloud run services list \
--project=${PROJECT} \
--region=${REGION} \
--platform=managed \
--format="value(status.address.url)" \
--filter="metadata.name=${SERVICE}")
ENDPOINT=${ENDPOINT#https://} && echo ${ENDPOINT}
Thanks to https://stackoverflow.com/a/16623897/609290
And then grpcurl the endpoint with:
grpcurl \
-proto protos/calculator.proto \
-d '{"first_operand": 2.0, "second_operand": 3.0, "operation": "ADD"}' \
${ENDPOINT}:443 Calculator.Calculate
NB Remove the
-plaintext
flag this time
{ “result”: 5 }
HTH!