Conf42 Golang 2024 - Online

When "go build" is not enough: Introduction to Bazel

Abstract

Building software is a complex process in which compilation is one of many steps that can occur. In this presentation, you’ll learn how Bazel can help orchestrate your build process beyond “go build” to provide a consistent experience that is both Fast and Correct.

Summary

  • Yershi Khabarov: Today we will be talking about build process and dependencies. Then we will talk about Bazel open source build and test tool. Let's talk about what Bazel can do in parallel.
  • With Bazel, we'll have just one invocation Bazel run followed by the label. Label is a unique name for a build target. Let's look at another demo. I'm going to build all the binaries, container images and kubernetes manifests with one command and deploy them into Kubernetes cluster.

Transcript

This transcript was autogenerated. To make changes, submit a PR.
Hello everybody, and welcome to my talk when Google Build is not enough. Introduction to Bazel I'm Yershi Khabarov, lead developer at articulf, where I help people to use Bazel. Today we will be talking about build process and dependencies, and then we will talk about Bazel open source build and test tool. So let's start with build go build. What is Go build? Go build is a command I used most of the time for building my Go software, and Gobuild is command which compiles the packages named by their improper along with their dependencies. Go build takes a list of Go files as an argument and produces an exhibit executable binary. The question is, do we have all the go source files from the beginning? Sometimes in our go source code we have directories like gorenarate followed by some arbitrary binary or script. We can have any other commands that produces some additional Go source files. Which means as the first step of our build, we have to run generator, generate those go files, and then we can run go build. And this is the first set of our dependencies where we have go packages, go compiler and generators. Also we can run build on different environments and platforms. We can run our builds locally, it could be run on cellular runner or somewhere else. And this is the second center. The second set of our dependencies where we have host machine is the machine where we run our build. We have target machine is the machine where we build software for. And we can have a bunch of environment variables that can affect our build. Let's say we build binaries. Now publishing we need to make those variables available for deployment tools. Usually in my work I plug those binaries into container images or into target zip archives to use it with AWS lambdas. And as well as I need to provide some yamls, some yaml manifests like kubernetes manifests, helm charts, or cloudformation templates with instructions of how to deploy those artifacts. And this is a third set of our dependencies where we have docker to build container images, customize weight, or any other Yamuna operating tool, AWC, CLI, etcetera. So what is the dependency? Technically, everything involved into a build process or anything can affect our build process is a dependency. How to control our dependencies for go packages we can use go mod and go sum files. For go compiler we can specify version inside the container image we run built inside, or we can use just a random Go version installed on host machine for generators, well, it depends on generator. For platforms we can use build flags or we can run build on a specific platform for environment variables. We can specify them explicitly during the build. For tools like Docker, customize and YT, well, we can just install. What else we should consider during the build process we have to think about is our build reproducible and is our build well validated and hermetic? Sometimes we can use docker images for hermeticity and run our build inside a docker image. And when build fails, can we start the build from the failure point but not from the beginning to save our time? The output for our build process will be artifacts. Among them we will have go binaries, container memories or tarzeep archives. We have to know that we are going to build and publish ready to go artifacts, but not to deploy them. So yobuild is not enough. Gobuild is just one step of the process. While we actually need in the build architect session, let's define our problem scope. So we are going to automate our build process. And by automation I mean as a result we will have one documents for build and publish our artifacts. So this process will include downloading and starting all the necessary dependencies, including generators, compilers, tools, etcetera. In that during this process we will build artifacts and then we'll publish those artifacts to ECR three or somewhere else. Also, we'll try to make this process as little as possible and as reproducible as possible, which means we're going to pin all the versions for all the dependencies we've defined a build process. Let's talk about Bazel. Bazel is an open source built and test tool that uses human readable high level build language to define build in a declarative way, which means to build anything with Bazel we need to write a build configuration. Bazel is aimed to build large codices. So Bazel was created at Google. So when we talk about large cool basis, think about Google scale. Bazel supports multi language and multi partner builds. Definitely. We can build with Bazel many different languages, and Bazel unifies build approaches across multiple languages and multiple tool chains, which means with one Bazel build command we can build applications in written in different languages. Let's talk about what Bazel can do. Bazel can build software and it does it in parallel way. Bazel uses as many codes as it found. We can run build with Bazel locally or remotely, which means if our project is large enough, we can spin up remote runners and build run build there. We can build everything from sources, including dependencies, but it caches all the download dependencies in intermediate build results, which means subsequent build will be much much faster and by the changes in sources and rebuilds change parts only, which means again subsequent builds, even for changes source code will be much faster, much faster than let's talk about a couple of important principles. When given the same input, source code and product configuration, a hermetic build system always returns the same output. Hermetic builds are insensitive to libraries and other software installed on the host machine, which means if we take our build and move it across different machines, the output will be the same for the same input. Source identity Hermit implement system try to ensure the sameness of inputs by using checksums to identify changes to the build inputs. As we will see later in our examples, we'll have checksums for all the dependencies we use. Sandboxing compilers and other tools during the build have the access to explicitly defined inputs only, which means first of all, we need to define all the inputs explicitly, and if we don't define them, build will not see them. Okay, it's demo time. For our demo, I have simple hello world generator. This program will print which we can compile and this code could be found by the link below. So how can use it? As a first step, we need to build and we'll have a binary. Then we can reference this binary from, let's say go generate director. Then we will invoke go generate and create a file with the following content. Then we will go we will run build for this newly newly generated file and then we can so let's see how it looks, how it works with Bazel. With Bazel, we'll have just one invocation Bazel run followed by the label. So what we have here, Bazel is just a binary run is a Bazel subcommand. Then we have a label. Label is a unique name for a build target and this label consists of several parts. First one double slash defines a product, double slash go defines a package and column hello world is a build target. And now let's look how this product will be modified with Bazel. So as you can see here, we don't have any go files, but we have other files. First one is worthless file. It defines portal define referenced by double slash and this file may contain may contain external dependencies. Also have build files build files define the package like double slash or root package or double search go package build files declares zero or more build targets in most cases for goprojects, build packet structure is the same as directory structure. As you remember, I've mentioned human readable high level build image. And this is Starload. Starlock is a subset of python, and it's limited to express configurations. It's not, it's not intended for writing applications. So here is our workforce file. A role set is in a session for Bazel. It's like a plugin that allows Bazel to build different software. In our case, rose Go will be our rule set that allows Bazel to build go software. Then within this file we explicitly find go version and this go version will be downloaded by Bezel. Also in the end of the file we have a gorepository dependency which references our hello world generator. As you can see, all of our dependencies have sum or checksums. Let's look at build files. So build files can have zero or more build targets. Build targets will be defined by rules. So rule is a function implementation. It takes an input and produces an output. We have three rules here, go library and gobindrary. All those rules define targets. Target is a buildable unit. The first rule called generatehallogo, it creates a hollow to go file by invoking our hello world generator generate file and creates a library. And third world go Binary takes our library and creates an executable binary. So what will happen when we execute basel run followed by the label? When we call basel run followed by the label, Bazel will know that it have to build binary first, and to build binary you have to build library first, and to build library you have to generate a loadable file. So when we run Bazel run command, all this, those builds happen automatically. Let's look at another demo. For this demo I have a bunch of mic services deployed into Kubernetes cluster and running with stilt. So we have here service one which is drape service which is listening on port 5000 and it's mapped onto port 55,000 on localhost. Then we have service Oz. This service will be authorizing all our coming requests. And third service is envoy proxy that acts as an paid gateway port 8084 and wei proxy mapped into allocost and it accepts HTTP requests in a boy will translate those HTTP requests into JPC requests and we'll call our service one. I'm going to build all the binaries, container images and kubernetes manifests with one command and deploy them into Kubernetes cluster. Let's see how it could be done and deploys all these services. I run built everything in the elements to save a time with zero service available, which is fine. And we need some time to propagate these services so we can use drapes URL and hit the service drape service directly. Now let's call it again. If I change this token to invalid one, we'll have authorized request. Then I can and this command will stop tilt delete all this, delete all the pods and delete namespace where it was deployed. As we've seen, everything was done with just one comment. Basel run when we work with Basel, we reference all the build targets by labels. Labels could be different. So what are we gonna do with labels? The generic format for the label is followed by path to the package column and target name. And this is our so called internal labels labels within our project we can with those labels everything recursive. For example, we can run bazel build double triple dot and. And that means we build everything under there special called all which is the same as triple dot. We also can build everything recursively under one package. How can build packages the same with triple dot or old target? Also we can generate. Sorry. We can build external dependencies and in this case path labeled external dependencies start with a sign. We also can build one target or we can run one target. Should we write those build files manually? So if we have quite a big project, we will have elderly build files, one file per package or per directory. Unfortunately, we don't need to run them manually from scratch. There is a tool called Gazelle which can generate build files for us. Also it can keep them up to date and format build files. Also Gazelle manages all the dependencies. As for other targets like for container images, yaml manifests and publishing artifacts. Yes, those targets we have to write manually, but it's for building stuff. What else we can do with Bazel? We can ask Bazel some questions like use package auto x which depends. If package X has with the same baselo query, we can visualize our dependency here like this. This one is a validation for one target and this container image includes just one go binary. Bazel is accessible. So as I mentioned before, we have rowsets which is an extension or plugin for Bazel. And there are different row sets like Rose go for working with building go applications, roS OCI for building containment images, Ros Proto for working with Protobuf, etcetera. A lot of different information about existing rule sets you can find on awesomebyzel.com website. And as for more info about Bazelo, you can look at Bazel build website and mentioned above awesome basel.com and that's it for today. Thank you for your attention. Happy building.
...

Eugene Khabarov

Lead Developer & DevX @ Arctic Wolf

Eugene Khabarov's LinkedIn account



Awesome tech events for

Priority access to all content

Video hallway track

Community chat

Exclusive promotions and giveaways