05. April 2020

Yesterday I spent some time experimenting with building containerized Go applications. Writing a small restful web service in go is really quite straightforward and there are many better examples out there on the web.
What was interesting for me was seeing how Bazel supports a typical workflow building and running containerized web services. In this case using a small Go program.
Bazel is still relatively new but gaining in popularity. Over the last 6 months or so stability and functionality has really improved. Bazel really shines when combined with a mono-repo approach. For this example the bazel configuration feels more than required to achieve the result.
Components used in Building the Go service container
<a href=“https://github.com/grahambrooks/go-bazel-container" icon:github[role=“blue”>https://github.com/grahambrooks/go-bazel-container Project Source]
<a href=“https://github.com/bazelbuild/rules_go" icon:github[role=“blue”>https://github.com/bazelbuild/rules_go Bazel build rules for Go projects]
<a href=“https://github.com/bazelbuild/bazel-gazelle" icon:github[role=“blue”>https://github.com/bazelbuild/bazel-gazelle Gazelle go dependency management]
<a href=“https://github.com/bazelbuild/rules_docker" icon:github[role=“blue”>https://github.com/bazelbuild/rules_docker Bazel build rules for docker]
Pre-requisites
Up and Running
> docker images #<1>
REPOSITORY TAG IMAGE ID CREATED SIZE
> bazel run //api:api-container --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 # <2>
INFO: Analyzed target //api:api-container (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //api:api-container up-to-date:
bazel-bin/api/api-container-layer.tar
INFO: Elapsed time: 0.605s, Critical Path: 0.01s
INFO: 0 processes.
INFO: Build completed successfully, 1 total action
INFO: Build completed successfully, 1 total action
932da5156413: Loading layer [==================================================>] 3.062MB/3.062MB
dffd9992ca39: Loading layer [==================================================>] 15.44MB/15.44MB
43babe50bd4f: Loading layer [==================================================>] 6.246MB/6.246MB
84ff92691f90: Loading layer [==================================================>] 10.24kB/10.24kB
Loaded image ID: sha256:525fa22e2c6beccba45e2a0dbc7370d7d808342785d6d8b825096ce0a338279f
Tagging 525fa22e2c6beccba45e2a0dbc7370d7d808342785d6d8b825096ce0a338279f as bazel/api:api-container
> docker images # <3>
REPOSITORY TAG IMAGE ID CREATED SIZE
bazel/api api-container 525fa22e2c6b 50 years ago 23.1MB
> docker run -p 8080:8080 bazel/api:api-container <4>
Current list of installed images - none
Run bazel to create and tag the container. Not the target flag - because the api will be running in a linux container we need to build a binary that will work in the environement
The container image is now available in my local docker install.
Fire up the application container and expose port 8080
Testing the container
> curl http://localhost:8080/hello/Everyone
{"Message":"Hello","Name":"Everyone"}
This short blog records my experiments in building a container using Bazel containing a Go application.
is a simple JSON service that accepts /hello/:name and returns a json response with a message and the name given in the request URL:
The full program
include::https://raw.githubusercontent.com/grahambrooks/go-bazel-container/master/api/main.go[]
Expected JSON response
{
"mmessage": "Hello",
"name": ":name"
}
Bazel is a little more difficult to explain.
Project layout
.
├── BUILD.bazel <2>
├── Dockerfile <6>
├── WORKSPACE <1>
├── api
│ ├── BUILD.bazel <3>
│ └── main.go <4>
├── go.mod <5>
└── go.sum
The project workspace. Loads the Bazel Skylark components and go dependencies for the project
Helper build for Gazelle dependency management
The API build script including targets for the API, container image and container
API service source
Go module dependencies. If the go.mod file is changed you can update the bazel dependencies with bazel run //:gazelle — update-repos -from_file=go.mod
Containerized Bazel build
It is both a build system and an environment for plugins supporting application build.
Bazel build rules for go
Gazelle is used to generate Bazel dependencies from the go.mod file. These dependencies appear in the bazel WORKSPACE file in the root of the project and then referenced in the BUILD.bazel files for each module.
Building software in a container has several advantages. Docker container images are just files and those files can be created without docker. The Bazel docker components don’t need docker installed to build an image. Using the same command line
Dockerfile
include::https://raw.githubusercontent.com/grahambrooks/go-bazel-container/master/Dockerfile[]
Running the containerized build
> docker build . # <1>
Sending build context to Docker daemon 107.5kB
Step 1/4 : FROM l.gcr.io/google/bazel:latest
---> dc530fa1c5ce
Step 2/4 : WORKDIR /build
---> Using cache
---> 9e3158821eed
Step 3/4 : COPY . /build
---> 3d309ccc2b35
Step 4/4 : RUN bazel run //api:api-container <2>
---> Running in 7d632d5bb256
Extracting Bazel installation...
Starting local Bazel server and connecting to it...
... omnitted logs ...
[0 / 35] [Prepa] Creating source manifest for @io_bazel_rules_docker//container/go/cmd/create_image_config:create_image_config [for host]
[36 / 45] GoStdlib external/io_bazel_rules_go/linux_amd64_pure_stripped/stdlib%/pkg; 4s processwrapper-sandbox
[36 / 45] GoStdlib external/io_bazel_rules_go/linux_amd64_pure_stripped/stdlib%/pkg; 14s processwrapper-sandbox
Target //api:api-container up-to-date:
bazel-bin/api/api-container-layer.tar <3>
INFO: Elapsed time: 64.211s, Critical Path: 19.27s
INFO: 27 processes: 27 processwrapper-sandbox.
INFO: Build completed successfully, 45 total actions
INFO: Running command line: bazel-bin/api/api-container.executable
INFO: Build completed successfully, 45 total actions
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? <4>
The command '/bin/sh -c bazel run //api:api-container --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64' returned a non-zero code: 1
Running docker instead of bazel to build the project
Build and runtimes match so we don’t have to specify the platform for the bazel build
The container file is created but because we are running in a container without docker the
image is not loaded or tagged
The bazel docker rules provide ways to upload the built image - something to look into next …
There’s a lot more to experiment with and this article only scratches the surface.
Building container images without having to install Docker simplifies the build process
Running the build in a container minimizes local development configuration management and keeps project isolated. This is particularly important when projects depend on different runtimes.