This repository demonstrates a modern monorepo setup for building scalable gRPC microservices in Go, using Bazel for reproducible builds and dependency management. It provides a reference architecture for teams looking to:
- Share protobuf definitions and implementations across services
- Use Bazel for fast, hermetic builds and CI/CD
- Integrate gRPC, REST (via grpc-gateway), and OpenAPI/Swagger documentation
- Deploy services to Kubernetes and container registries
The example service, helloworld
, showcases the recommended project structure, build rules, and development workflow.
- Monorepo structure for multiple Go microservices
- gRPC and grpc-gateway for both gRPC and RESTful APIs
- Bazel for fast, reproducible builds and dependency management
- Protobuf definitions and code generation
- OpenAPI/Swagger documentation generation
- Docker/OCI image builds and publishing
- Kubernetes deployment manifests and scripts
- CI/CD with GitHub Actions
Follow these steps to get the example service running locally:
-
Install Prerequisites
-
Clone the Repository
git clone https://github.com/esurdam/go-grpc-bazel-example.git cd go-grpc-bazel-example
-
Generate Protobuf and Build Files
make link # Generates protobuf Go code for local development (optional)
-
Run Tests
make test
-
Run the Example Service Locally
- Generate self-signed TLS certificates:
mkdir -p ssl go run $GOROOT/src/crypto/tls/generate_cert.go --rsa-bits 2048 --host 127.0.0.1,::1,localhost,localhost:4443 --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h -certfile ssl/cert.pem -keyfile ssl/key.pem
- Start the service:
bazel run //services/helloworld:helloworld -- -http-port 4443 -cert $(pwd)/ssl/cert.pem -key $(pwd)/ssl/key.pem
- Test with cURL:
curl -X POST -k https://localhost:4443/v1/greeter -d '{"name": "TestName"}'
- Generate self-signed TLS certificates:
-
View Swagger/OpenAPI Docs
- Open https://localhost:4443/swagger.json in your browser.
For more details, see the sections below.
.github/ # github Action CI configs
ci/ # contains ci/automation scripts
cmd/ # command line tool entrypoints
pb/ # contains all proto definitions and gen output
pkg/ # contains proto implementations
services/ # entrypoints for kubernetes defined microservices
tools/ # tool versioning
BUILD # root bazel BUILD definitions; aggregates services
WORKSPACE # bazel workspace rules; external code
- Go (>= 1.23)
- Bazel (tested with 8.2.1)
- Create proto file and define types/service:
touch pb/helloworld/helloworld.proto # Add definitions to this file
- Generate
BUILD.bazel
files and add generated files locally:make gazelle make link
- Implement proto service in
pkg
:mkdir -p pkg/helloworld/server touch pkg/helloworld/server/server.go
- Create service entrypoint in
services
:mkdir services/helloworld touch services/helloworld/main.go
- Define kubernetes service in
ci/services
:touch ci/services/helloworld.yaml
- Add the service definition to aggregate rule in
BUILD
.
Example service scaffold:
project
│
└───ci
│ └───services
│ │ helloworld.yaml
│
└───pb
│ └───helloworld
│ │ helloworld.proto
│
└───pkg
│ └───helloworld
│ └───server
│ │ server.go
|
└───services
│ └───helloworld
│ │ main.go
|
Run make gazelle
to generate/update BUILD files (which include test and binaries). This also updates the WORKSPACE with required deps. BUILD.bazel files located in pb directory will contain grpc rules.
Use JetBrains/VSCode plugin to handle the Bazel workspace to not require local files for development.
Generated files don't necessarily need to be checked in to repo. Bazel will handle generating the pb file during build.
It is generally a good idea to track changes to pb in repo.
make link
To run all tests:
make test
Test individual package:
bazel test --@rules_go//go/config:race \
--verbose_failures \
--test_output=errors \
//pkg/helloworld/server:server_test
Tests can also be aggregated into test groups to be tested at once.
Generate a self-signed certificate (cert.pem & key.pem) to run services locally. Required for multiplexing grpc/http2 over single port.
mkdir -p ssl && \
(cd ssl && \
go run $GOROOT/src/crypto/tls/generate_cert.go --rsa-bits 2048 --host 127.0.0.1,::1,localhost,localhost:443,localhost:4443 --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h)
Use bazel to run the service
bazel run //services/helloworld:helloworld -- -http-port 4443 -cert $(pwd)/ssl/cert.pem -key $(pwd)/ssl/key.pem
Then we use cURL to send HTTP requests
curl -X POST -k https://localhost:4443/v1/greeter -d '{"name": "TestName"}'
You can view the swagger at https://localhost:4443/swagger.json
Or use the client:
With the server running, you can test command line tools from cmd
.
$ bazel run //cmd/helloworld-client -- \
--name "Beutiful" \
--server-addr localhost:4443 \
--ca-cert $(pwd)/ssl/cert.pem
INFO: Analyzed target //cmd/helloworld-client:helloworld-client (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //cmd/helloworld-client:helloworld-client up-to-date:
bazel-bin/cmd/helloworld-client/helloworld-client_/helloworld-client
INFO: Elapsed time: 0.365s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/cmd/helloworld-client/helloworld-client_/helloworld-client --name 'Beutiful' --server-addr localhost:4443 --cert ...
INFO: Build completed successfully, 1 total action
2022/10/22 18:03:59 message:"Hello Beutiful!"
oci_load(
name = "load",
# Use the image built for the target platform
image = ":transitioned_image",
repo_tags = ["ghcr.io/esurdam/go-grpc-bazel-example/cmd/helloworld-client:latest"],
)
For example, to load tarball with current architecture:
bazel run //services/helloworld:load
# Run the loaded image
docker run --rm -v $(pwd)/ssl:/ssl -p 4443:4443 ghcr.io/esurdam/go-grpc-bazel-example/services/helloworld:latest --http-port 4443 --cert /ssl/cert.pem --key /ssl/key.pem
arch example:
bazel run \
--platforms=@rules_go//go/toolchain:linux_amd64 \
--cpu=k8 \
//services/helloworld:load
Services may utilize grpc-gateway
for a JSON-to-GRPC proxy. This is autogenerated with the gateway_grpc_library
rule. Swagger json is autogenerated via the gateway_openapiv2_compile
rule.
load("@rules_proto_grpc//grpc-gateway:defs.bzl", "gateway_grpc_compile", "gateway_grpc_library", "gateway_openapiv2_compile")
gateway_grpc_library(
name = "helloworld_gateway_lib_proto",
importpath = "github.com/esurdam/go-grpc-bazel-example/pb/helloworld",
protos = [":helloworld_proto"],
visibility = ["//visibility:public"],
)
gateway_openapiv2_compile(
name = "helloworld_gateway_grpc",
protos = [":helloworld_proto"],
visibility = ["//visibility:public"],
)
Services can then use the embed
rule to embed the swagger.json
compiled output.
View the genrule
in BUILD.bazel
//go:embed helloworld_openapi_swagger.json
var Data []byte
Services can then expose the swagger.json
file directly.
Used to deploy a service to the container registry.
Each service should contain a oci_push
rule, which defines the container registry and stamped image. The :transitioned_image
contains multi-arch capabilites.
oci_push(
name = "push",
image = ":transitioned_image",
remote_tags = ":stamped",
repository = "ghcr.io/esurdam/go-grpc-bazel-example/cmd/helloworld-client",
visibility = ["//visibility:public"],
)
To push an individual service (where version is the container label):
Iterates through all :push
targets and pushes to the container registry.
make push
e.g.
bazel run --platforms=@rules_go//go/toolchain:linux_amd64 \
--cpu=k8 \
//services/helloworld:push
Since rules_docker has been deprecated, we can no longer use the k8s_deploy
rule to deploy to k8s. Instead, we can use the oci_push
rule to push the image to the container registry, and then use kubectl
to apply the deployment.
Iterates through all :push
targets, uses the stamp to update the image tag and applies the k8s deployment.
make deploy
See ci/deploy.sh
GRPC
Bazelbuild rules