问题描述
我们在 Google Cloud Run 实例上部署了一个 gRPC 服务器,我们希望从其他 Google Cloud 环境(特别是 GKE 和 Cloud Run)访问该实例。
我们有以下代码来获取连接对象以及从 Google 默认凭据流生成的不记名令牌的上下文:
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
}
然后使用上面的:
// 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...
}
要强调的几点:
- NewServerConnection 基于 Google 的文档:Obtaining an OIDC token for the default service account 和 Sending gRPC requests with authentication
- 我们全局声明
myServer
对象并初始化一次。这是为了避免对底层元数据服务器进行不必要的调用以检索 Google 默认凭据,即令牌。这是来自 Google's Documentation 的有关此概念的链接
- 一旦“初始化”,我们就有了一个包含不记名令牌的 ctx 对象,然后我们在每次调用客户端的任何 rpc 方法时使用该令牌。
问题:
- 以上是访问 Cloud Run 的一种优雅方式吗?
- 目前我们必须将
myServer.Ctx
添加到我们所有的客户端过程调用中 - 有没有办法将它“嵌入”到myServer.Conn
中? WithPerRPCCredentials 在这里有用吗? - 如何处理过期的令牌?令牌的默认到期时间为 1 小时,任何从初始实例化开始超过 1 小时的客户端过程调用都将失败。是否有一种优雅的方式来“刷新”或生成新令牌?
希望这一切都有意义!在 Google Cloud 上运行服务时,用于管理访问权限的 Cloudrun、gRPC 和 IAM 可能是一个非常优雅的设置。
解决方法
这里有一些相当优雅的东西。它使用 Google 应用程序凭据并将 lstStudents = thisStudents.Cast(Of Student)().ToList()
对象附加到 gRPC 连接对象。我的理解是,如果需要,这将允许在每次 gRPC 调用时自动刷新令牌。
NewTokenSource
可以如下使用:
// NewServerConnection creates a new gRPC connection.
//
// The host should be the domain where the Cloud Run Service is hosted
//
// 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) (*grpc.ClientConn,error) {
// Creates an identity token.
// With a global TokenSource tokens would be reused and auto-refreshed at need.
// A given TokenSource is specific to the audience.
tokenSource,err := idtoken.NewTokenSource(ctx,"https://"+host)
if err != nil {
return nil,status.Errorf(
codes.Unauthenticated,"NewTokenSource: %s",err,)
}
// 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(grpcTokenSource{
TokenSource: oauth.TokenSource{
tokenSource,},}))
conn,err := grpc.Dial(host+":443",opts...)
if err != nil {
return nil,"grpc.Dail: %s",)
}
return conn,nil
}