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.
> 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 |
> 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:
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)
}
{
"mmessage": "Hello",
"name": ":name"
}
Bazel
Bazel is a little more difficult to explain.
.
├── 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.
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
FROM l.gcr.io/google/bazel:latest
WORKDIR /build
COPY . /build
RUN bazel run //api:api-container
> 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.