Production deployments often require a container image for a service or application, to be run in a Docker daemon, Kubernetes cluster, or other container-based runtime. Container images are merely archive (tar) files, containing some metadata and layers, each of which is another archive file.
Container images are a great usecase to be built with Bazel, which has advantages over the Dockerfile
/ docker build
workflow. It is more reproducible, and much higher performance since individual layers can be built in parallel.
Build a container with a Go binary
The aspect init
command offered to install support for OCI containers. If you selected Yes for this option, then it added a file .aspect/cli/go_image.star
in the repository. This is a plugin for the configure
command, registered in .aspect/cli/config.yaml
.
When you ran bazel configure
you should see a go_image
rule was automatically written in each BUILD
file next to the go_binary
target. Of course, it’s also possible to write this target by hand.
By default, go_image
uses the base
image from the Distroless project, to minimize dependencies and reduce your exposure to vulnerabilities. See the oci.pull
call in MODULE.bazel
. Read more: Embed GitHub
go_binary(
name = "hello",
embed = [":hello_lib"],
visibility = ["//visibility:public"],
)
go_image(
name = "image",
binary = "hello",
)
Building this target results in a container image written to bazel-out
, as a directory following the OCI image layout specification:
% bazel build cmd/hello:image
INFO: Analyzed target //cmd/hello:image (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //cmd/hello:image up-to-date:
bazel-bin/cmd/hello/image
% ls bazel-bin/cmd/hello/image
blobs index.json oci-layout
Q. Why does it produce a folder rather than a .tar
file?
A. This is because container images can be large, and when a Bazel action produces a large output, it will be stored in the remote cache. This causes expensive and slow network transfer, especially when a large number of image cache entries are invalidated by some code changes.
Running a local container
We’ll load the image data into a locally running daemon, like with Docker or Podman. Make sure you have a container daemon running, and maybe set the DOCKER_HOST
environment variable so that it can be located by commands like docker load
.
The go_image
macro in our BUILD
file expands to several targets, which you can inspect with bazel query
. One is oci_load rule //cmd/hello:image.load
which is executable, so we can load our image by running it.
% bazel run //cmd/hello:image.load
INFO: Analyzed target //cmd/hello:image.load (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //cmd/hello:image.load up-to-date:
bazel-bin/cmd/hello/image.load.sh
INFO: Running command line: bazel-bin/cmd/hello/image.load.sh
Loaded image: localhost/cmd/hello:latest
This printed the image and tag (”latest”) which the daemon has stored. Starting the container then uses the docker
or podman
command like you’d use outside of Bazel, for example:
% podman run --rm localhost/cmd/hello -p 8080:8080
Navigate again to http://localhost:8080 in your browser to access the application.
Pushing to a remote registry
To run the application in a remote environment like on a Cloud service, we’ll need to run a push
command. The oci_push
rule creates an executable target that can be used with bazel run
. See Embed GitHub
However we would need credentials for a registry to perform a push, so this lesson doesn’t include that.
Continuous Delivery
Ideally our go_image
should be pushed to the remote registry any time we’ve landed code changes that affect the binary. This way the developer workflow is simply to merge changes, then run a deployment command that promotes a binary built during CI. This is better than running a push
command locally, because we are guaranteed that the container image is exactly the one that passed all the tests.
See Aspect’s Guide: Modeling Continuous Delivery under Bazel | Aspect Docs for our recommended approach to setting this up.