Deterministic container images for gRPC+golang bazel.
The following sample will build a golang gRPC client/server and then embed the binaries into container images.
These images are will have a consistent image hash no matter where it is built
greeter_server@sha256:e53d590865d15575c9fed02170ebe8c77cae3d9ddb2ba99583ee41c059768fc1greeter_client@sha256:16b185fc504ba82aadf210f1c69d6b2e5114cb816c2156746280ec51508b31c9For reference, see:
You can find the source here

To run this sample, you will need bazel installed (see Cloud Shell for an easy way to use bazel)
In the end, you’ll end up with the same digests
$ docker pull salrashid123/greeter_server:greeter_server_image
$ docker inspect salrashid123/greeter_server:greeter_server_image
[
    {
        "RepoTags": [
            "bazel/greeter_server:greeter_server_image",
            "salrashid123/greeter_server:greeter_server_image"
        ],
        "RepoDigests": [
            "salrashid123/greeter_server@sha256:e53d590865d15575c9fed02170ebe8c77cae3d9ddb2ba99583ee41c059768fc1"
        ],
$ docker pull salrashid123/greeter_client:greeter_client_image
$ docker inspect salrashid123/greeter_client:greeter_client_image
[
    {
        "RepoTags": [
            "bazel/greeter_client:greeter_client_image",
            "salrashid123/greeter_client:greeter_client_image"
        ],
        "RepoDigests": [
            "gcr.io/mineral-minutia-820/greeter_client@sha256:16b185fc504ba82aadf210f1c69d6b2e5114cb816c2156746280ec51508b31c9"
        ],
The easiest way here it to run bazel in docker using the provided image.
[i know,its weird but the only thing we’re using docker here for is for bazel…the build still happens deterministically]
git clone https://github.com/salrashid123/go-grpc-bazel-docker.git
cd go-grpc-bazel-docker
$ docker version
    Client: Docker Engine - Community
    Version:           20.10.12
    Server: Docker Engine - Community
    Engine:
      Version:          20.10.2
# server
docker run \
  -e USER="$(id -u)" \
  -v `pwd`:/src/workspace \
  -v /tmp/build_output:/tmp/build_output \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -w /src/workspace \
  gcr.io/cloud-builders/bazel@sha256:4a19236baf0e5d663942c3947497e3f5b5356ae3dd6f97b1fae92897a97a11ad \
  --output_user_root=/tmp/build_output \
  run  --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 greeter_server:greeter_server_image
# client
docker run \
  -e USER="$(id -u)" \
  -v `pwd`:/src/workspace \
  -v /tmp/build_output:/tmp/build_output \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -w /src/workspace \
  gcr.io/cloud-builders/bazel@sha256:4a19236baf0e5d663942c3947497e3f5b5356ae3dd6f97b1fae92897a97a11ad \
  --output_user_root=/tmp/build_output \
  run  --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 greeter_client:greeter_client_image
Note, the bazel version used here is
$ docker run gcr.io/cloud-builders/bazel@sha256:4a19236baf0e5d663942c3947497e3f5b5356ae3dd6f97b1fae92897a97a11ad version
    Build label: 5.0.0
    Build target: bazel-out/k8-opt/bin/src/main/java/com/google/devtools/build/lib/bazel/BazelServer_deploy.jar
    Build time: Wed Jan 19 14:08:54 2022 (1642601334)
    Build timestamp: 1642601334
    Build timestamp as int: 1642601334
If you have access to Google Cloud Platform account, you can use Cloud Shell to run bazel and save yourself an installation.
gcloud alpha cloud-shell ssh 
git clone https://github.com/salrashid123/go-grpc-bazel-docker.git
cd go-grpc-bazel-docker
Then within the shell, you should be able to bazel version to ensure it is installed.
Declare go dependencies from go.mod into repositories.bzl using gazelle:
$ bazel --version
bazel 5.0.0
bazel run :gazelle -- update-repos -from_file=go.mod -prune=true -to_macro=repositories.bzl%go_repositories
Then build the client and server
bazel build --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 greeter_server:all
bazel run  --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 greeter_server:greeter_server_image
bazel build --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 greeter_client:all
bazel run  --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 greeter_client:greeter_client_image
Note, the BUILD.bazel files for the client and server targets is set for a specific arch and os. eg
go_binary(
    name = "server",
    embed = [":go_default_library"],
    visibility = ["//visibility:public"],
    goos = "linux", 
    goarch = "amd64",    
)
The output of the commands above will yield
$ docker images
    REPOSITORY             TAG                    IMAGE ID       CREATED        SIZE
    bazel/greeter_client   greeter_client_image   f77d61a81826   52 years ago   31.5MB
    bazel/greeter_server   greeter_server_image   67ccf97f9421   52 years ago   31.6MB
Inspect the image thats generated.  The hash we’re after is actually RepoTags which we’ll generate and show later, for now
(why not, you already built it)
docker run -p 50051:50051 bazel/greeter_server:greeter_server_image --grpcport :50051
docker run --network="host" bazel/greeter_client:greeter_client_image --host localhost:50051 -skipHealthCheck 
with bazel
bazel run --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 greeter_server:server -- --grpcport :50051
bazel run --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 greeter_client:client -- --host localhost:50051 -skipHealthCheck
with go
You will first want to build the files, see corresponding steps above
Then in go.mod:
module main
go 1.17
require (
	github.com/golang/protobuf v1.4.3 // indirect
	github.com/google/uuid v1.3.0 // indirect
	golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
	golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
	golang.org/x/text v0.3.7 // indirect
	google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
	google.golang.org/grpc v1.44.0 // indirect
	google.golang.org/protobuf v1.25.0 // indirect
	github.com/salrashid123/go-grpc-bazel-docker/echo v0.0.0
)
replace github.com/salrashid123/go-grpc-bazel-docker/echo => ./echo
then
go run greeter_server/main.go --grpcport :50051 
go run greeter_client/main.go \
  --host localhost:50051 \
  -skipHealthCheck
Specify a docker repo to by setting the repository command here. In the case below, its container registry gcr.io/project_id
container_image(
    name = "greeter_server_image",
    base = "@alpine_linux_amd64//image",
    entrypoint = ["/server"],
    files = [":server"],
    repository = "gcr.io/PROJECT_ID`"
)
on push to a repo
Server$ bazel build --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 greeter_server:all
$ bazel run  --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 greeter_server:greeter_server_image
$ docker push gcr.io/PROJECT_ID/greeter_server:greeter_server_image
you’ll see the hash we need…this is specific and intrinsic to the image.
On any other machine, generate the builds and inspect
$ docker pull salrashid123/greeter_server:greeter_server_image
$ docker inspect gcr.io/PROJECT_ID/greeter_server:greeter_server_image
You can use Cloud Build to create the image by using the bazel builder and specifying the repository path to export to.  In the sample below, the repository is set o google container registry:
container_image(
    name = "greeter_server_image",
    base = "@alpine_linux_amd64//image",
    entrypoint = ["/server"],
    files = [":server"],
    repository = "gcr.io/PROJECT_ID"
)
Note that cloudbuild.yaml specifies the base bazel version by hash too
steps:
- name: gcr.io/cloud-builders/bazel@sha256:4a19236baf0e5d663942c3947497e3f5b5356ae3dd6f97b1fae92897a97a11ad
  args: ['run', '--platforms=@io_bazel_rules_go//go/toolchain:linux_amd64', 'greeter_server:greeter_server_image']
images: ['gcr.io/$PROJECT_ID/greeter_server:greeter_server_image']
$ bazel clean
$ gcloud builds submit --config=cloudbuild.yaml --machine-type=n1-highcpu-32
    INFO: Elapsed time: 76.945s, Critical Path: 17.73s
    Loaded image ID: sha256:6dac89d9fe7ae4fb25130b14fc35bae1ac939a58911242ba7d0c346290fc89f3
    Tagging 6dac89d9fe7ae4fb25130b14fc35bae1ac939a58911242ba7d0c346290fc89f3 as gcr.io/mineral-minutia-820/greeter_server:greeter_server_image
    PUSH
    Pushing gcr.io/mineral-minutia-820/greeter_server:greeter_server_image
    greeter_server_image: digest: sha256:e53d590865d15575c9fed02170ebe8c77cae3d9ddb2ba99583ee41c059768fc1 size: 948
    DONE
    --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
    ID                                    CREATE_TIME                DURATION  SOURCE                                                                                             IMAGES                                                          STATUS
    0180d5ae-314c-462f-bb5a-c4c670eb8ad8  2022-08-02T09:27:47+00:00  2M10S     gs://mineral-minutia-820_cloudbuild/source/1659432466.081614-00236c466710478d97ed731cb9fec677.tgz  gcr.io/mineral-minutia-820/greeter_server:greeter_server_image  SUCCESS
The WORKSPACE and git dependencies are all known down to the specific version of bazel and base container image
WORKSPACEThe base image used for the client and server is distroless:
container_pull(
    name = "distroless_base",
    digest = "sha256:75f63d4edd703030d4312dc7528a349ca34d48bec7bd754652b2d47e5a0b7873",
    registry = "gcr.io",
    repository = "distroless/base",
)
The default bazel configuration in echo/BUILD.bazel compiles the proto files.  If you would rather use pregenerated proto files (eg, to avoid conflicts, you must do that outside of bazel and just specify a library)
A) Generate proto.pb:
/usr/local/bin/protoc -I ./echo  \
  --include_imports --include_source_info \
  --descriptor_set_out=echo/echo.proto.pb \
  --go_opt=paths=source_relative \
  --go_out=plugins=grpc:./echo/ echo/echo.proto
B) comment the local replace directives in go.mod:
module main
go 1.17
require (
	github.com/golang/protobuf v1.4.3 // indirect
	github.com/google/uuid v1.3.0 // indirect
	golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
	golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
	golang.org/x/text v0.3.7 // indirect
	google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
	google.golang.org/grpc v1.44.0 // indirect
	google.golang.org/protobuf v1.25.0 // indirect
	// github.com/salrashid123/go-grpc-bazel-docker/echo v0.0.0
)
// replace github.com/salrashid123/go-grpc-bazel-docker/echo => ./echo
C): Edit echo/BUILD.bazel
Enable the rule that uses echo.pb.go and disable the rest:
# proto_library(
#     name = "echo_proto",
#     srcs = ["echo.proto"],
#     visibility = ["//visibility:public"],
# )
# go_proto_library(
#     name = "echo_go_proto",
#     compiler = "@io_bazel_rules_go//proto:go_grpc",
#     compilers = ["@io_bazel_rules_go//proto:go_grpc"],
#     importpath = "github.com/salrashid123/go-grpc-bazel-docker/echo",
#     proto = ":echo_proto",
#     visibility = ["//visibility:public"],
# )
# go_library(
#     name = "go_default_library",
#     embed = [":echo_go_proto"],
#     importpath = "echo",
#     visibility = ["//visibility:public"],
# )
go_library(
    name = "echo_go_proto",
    srcs = [
        "echo.pb.go",
    ],
    importpath = "github.com/salrashid123/go-grpc-bazel-docker/echo",
    visibility = ["//visibility:public"],
    deps = [
        "@com_github_golang_protobuf//proto:go_default_library",
        "@org_golang_google_protobuf//reflect/protoreflect:go_default_library",
        "@org_golang_google_protobuf//runtime/protoimpl:go_default_library",
        "@org_golang_google_grpc//:go_default_library",
        "@org_golang_google_grpc//codes:go_default_library",
        "@org_golang_google_grpc//status:go_default_library",
        "@org_golang_google_grpc//credentials:go_default_library",                          
        "@org_golang_x_net//context:go_default_library",         
    ],
)
D)  Run gazelle to populate dependencies in repositories.bzl:
bazel run :gazelle -- update-repos -from_file=go.mod -prune=true -to_macro=repositories.bzl%go_repositories
The focus of this repo is to use bazel to build and run. …but if you want to manually build the proto and use go,
$ protoc --version
  libprotoc 3.19.1
$ go version
   go version go1.17.1 linux/amd64
$ bazel version
  Build label: 5.0.0
# as go, optionally compile
/usr/local/bin/protoc -I ./echo  \
  --include_imports --include_source_info \
  --descriptor_set_out=echo/echo.proto.pb \
  --go_opt=paths=source_relative \
  --go_out=plugins=grpc:./echo/ echo/echo.proto
Edit go.mod and uncomment the local imports.  The file should look like
module main
go 1.17
require (
	github.com/golang/protobuf v1.4.3 // indirect
	github.com/google/uuid v1.3.0 // indirect
	golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
	golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
	golang.org/x/text v0.3.7 // indirect
	google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 // indirect
	google.golang.org/grpc v1.44.0 // indirect
	google.golang.org/protobuf v1.25.0 // indirect
	github.com/salrashid123/go-grpc-bazel-docker/echo v0.0.0
)
replace github.com/salrashid123/go-grpc-bazel-docker/echo => ./echo
then,
go run greeter_server/main.go --grpcport :50051  
go run greeter_client/main.go --host localhost:50051
This site supports webmentions. Send me a mention via this form.