Kubernetes and gRPC are technologies to build distributed microservices.
Writing gRPC servers in Go is fun and easy. With go-grpc-middleware, you can add various features to gRPC servers including access logs and Prometheus metrics.
Kubebuilder is a toolkit to develop custom Kubernetes controllers. The main component of kubebuilder is a library called controller-runtime that provides logging and Prometheus metrics as well as Kubernetes client libraries.
When developing a microservice for Kubernetes, these two technologies are often used together. This article describes how. The readers need to have basic knowledge about Go, kubebuilder/controller-runtime, and gRPC.
- Introducing go-grpc-middleware
- Problems
- How to run gRPC server with manager.Manager
- How to share the same zap logger
- How to register gRPC metrics with kubebuilder's Registry
- Conclusion
Introducing go-grpc-middleware
go-grpc-middleware is an umbrella project for useful gRPC interceptors for Go. You can easily add access logging, Prometheus metrics, and OpenTracing tags to your gRPC server with go-grpc-middleware.
The following code illustrates how to extract fields from request parameters and log requests using zap.
import ( grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap" grpc_ctxtags "github.com/grpc-ecosystem/go-grpc-middleware/tags" "go.uber.org/zap" "google.golang.org/grpc" ) // extract fields from request parameters func extractFields(fullMethod string, req interface{}) map[string]interface{} { args, ok := req.(*SomeArgumentType) if !ok { return nil } ret := make(map[string]interface{}) if args.Foo != "" { ret["foo"] = args.Foo } ret["bar"] = args.Bar return ret } func makeServer(log *zap.Logger) *grpc.Server { return grpc.NewServer(grpc.UnaryInterceptor( grpc_middleware.ChainUnaryServer( grpc_ctxtags.UnaryServerInterceptor(grpc_ctxtags.WithFieldExtractor(extractFields)), grpc_zap.UnaryServerInterceptor(log), ), )) }
This server will log requests with grpc.request.foo
and grpc.request.bar
fields.
If zap is configured to output JSON, it should look like:
{ "grpc.code": "OK", "grpc.method": "Add", "grpc.request.foo": "some message", "grpc.request.bar": 10, "grpc.service": "your.service", "grpc.start_time": "2020-08-30T10:18:49Z", "grpc.time_ms": 102.34100341796875, "level": "info", "msg": "finished unary call with code OK", "peer.address": "@", "span.kind": "server", "system": "grpc", "ts": 1598782729.4605289 }
Problems
Kubebuilder generates code that assumes the main function runs only manager.Manager
.
Custom controllers, admission webhooks, and the metrics exporter for Prometheus are all run under the manager. The first problem is: how can we do the same for gRPC server?
The second and third problems are the duplication of features between kubebuilder and go-grpc-middleware.
Both uses zap as a logging library, but kubebuilder abstracts it in logr interface and does not expose raw zap structs. The second problem is: how can we share the same zap logger?
To export metrics for Prometheus, kubebuilder defines a custom Registry
whereas go-grpc-middleware's example uses the builtin Registry as follows:
myServer := grpc.NewServer( // snip... grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer( grpc_ctxtags.UnaryServerInterceptor(), grpc_prometheus.UnaryServerInterceptor, // uses the builtin registry in the prometheus client library
So the last problem is: how can we register gRPC metrics with kubebuilder's Registry?
How to run gRPC server with manager.Manager
manager.Manager
can run arbitrary processes that implements Runnable
interface.
The following example runs gRPC server under the manager.
import ( "google.golang.org/grpc" ctrl "sigs.k8s.io/controller-runtime" ) // GRPCRunner implements `manager.Runnable`. type GRPCRunner { server *grpc.Server listener net.Listener } // Start the gRPC server. It will be gracefully shutdown when `ch` is closed. func (r GRPCRunner) Start(ch <-chan struct{}) error { go func() { <-ch r.server.GracefulStop() }() return r.server.Serve(r.listener) } func main() { mgr, _ := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{...} l, _ := net.Listen("unix", "/...") grpcServer := grpc.NewServer() r := GRPCRunner{server: gRPCServer, listener: l} mgr.Add(r) mgr.Start(ctrl.SetupSignalHandler()) }
How to share the same zap logger
Normally, kubebuilder generates code like this:
import "sigs.k8s.io/controller-runtime/pkg/log/zap" func main() { // ... ctrl.SetLogger(zap.New(zap.UseDevMode(true))) }
The problem here is that zap.New
from controller-runtime does not return the raw *zap.Logger
. Instead, it returns an object wrapped in logr.Logger
interface.
We can rewrite the code so that it creates and shares the same *zap.Logger
as follows:
import ( "github.com/go-logr/zapr" grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" grpc_zap "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap" grpc_ctxtags "github.com/grpc-ecosystem/go-grpc-middleware/tags" "google.golang.org/grpc" "sigs.k8s.io/controller-runtime/pkg/log/zap" ) func main() { // zap.NewRaw creates *zap.Logger zapLogger := zap.NewRaw() defer zapLogger.Sync() // Wrap the raw logger with zapr.NewLogger ctrl.SetLogger(zapr.NewLogger(zapLogger)) // Use the same logger for gRPC grpcLogger := zapLogger.Named("grpc") grpcServer := grpc.NewServer(grpc.UnaryInterceptor( grpc_middleware.ChainUnaryServer( grpc_ctxtags.UnaryServerInterceptor(), grpc_zap.UnaryServerInterceptor(gRPCLogger), ), )) }
How to register gRPC metrics with kubebuilder's Registry
Kubebuilder exports various metrics registered with metrics.Registry
.
To register gRPC metrics with metrics.Registry
, write code like this:
import ( grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" grpc_ctxtags "github.com/grpc-ecosystem/go-grpc-middleware/tags" grpc_prometheus "github.com/grpc-ecosystem/go-grpc-prometheus" "google.golang.org/grpc" "sigs.k8s.io/controller-runtime/pkg/metrics" ) func main() { grpcMetrics := grpc_prometheus.NewServerMetrics() metrics.Registry.MustRegister(grpcMetrics) grpcServer := grpc.NewServer(grpc.UnaryInterceptor( grpc_middleware.ChainUnaryServer( grpc_ctxtags.UnaryServerInterceptor(grpc_ctxtags.WithFieldExtractor(fieldExtractor)), grpcMetrics.UnaryServerInterceptor(), ), )) // register your service // after all services are registered, initialize metrics. grpcMetrics.InitializeMetrics(grpcServer) }
This combined with manager.Manager
exports metrics including gRPC's.
Conclusion
Now we can use go-grpc-middleware together with kubebuilder. You can find a real world example in github.com/cybozu-go/coil/v2/runners/coild_server.go.