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.

Pre-requisites
  • Install Docker (required for both local and container builds) Docker

  • Bazel (required for local build) Bazel

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)
1 Current list of installed images - none
2 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
3 The container image is now available in my local docker install.
4 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.

The API

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
package main

import (
	"encoding/json"
	"github.com/gorilla/mux"
	"log"
	"net/http"
)

func main() {
	router := mux.NewRouter().StrictSlash(true)
	router.HandleFunc("/hello/{name}", helloHandler).Methods("GET")

	log.Fatal(http.ListenAndServe(":8080", router))
}

type helloResponse struct {
	Message string
	Name string
}

func helloHandler(writer http.ResponseWriter, request *http.Request) {
	writer.Header().Set("Content-Type", "application/json")
	vars := mux.Vars(request)
	r := helloResponse{
		Message: "Hello",
		Name:    vars["name"],
	}

	encoder := json.NewEncoder(writer)
	_ = encoder.Encode(r)
}
Expected JSON response
{
  "mmessage": "Hello",
  "name": ":name"
}

Bazel

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
1 The project workspace. Loads the Bazel Skylark components and go dependencies for the project
2 Helper build for Gazelle dependency management
3 The API build script including targets for the API, container image and container
4 API service source
5 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
6 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 a container in a container

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
FROM l.gcr.io/google/bazel:latest

WORKDIR /build
COPY . /build

RUN bazel run //api:api-container
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
1 Running docker instead of bazel to build the project
2 Build and runtimes match so we don’t have to specify the platform for the bazel build
3 The container file is created but because we are running in a container without docker the
4 image is not loaded or tagged

The bazel docker rules provide ways to upload the built image - something to look into next …​

Wrapping up

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.