gRPCサーバを含むGoプロジェクトをBazelでビルドする

前回までのエントリに引き続きBazelのビルドをまとめていく。

blog.soushi.me

blog.soushi.me

今回のエントリではgRPCサーバを含むGoプロジェクトをBazelでビルドする方法をまとめる。

Protocol BufferとgRPCサーバのInterfaceの生成をBazelで行う

これまではprotocをつかって.protoファイルからProtocol BufferとgRPCサーバのInterfaceの生成を行っていたがBazelのruleを使えば簡単にビルドタスクに追加することができる。

利用するruleはrules_proto

github.com

このruleに辿り着く前にpubref/rules_protobufを見つけたがpubrefのほうは周辺のruleのアップデートに追いついておらずstackbのほうがメンテナンスが活発である。

WORKSPACEにruleの追加と.protoファイルの追加

WORKSPACEにruleを追加する。

# WORKSPACE

http_archive(
    name = "build_stack_rules_proto",
    urls = ["https://github.com/stackb/rules_proto/archive/1d6550fc2e62.tar.gz"],
    sha256 = "113e6792f5b20679285c86d91c163cc8c4d2b4d24d7a087ae4f233b5d9311012",
    strip_prefix = "rules_proto-1d6550fc2e625d47dc4faadac92d7cb20e3ba5c5",
)

シンプルなecho.protoをproto/echoディレクトリ以下に追加する。

# proto/echo/echo.proto

syntax = "proto3";

package echo;

service Echo {
    rpc Echo (EchoInbound) returns (EchoOutbound);
}

message EchoInbound {
    string message = 1;
}

message EchoOutbound {
    string message = 1;
}

この状態でgazelleを実行するとproto/echo以下にBUILD.bazelが追加されている。

(bazel-multiprojects) $ bazel run //:gazelle
# proto/echo/BUILD.bazel

load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")

proto_library(
    name = "echo_proto",
    srcs = ["echo.proto"],
    visibility = ["//visibility:public"],
)

go_proto_library(
    name = "echo_go_proto",
    compilers = ["@io_bazel_rules_go//proto:go_grpc"],
    importpath = "github.com/soushin/bazel-multiprojects/proto/echo",
    proto = ":echo_proto",
    visibility = ["//visibility:public"],
)

go_library(
    name = "go_default_library",
    embed = [":echo_go_proto"],
    importpath = "github.com/soushin/bazel-multiprojects/proto/echo",
    visibility = ["//visibility:public"],
)

ここで生成されたgo_libraryのgo_default_libraryを後でgRPCサーバ側のソースに依存させる。

go_grpc_compileをつかってProtocol Bufferを生成するタスクを追加する

go_default_libraryのビルドタスクのみだとecho.pb.goファイルが生成されない。ここでstackb/rules_protoのruleを追加する。proto/echo/BUILD.bazelに以下を追加する。

load("@build_stack_rules_proto//go:go_grpc_compile.bzl", "go_grpc_compile")

go_grpc_compile(
    name = "proto_buf",
    deps = [":echo_proto"],
)

このproto_bufのタスクを実行するとbazel-genfiles/proto/echo/proto_buf/proto/echo/以下にecho.pb.goファイルが生成される。

(bazel-multiprojects) $ bazel build //proto/echo:proto_buf
INFO: Analysed target //proto/echo:proto_buf (0 packages loaded).
INFO: Found 1 target...
Target //proto/echo:proto_buf up-to-date:
  bazel-genfiles/proto/echo/proto_buf/proto/echo/echo.pb.go
INFO: Elapsed time: 0.261s, Critical Path: 0.08s
INFO: 1 process: 1 darwin-sandbox.
INFO: Build completed successfully, 2 total actions

生成されたecho.pb.goファイルをプロジェクトの/proto/echoに配置することでgRPCサーバ側のコードにimportすることができる。

gRPCサーバを実装する

gRPCサーバのコードではgoogle.golang.org/grpcのライブラリを利用するのでWORKSPACEに外部ライブラリの依存を追加しておく。

go_repository(
    name = "org_golang_google_grpc",
    importpath = "github.com/grpc/grpc-go",
    tag = "v1.16.0",
)

main.goのコードは次のような差分になった。シンプルにechoServerを実装しているコードである。

