Build Your Serverless Azure Function Apps With Bazel 1.0 🌿
Tags: bazel, serverless, typescript, azure
Published: 2019-10-11 10:32:40

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.

{% tweet 1182386311761747969 %}

In this quick guide, we are going to use Bazel to build an [Azure Function](https://docs.microsoft.com/azure/azure-functions/?WT.mc_id=devto-blog-wachegha) 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.

{% link https://dev.to/jakeherringbone/layering-in-bazel-for-web-389h %}


Here are the steps we are going to follow:

1. [Scaffold an Azure Function Application](#Scaffold-an-Azure-Function-application)
1. [Set up a Bazel workspace](#Set-up-a-Bazel-workspace)
1. [Build the Azure Function Application using Bazel](#Build-the-Azure-Function-Application-using-Bazel)
1. [Deploy to Azure](#Deploy-the-app-to-Azure-using-Hexa)

> If needed, I included a link to [a free Azure trial](https://azure.microsoft.com/free/?WT.mc_id=devto-blog-wachegha), so you can try it yourself.

Let's get started...

> UPDATE Oct. 20th: [Hexa](www.hexa.run) now has built-in support for Bazel! {% tweet 1185989686117969923 %}

# Scaffold an Azure Function application <a name="Scaffold-an-Azure-Function-application"></a>

In order to easily create and then deploy our [Azure Function](https://docs.microsoft.com/azure/azure-functions/?WT.mc_id=devto-blog-wachegha) application, we are going to use the [Hexa](www.hexa.run) CLI:


```shell
$ 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](https://www.hexa.run)). 

Using [Hexa](www.hexa.run) we scaffolded inside the `./functions/azurefunctionbazel-5816` a new TypeScript [Azure Function](https://docs.microsoft.com/azure/azure-functions/?WT.mc_id=devto-blog-wachegha) 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 <a name="Set-up-a-Bazel-workspace"></a>

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
1. Add a `WORKSPACE` file at the root of the project
1. Add a `BUILD.bazel` file at the root of the project
1. 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 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):

```python
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...

```python
workspace(
    name = "azurefunctionbazel",
    managed_directories = {"@npm": ["node_modules"]},
)
```

1. The `workspace` rule declares that this folder is the root of a Bazel [workspace](https://docs.bazel.build/versions/master/build-ref.html#workspace).
1. The `name` attribute declares how this workspace would be referenced with absolute labels from another workspace, e.g. `@azurefunctionbazel//`
1. 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.


```python

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.

```python
load("@build_bazel_rules_nodejs//:index.bzl", "npm_install")
npm_install(
    name = "npm",
    package_json = "//:package.json",
    package_lock_json = "//:package-lock.json",
)
```
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`

```python
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.


```python
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:


```python
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:

```python
# 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:

```python
load("@npm_bazel_typescript//:index.bzl", "ts_library")

package(default_visibility = ["//visibility:public"])

ts_library(
    name = "httpTrigger",
    srcs = ["index.ts"],
    deps = ["@npm//@azure/functions"]
)
```

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 <a name="Build-the-Azure-Function-Application-using-Bazel"></a>

Now, we can run Bazel to build this specific package:

```bash
$ bazel build @azurefunctionbazel//httpTrigger:httpTrigger
```

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

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/cvxd3auqbu7viaolq2dj.png)

In fact, this path can be shortened to:

```bash
$ 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:

```bash
$ 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:

```bash
$ bazel query --noimplicit_deps 'deps(//...)' --output graph | dot -Tpng > azurefunctionbazel.png
```

Will output:

![Alt Text](https://thepracticaldev.s3.amazonaws.com/i/6khc5oi3otfoksgxtf19.png)

> We could use this visualization to better understand the internals of our application. Image an application with 100s of Function Apps!


# Deploy to Azure <a name="Deploy-the-app-to-Azure-using-Hexa"></a>

By default, the output of the`ts_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/`:


```json
{
  ...
  "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](www.hexa.run) CLI:

```shell
$ # 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](www.hexa.run).

Find the sample project on Github

{% github manekinekko/azure_function_bazel %}