DEV Community

Cover image for Build Your Serverless Azure Function Apps With Bazel 1.0 ๐ŸŒฟ
Wassim Chegham for Microsoft Azure

Posted on • Updated on

Build Your Serverless Azure Function Apps With Bazel 1.0 ๐ŸŒฟ

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:

  1. Scaffold an Azure Function Application
  2. Set up a Bazel workspace
  3. Build the Azure Function Application using Bazel
  4. 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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Install Bazel and its dependencies from NPM
  2. Add a WORKSPACE file at the root of the project
  3. Add a BUILD.bazel file at the root of the project
  4. 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
Enter fullscreen mode Exit fullscreen mode

Make sure typescript is listed and installed as a dependency in the package.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()

Enter fullscreen mode Exit fullscreen mode

Let's explain each section...

workspace(
    name = "azurefunctionbazel",
    managed_directories = {"@npm": ["node_modules"]},
)
Enter fullscreen mode Exit fullscreen mode
  1. The workspace rule declares that this folder is the root of a Bazel workspace.
  2. The name attribute declares how this workspace would be referenced with absolute labels from another workspace, e.g. @azurefunctionbazel//
  3. the managed_directories attribute maps the @npm Bazel workspace to the node_modules directory. This lets Bazel use the same node_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"],
)
Enter fullscreen mode Exit fullscreen mode

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",
)
Enter fullscreen mode Exit fullscreen mode
  1. The npm_install rule runs npm (or yarn depending on your configuration) anytime the package.json or package-lock.json file changes. It also extracts any Bazel rules distributed in an npm package.
  2. 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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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__"])
Enter fullscreen mode Exit fullscreen mode

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",
)
Enter fullscreen mode Exit fullscreen mode

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"]
)
Enter fullscreen mode Exit fullscreen mode
  1. The name attribute is the name of the target (this ts_library call)
  2. The srcs attribute lists all the TypeScript files to transpile
  3. 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 the WORKSPACE 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
Enter fullscreen mode Exit fullscreen mode

The canonical path to a target is explained as the following:

Alt Text

In fact, this path can be shortened to:

$ bazel build //httpTrigger
Enter fullscreen mode Exit fullscreen mode

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 //...
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Will output:

Alt Text

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"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

Congratulations ๐ŸŽ‰! Your Azure Function application has now been built using Bazel and deployed with Hexa.

Find the sample project on Github

Build Azure Function Apps With Bazel 1.0

Top comments (0)