# pkg/public_go/main.go

diff --git a/pkg/public_go/main.go b/pkg/public_go/main.go
index 73c925b..34bddcc 100644
--- a/pkg/public_go/main.go
+++ b/pkg/public_go/main.go
@@ -1,17 +1,29 @@
 package main

 import (
+       "context"
        "flag"
        "fmt"
-       "context"
        "log"
+       "net"
        "net/http"

+       "google.golang.org/grpc"
+       "google.golang.org/grpc/reflection"
+
        "github.com/soushin/bazel-multiprojects/pkg/common_go/util"
+       "github.com/soushin/bazel-multiprojects/proto/echo"
 )

 var msg string

+type server struct{}
+
+func (s *server) Echo(ctx context.Context, in *echo.EchoInbound) (*echo.EchoOutbound, error) {
+       return &echo.EchoOutbound{
+               Message: in.Message}, nil
+}
+
 func main() {
        greet := flag.String("greet", "Hello", "greet message")
        flag.Parse()
@@ -21,6 +33,22 @@ func main() {
        }
        msg = greetUsecase.Msg

+       // serve gRPC server
+       lis, err := net.Listen("tcp", ":50051")
+       defer lis.Close()
+       if err != nil {
+               log.Fatalf("failed to listen: %v", err)
+       }
+       grpcServer := grpc.NewServer()
+       server := &server{}
+       echo.RegisterEchoServer(grpcServer, server)
+       reflection.Register(grpcServer)
+       go func() {
+               if err := grpcServer.Serve(lis); err != nil {
+                       log.Fatalf("failed to serve grpc server")
+               }
+       }()
+
        http.HandleFunc("/", handler)
        http.ListenAndServe(":8080", nil)
 }

上記のコードの状態でgazelleを実行するとpkg/public_go/BUILD.bazelに依存が追加される。

diff --git a/pkg/public_go/BUILD.bazel b/pkg/public_go/BUILD.bazel
index 0c4c20d..24250db 100644
--- a/pkg/public_go/BUILD.bazel
+++ b/pkg/public_go/BUILD.bazel
@@ -11,6 +11,9 @@ go_library(
     deps = [
         "//pkg/common_go/util:go_default_library",
         "//pkg/public_go/usecase:go_default_library",
+        "//proto/echo:go_default_library",
+        "@org_golang_google_grpc//:go_default_library",
+        "@org_golang_google_grpc//reflection:go_default_library",
     ],
 )

gazelleがWORKSPACEの外部ライブラリ依存の定義とmain.goのコードを解析して依存関係をBUILD定義に反映してくれている。

ここまでくればgRPCサーバを起動することができる。

gRPCサーバの起動

pkg/public_go/BUILD.bazelに定義されているgo_binaryのタスクのrunコマンドを実行してサーバを起動させる。

$ (bazel-multiprojects) $ bazel run //pkg/public_go:public_go

gRPCのデバッグに最適なevansで起動したgRPCサーバにリクエストを送り確認する。

github.com

evans --port 50051 proto/echo/echo.proto

  ______
 |  ____|
 | |__    __   __   __ _   _ __    ___
 |  __|   \ \ / /  / _. | | '_ \  / __|
 | |____   \ V /  | (_| | | | | | \__ \
 |______|   \_/    \__,_| |_| |_| |___/

 more expressive universal gRPC client


127.0.0.1:50051> show package
+---------+
| PACKAGE |
+---------+
| echo    |
+---------+

127.0.0.1:50051> package echo

echo@127.0.0.1:50051> show service
+---------+------+-------------+--------------+
| SERVICE | RPC  | REQUESTTYPE | RESPONSETYPE |
+---------+------+-------------+--------------+
| Echo    | Echo | EchoInbound | EchoOutbound |
+---------+------+-------------+--------------+

echo@127.0.0.1:50051> service Echo

echo.Echo@127.0.0.1:50051> call Echo
message (TYPE_STRING) => Awesome Bazel!
{
  "message": "Awesome Bazel!"
}

EchoSeverが正常に起動していることを確認できた。

まとめ

  • gRPCサーバを含むGoプロジェクトをBazelでビルドする方法をまとめた
  • Protocol Bufferを生成するruleもありGoプロジェクト周辺のビルドユースケースが整っている

コード

github.com