We have a gRPC server deployed on a Google Cloud Run instance which we like to access from other Google Cloud environments (GKE and Cloud Run in particular).
We have the following code to get a connection object as well as context with the Bearer token generated from the Google Default Credential flow:
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"os"
"regexp"
"google.golang.org/api/idtoken"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
grpcMetadata "google.golang.org/grpc/metadata"
)
type ServerConnection struct {
Conn *grpc.ClientConn
Ctx context.Context
}
// NewServerConnection creates a new gRPC connection and request a Token to be used in the context.
//
// The host should be the domain where the Service is hosted, e.g., my-cloudrun-url-v1-inb33tjqiq-ew.a.run.app
//
// This method also uses the Google Default Credentials workflow. To run this locally ensure that you have the
// environmental variable GOOGLE_APPLICATION_CREDENTIALS = ../key.json set.
//
// Best practise is to create a new connection at global level, which could be used to run many methods. This avoids
// unnecessary api calls to retrieve the required ID tokens each time a single method is called.
func NewServerConnection(ctx context.Context, host string) (*ServerConnection, error) {
// Establishes a connection
var opts []grpc.DialOption
if host != "" {
opts = append(opts, grpc.WithAuthority(host+":443"))
}
systemRoots, err := x509.SystemCertPool()
if err != nil {
return nil, err
}
cred := credentials.NewTLS(&tls.Config{
RootCAs: systemRoots,
})
opts = append(opts, grpc.WithTransportCredentials(cred))
opts = append(opts, grpc.WithPerRPCCredentials())
conn, err := grpc.Dial(host+":443", opts...)
// Creates an identity token.
// A given TokenSource is specific to the audience.
tokenSource, err := idtoken.NewTokenSource(ctx, "https://"+host)
if err != nil {
return nil, err
}
token, err := tokenSource.Token()
if err != nil {
return nil, err
}
// Add token to gRPC Request.
ctx = grpcMetadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+token.AccessToken)
return &ServerConnection{
Conn: conn,
Ctx: ctx,
}, nil
}
Then using the above:
// Declare Globally
var myServer *ServerConnection
func TestNewServerConnection(t *testing.T) {
// Connects to the server and add token to ctx.
// In cloud run this is done once, populating the global variable
ctx := context.Background()
var err error;
myServer, _ = NewServerConnection(ctx, "my-cloudrun-url-v1-inb33tjqiq-ew.a.run.app")
// Now that we have a connection as well as a Context object with the Token
// we would like to make many client calls.
client := pb.NewBookstoreClient(myServer.Conn)
result, err := client.CreateBook(myServer.Ctx, &pb.Book{})
if err != nil {
// TODO: handle error
}
// Use result
_ = result
// ... make more client procedure calls here...
}
A few points to highlight:
- The NewServerConnection is based on Google's documentation: Obtaining an OIDC token for the default service account and Sending gRPC requests with authentication
- We declare the
myServer
object globally and initialise it once. This is to avoid making unnecessary calls to the underlying meta-data server to retrieve Google Default Credentials, i.e. the Token. Here is a link on this concept from Google's Documentation - Once 'initalised' we have a ctx object which contains a bearer token which we then use with each call to any of the client's rpc methods.
Questions:
- Is the above an elegant way to access Cloud Run?
- Currently we have to add the
myServer.Ctx
to all our client procedure calls - is there a way to 'embed' this within themyServer.Conn
? Could WithPerRPCCredentials be of use here? - How would one handle expired tokens? Default expiry of a token is 1 hour, any client procedure calls made more than 1 hours from the initial instantiation will fail. Is there an elegant way to 'refresh' or generate a new token?
Hope this all makes sense! Cloudrun, gRPC and IAM to manage access is potentially a really elegant setup when running services on Google Cloud.