Golang monorepo part 2

Nikunj Yadav
goc0de
Published in
4 min readJun 2, 2020

As I promised in the last post about How to golang monorepo, we will talk about how to use Grpc in golang + bazel and customize the bazel docker image setup a little bit.

Key Terms

Grpc is a RPC framework developed at Google, and if you are writing services then I highly recommend you use it. Please read this for introduction about grpc.

Protocol Buffers are a way of serializing data and sending from one service to another. If you are writing a service in a polyglot monorepo then you can’t go wrong with using protocol buffers because they are a language agnostic way of defining a message which can be used across different languages.

Write your first grpc service

We will continue in our monorepo. In the last post we wrote a hello service that was a regular http service. Http services work fine but we want to write a hello service with self documenting, strict contracts, so lets convert it to grpc:

In this file you have a defined a service Hello on line 4, it has one method Greet that takes in GreetingRequest as input and outputs GreetingResponse which are defined on line #8 and #12 respectively.

Now if you run the magic incantation like our last post bazel run //:gazelle you get

Things to note here:

  1. proto_library is the bazel way to define a library of protocol buffers. Other language target can depend on this common target.
  2. go_proto_library is the go implementation that will generate .go files for the your proto implementation.
  3. If you had other languages you would write those targets here, ex: you could write a java_proto_library

Setting up WORKSPACE

In order for your bazel setup to know about proto compiler proto and actually compile the above, you will have to add a snippet to your WORKSPACE file like this.

Lets try compiling

To compile the hello_go_proto target you will need couple of more dependencies, declared here.

Now this should work bazel build //protos/hello:hello_go_proto

That build command in the background actually generates .go files and places them in bazel-bin folder.

Implement the GRPC service

Implementing the grpc service is easy, you just implement the golang interface which you can find in the autogenerated code. Let’s change our old hello http server

Change our main.go to construct hello server and run on a port.

Couple of things to note:

  1. You can construct grpc.Server with multiple grpc options like adding a logging or tracing interceptor. In this snippet at line #7 I am not using any option right now.
  2. Register the implementation to the interface in line #9
  3. We start a listener and call grpc.Server
  4. waitForShutdown will call server.GracefulStop

Run bazel run //:gazelle and then you can build the service like this bazel build //services/hello

You can find the working version in this commit.

Manage bazel code

I briefly want to talk about how bazel code can be organized. You don’t have to follow this advice but know that in the futureWORKSPACE file will look a lot shorter from now on.

If your golang code resides in a monorepo and you are following my post, you will have a pretty long(ish) WORKSPACE file. Because there are going to be different customizations for different languages, let’s try to break up the WORKSPACE file into modular pieces. We will separate rules_go stuff into one package and docker into another.

  1. repos.bzl for a package will have dependencies like http_archive and git_repository
  2. def.bzl for a package will call the relevant functions to add the functionality

Then you call the methods from these two files into WORKSPACE

Here are two commits for moving golang stuff and docker bazel stuff

Customizing docker images

As I talked about in part 1, docker image that we produce for hello is built on top of distroless.

One of the features of distroless is that you can’t bash into your container. It is actually a good security feature but at least in your dev environment I have had the need to shell into my container. To achieve this I decided to base a version of hello image off of ubuntu:

Import ubuntu using container_pull in our bazel/docker/def.bzl . I am pulling here from one of the public repos of tensorflow but you can pull it from any other image repository you want. I would also recommend looking into alpine.

Add the method above and make sure to call it

Now we change BUILD.bazel for hello service

Add these two targets to build images you can bash into

And now you should be able to build docker images that you can bash into.

➜  docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
bazel/services/hello image_dev 9496338fcef9 25 hours ago 2.16GB

Lets bash in

➜ docker run -it --entrypoint /bin/bash 9496338fcef9root@ebe363b85118:/app/services/hello/hello.runfiles/__main__# cd /app/services/
root@ebe363b85118:/app/services# ./hello

You will notice that the binary is called hello as opposed to a weird name like base_image.binary which is what you would have gotten if you followed the last post. This is because I specified it here.

You can find this customization on this commit.

References

  1. Grpc Introduction
  2. Distroless, alpine introduction
  3. Experiments with distroless
  4. k8/test-infra repo

--

--