Carpe Diem

備忘録

Bazelを使ってみる その4(gRPCのビルド)

概要

Bazel解説第4弾です。

Bazelを使ってみる その1(Goのビルド) - Carpe Diem
Bazelを使ってみる その2(protobufのビルド) - Carpe Diem
Bazelを使ってみる その3(docker imageのビルド) - Carpe Diem

今回はgRPCをビルドしてみます。

gRPCは2通りの設定方法があります。

今回は簡単なgazelleのやり方で説明します。

環境

  • Bazel v4.2.2

準備

gRPCコード準備

まずgRPCのリポジトリを用意します。

ディレクトリ構造

以下のディレクトリ構造で、

├── client
│   └── main.go
├── go.mod
├── go.sum
├── proto
│   └── helloworld.proto
└── server
    └── main.go

proto/helloworld.proto

Unary callなgRPCを用意します。

syntax = "proto3";

package helloworld;

option go_package = "github.com/jun06t/bazel-sample/grpc/proto;helloworld";

service Greeter {
  rpc SayHello(HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

server/main.go

protoに依存させてますがこの段階では.pb.goがないので依存関係でWarning等出ると思います。
go buildしてもコケるでしょう。

package main

import (
    "context"
    "log"
    "net"

    pb "github.com/jun06t/bazel-sample/grpc/proto"
    "google.golang.org/grpc"
)

const (
    port = ":8080"
)

type server struct{}

func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatal(err)
    }

    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    err = s.Serve(lis)
    if err != nil {
        log.Fatal(err)
    }
}

client

こちらも同様にClientの.pb.goがないのでWarningが出るでしょうしビルドもエラーになります。

package main

import (
    "context"
    "log"

    pb "github.com/jun06t/bazel-sample/grpc/proto"
    "google.golang.org/grpc"
)

const (
    address = "localhost:8080"
)

func main() {
    conn, err := grpc.Dial(address, grpc.WithInsecure())
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    c := pb.NewGreeterClient(conn)

    req := &pb.HelloRequest{
        Name: "alice",
    }
    resp, err := c.SayHello(context.Background(), req)
    if err != nil {
        log.Fatal(err)
    }
    log.Println("Reply: ", resp.Message)
}

Bazel準備

WORKSPACE

こちらはいつもの設定にprotobufのときと同じ設定を追記します。

http_archive(
    name = "com_google_protobuf",
    sha256 = "d0f5f605d0d656007ce6c8b5a82df3037e1d8fe8b121ed42e536f569dec16113",
    strip_prefix = "protobuf-3.14.0",
    urls = [
        "https://mirror.bazel.build/github.com/protocolbuffers/protobuf/archive/v3.14.0.tar.gz",
        "https://github.com/protocolbuffers/protobuf/archive/v3.14.0.tar.gz",
    ],
)

load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps")
protobuf_deps()

rootのBUILD.bazel

gRPCは依存が多いので別ファイルで生成できるように-to_macro=deps.bzl%go_dependenciesオプションを付けます。
また依存ライブラリのprotoを再ビルドしないように-build_file_proto_mode=disable_globalオプションもデフォルトで付けるようにします。

load("@bazel_gazelle//:def.bzl", "gazelle")

# gazelle:prefix github.com/jun06t/bazel-sample/grpc
gazelle(name = "gazelle")

gazelle(
    name = "gazelle-update-repos",
    args = [
        "-from_file=go.mod",
        "-to_macro=deps.bzl%go_dependencies",
        "-prune",
        "-build_file_proto_mode=disable_global",
    ],
    command = "update-repos",
)

この状態で一度

$ bazel run //:gazelle

すると、以下のように./proto/BUILD.bazelが配置されます。

./proto/BUILD.bazel

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

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

go_proto_library(
    name = "helloworld_go_proto",
    compilers = ["@io_bazel_rules_go//proto:go_grpc"],
    importpath = "github.com/jun06t/bazel-sample/grpc/proto",
    proto = ":helloworld_proto",
    visibility = ["//visibility:public"],
)

go_library(
    name = "proto",
    embed = [":helloworld_go_proto"],
    importpath = "github.com/jun06t/bazel-sample/grpc/proto",
    visibility = ["//visibility:public"],
)

protobufの時と違って

compilers = ["@io_bazel_rules_go//proto:go_grpc"],

がありますね。

./server/BUILD.bazel

サーバは以下のように生成されました。

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

go_library(
    name = "server_lib",
    srcs = ["main.go"],
    importpath = "github.com/jun06t/bazel-sample/grpc/server",
    visibility = ["//visibility:private"],
    deps = [
        "//proto",
        "@org_golang_google_grpc//:go_default_library",
    ],
)

go_binary(
    name = "server",
    embed = [":server_lib"],
    visibility = ["//visibility:public"],
)

deps//protoに依存することで、先程解決できなかった依存関係をビルド時に解決させます。
クライアントも同様です。

ビルド

ではサーバ、クライアントをそれぞれ実行してみます。

サーバ

$ bazel run //server

Starting local Bazel server and connecting to it...
INFO: Analyzed target //server:server (140 packages loaded, 9306 targets configured).
INFO: Found 1 target...
Target //server:server up-to-date:
  bazel-bin/server/server_/server
INFO: Elapsed time: 116.644s, Critical Path: 24.24s
INFO: 341 processes: 13 internal, 328 darwin-sandbox.
INFO: Build completed successfully, 341 total actions
INFO: Build completed successfully, 341 total actions

クライアント

$ bazel run //client

INFO: Analyzed target //client:client (1 packages loaded, 3 targets configured).
INFO: Found 1 target...
Target //client:client up-to-date:
  bazel-bin/client/client_/client
INFO: Elapsed time: 1.476s, Critical Path: 0.88s
INFO: 5 processes: 3 internal, 2 darwin-sandbox.
INFO: Build completed successfully, 5 total actions
INFO: Build completed successfully, 5 total actions
2022/01/06 19:35:02 Reply:  Hello alice

ちゃんとReplyが返ってきました。

ただし.pb.goサンドボックスにしかないです。

サンプルコード

今回の成果物はこちら

github.com

まとめ

Bazelを使ったgRPCのビルドを行いました。

  • gazelleのおかげでほぼいじる必要がない
  • protoの依存関係も解決される
    • ただし.pb.goサンドボックスに生成されるため、エディタなどでは依存関係が解決されないので別途対応が必要

といった点が分かりました。