Bazel has just hit a new milestone: Bazel 1.0 has been announced today. It is time to start investing in this distributed build and test orchestration tool.
In this quick guide, we are going to use Bazel to build an Azure Function application, written in TypeScript.
Bazel is polyglot, meaning you can use it to build and test different technologies, even in the same monorepo, at the same! Today we are going to focus on TypeScript.
If you want to know more about the capabilities of Bazel when it comes to building web applications, I highly recommend reading the following post by my friend Alex Eagle from the JavaScript build/serve team at Google.
Here are the steps we are going to follow:
- Scaffold an Azure Function Application
- Set up a Bazel workspace
- Build the Azure Function Application using Bazel
- Deploy to Azure
If needed, I included a link to a free Azure trial, so you can try it yourself.
Let's get started...
UPDATE Oct. 20th: Hexa now has built-in support for Bazel!
Scaffold an Azure Function application
In order to easily create and then deploy our Azure Function application, we are going to use the Hexa CLI:
$ mkdir azurefunctionbazel && cd $_
$ hexa init --just functions
$ cd functions/azurefunctionbazel-5816
Hexa is an open-source CLI tool that enhances the Azure CLI (learn more about Hexa and how to install it).
Using Hexa we scaffolded inside the ./functions/azurefunctionbazel-5816 a new TypeScript Azure Function application that uses tsc (by default) to build.
We are going to setup Bazel and use it to orchestrate the build using the ts_library rule (i.e. a Bazel plugin).
Set up a Bazel workspace
In order to set up and use Bazel inside our Azure Function project ./functions/azurefunctionbazel-5816, we will need:
- Install Bazel and its dependencies from NPM
- Add a
WORKSPACEfile at the root of the project - Add a
BUILD.bazelfile at the root of the project - Add one
BUILD.bazelinside each function
Usually you wouldn't have to set up Bazel manually, but rather use a tool such as
@bazel/createto automate the setup and config.
Installing Bazel and its dependencies
Inside the ./functions/azurefunctionbazel-5816 folder, install the required Bazel and its peer dependencies from NPM using the following command (it will also update the existing package.json):
$ npm install -D \
@bazel/bazel@latest \
@bazel/typescript@latest \
typescript@^3.3.3
Make sure
typescriptis listed and installed as a dependency in thepackage.json
Add a WORKSPACE file
Create a file WORKSPACE at the root of ./functions/azurefunctionbazel-5816 with the following content (see explanation below):
workspace(
name = "azurefunctionbazel",
managed_directories = {"@npm": ["node_modules"]},
)
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name = "build_bazel_rules_nodejs",
sha256 = "1447312c8570e8916da0f5f415186e7098cdd4ce48e04b8e864f793c766959c3",
urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/0.38.2/rules_nodejs-0.38.2.tar.gz"],
)
load("@build_bazel_rules_nodejs//:index.bzl", "npm_install")
npm_install(
name = "npm",
package_json = "//:package.json",
package_lock_json = "//:package-lock.json",
)
load("@npm//:install_bazel_dependencies.bzl", "install_bazel_dependencies")
install_bazel_dependencies()
load("@npm_bazel_typescript//:index.bzl", "ts_setup_workspace")
ts_setup_workspace()
Let's explain each section...
workspace(
name = "azurefunctionbazel",
managed_directories = {"@npm": ["node_modules"]},
)
- The
workspacerule declares that this folder is the root of a Bazel workspace. - The
nameattribute declares how this workspace would be referenced with absolute labels from another workspace, e.g.@azurefunctionbazel// - the
managed_directoriesattribute maps the@npmBazel workspace to thenode_modulesdirectory. This lets Bazel use the samenode_modulesas another local tooling.
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name = "build_bazel_rules_nodejs",
sha256 = "1447312c8570e8916da0f5f415186e7098cdd4ce48e04b8e864f793c766959c3",
urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/0.38.2/rules_nodejs-0.38.2.tar.gz"],
)
This will install the Node.js "bootstrap" package which provides the basic tools for running and packaging Node.js apps in Bazel.
load("@build_bazel_rules_nodejs//:index.bzl", "npm_install")
npm_install(
name = "npm",
package_json = "//:package.json",
package_lock_json = "//:package-lock.json",
)
- The
npm_installrule runsnpm(oryarndepending on your configuration) anytime thepackage.jsonorpackage-lock.jsonfile changes. It also extracts any Bazel rules distributed in annpmpackage. - The
nameattribute is what Bazel Label references look like@npm//@azure/functions
load("@npm//:install_bazel_dependencies.bzl", "install_bazel_dependencies")
install_bazel_dependencies()
This call will install any Bazel rules which were extracted earlier by the npm_install rule.
load("@npm_bazel_typescript//:index.bzl", "ts_setup_workspace")
ts_setup_workspace()
This will set up the TypeScript toolchain for your workspace.
Root BUILD.bazel
Create a file BUILD.bazel at the root of ./functions/azurefunctionbazel-5816 with the following content:
exports_files(["tsconfig.json"], visibility = ["//:__subpackages__"])
This simple BUILD.bazel will export the tsconfig.json so it can be referenced from other packages.
Package BUILD.bazel
Bazel recommends creating fine-grained packages for more efficient build executions. A package in Bazel terminology is a folder containing a BUILD.bazel file, e.g. ./functions/azurefunctionbazel-5816/httpTrigger/BUILD.bazel.
In our case, the Azure Function application contains one folder for each function. These folders are going to be our Bazel packages.
Inside each BUILD.bazel we are going to describe how that package (i.e. an Azure Function) should be built, and let Bazel take care of the orchestration.
To build our application, we are going to keep it simple and just use the tsc compiler. However, for more complex scenarios, we can also bundle our application with rollup and minify it using terser tools. Such a hypothetical build process could look like:
# Note: this build hasn't been tested!
load("@npm_bazel_typescript//:index.bzl", "ts_library")
load("@npm_bazel_rollup//:index.bzl", "rollup_bundle")
load("@npm_bazel_terser//:index.bzl", "terser_minified")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "httpTrigger",
srcs = ["index.ts"],
deps = ["@npm//@azure/functions"]
)
rollup_bundle(
name = "bundle",
entry_point = ":index.ts",
deps = [":httpTrigger"],
)
terser_minified(
name = "bundle.min",
src = ":bundle",
)
But in our case, we will keep it simple:
load("@npm_bazel_typescript//:index.bzl", "ts_library")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "httpTrigger",
srcs = ["index.ts"],
deps = ["@npm//@azure/functions"]
)
- The
nameattribute is the name of the target (thists_librarycall) - The
srcsattribute lists all the TypeScript files to transpile - The
depsattribute lists all external dependencies used inside this package (e.g.@azure/functions). The@npm//is the name of the NPM workspace (see theWORKSPACEabove).
Having fine-grained packages gives us the guarantee from Bazel to get incremental, remote-parallelizable builds and cache automatically which will drastically improve the build time, for huge more complex projects.
Build the Azure Function Application
Now, we can run Bazel to build this specific package:
$ bazel build @azurefunctionbazel//httpTrigger:httpTrigger
The canonical path to a target is explained as the following:
In fact, this path can be shortened to:
$ bazel build //httpTrigger
This is because we are building the specified target from the same workspace. So we can omit the workspace identifier, and since the package and the target names are identical, we can omit the target name.
We can also instruct Bazel to build all targets of all packages using inside the current workspace:
$ bazel build //...
You may not see the benefits of using Bazel to build one single trivial Function, but once you start dealing with 100s of serverless applications, Bazel will be there to help you boost your build and test performances.
Bazel is also good at understanding the dependencies graph of our application. We can ask Bazel to show us this dependencies graph:
$ bazel query --noimplicit_deps 'deps(//...)' --output graph | dot -Tpng > azurefunctionbazel.png
Will output:
We could use this visualization to better understand the internals of our application. Image an application with 100s of Function Apps!
Deploy to Azure
By default, the output of thets_library() (*.js and *.d.ts) rule will be stored in ./functions/azurefunctionbazel-5816/bazel-bin/httpTrigger.
Before deploying to Azure, we need to update the function.json of each function and point the scriptFile to bazel-bin/:
{
...
"scriptFile": "../bazel-bin/httpTrigger/index.js"
}
We also need to make sure not to deploy the Bazel workspace to Azure, by updating the .funcignore file:
## Bazel ignored files
node_modules/@bazel/*
BUILD.bazel
# ignore all bazel output folder except the bazel-bin folder
# where the transpiled code lives
bazel-*
!bazel-bin
Now we are ready to deploy our application to Azure, using the Hexa CLI:
$ # cd to the root of azurefunctionbazel where hexa.json is located
$ hexa deploy
The output result of the deploy command would be:
โ Building Function app azurefunctionbazel-5816...
โ Deploying Function app azurefunctionbazel-5816...
โ Application azurefunctionbazel deployed successfully!
โ Functions:
- httptrigger: https://azurefunctionbazel-5816.azurewebsites.net/api/httptrigger?code=POfqT9xFCtYKOaryFW8LkzFsUtD3ioWi8Y2e4M8BpBeLhTOH8aqUIg==
โ Done in 68 seconds.
Congratulations ๐! Your Azure Function application has now been built using Bazel and deployed with Hexa.
Find the sample project on Github


Top comments (0)