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
WORKSPACE
file at the root of the project - Add a
BUILD.bazel
file at the root of the project - Add one
BUILD.bazel
inside each function
Usually you wouldn't have to set up Bazel manually, but rather use a tool such as
@bazel/create
to 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
typescript
is 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
workspace
rule declares that this folder is the root of a Bazel workspace. - The
name
attribute declares how this workspace would be referenced with absolute labels from another workspace, e.g.@azurefunctionbazel//
- the
managed_directories
attribute maps the@npm
Bazel workspace to thenode_modules
directory. This lets Bazel use the samenode_modules
as 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_install
rule runsnpm
(oryarn
depending on your configuration) anytime thepackage.json
orpackage-lock.json
file changes. It also extracts any Bazel rules distributed in annpm
package. - The
name
attribute 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
name
attribute is the name of the target (thists_library
call) - The
srcs
attribute lists all the TypeScript files to transpile - The
deps
attribute lists all external dependencies used inside this package (e.g.@azure/functions
). The@npm//
is the name of the NPM workspace (see theWORKSPACE
above).
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)