Deploying an Angular app to Kubernetes using Bazel (preview)
As part of a hackathon with Torgeir Helgevold last month, we built a prototype of a Rollup rule for Bazel. It’s not ready for usage yet, but would eventually be as simple as wrapping your Angular app in a rollup_bundle like this:
You could then
bazel build :bundle and it would produce the
bundle.js file ready for deployment.
Disclaimer: Bazel with Angular is still considered experimental by the Angular team.blog.nrwl.io
Note: I’m going to describe a prototype, which is still Work-in-Progress. But it’s a really exciting indicator of what direction the ABC project is going, making the whole Google model of Angular development available.
As soon as we had the Rollup bundle working, it was time to try a production deployment with Bazel. What we want is a portable Docker container that holds our Angular app along with its server and whatever machine image it requires, that we can bring up on any Cloud provider. This setup can be easily expanded to bring up a set of microservices, allowing a developer to instantly spin up a local integration testing environment that mirrors production. But we don’t want to lose our fast development round-trip that comes with Bazel: we want the entire pipeline to be incremental.
Fortunately my buddy Matt Moore gave a talk at Bazel Conf which shows how fast the Docker rules are. Essentially it provides incrementality by adding new Docker layers, so that the changes you make to the app are the only things sent over the wire to the cloud host. Also, since Docker images are tagged with a SHA, we only re-deploy images that changed. To manage our production deployment, we’ll use Kubernetes, for which Bazel rules also exist.
Here’s a high-level view of what happens when I make a change to the app:
- I run
ibazel run :deploy.replace. ibazel is the “watch mode” for Bazel, so this command stays running as I make changes. I use the “run” command because the deployment requires executing a Kubernetes binary, not just compiling code. The “:deploy.replace” target tells Kubernetes to update the deployed version of the app.
- I make a change in my Angular app and save the file.
- Bazel incrementally re-builds just the parts of the build graph that depend on the changed file. In this case, that includes the
ng_modulethat I changed, the
rollup_bundlethat includes that module, and the Docker
nodejs_imagethat holds the server. Because I asked to update the deployment, after the build is complete it pushes the new Docker container to Google Container Registry and my Kubernetes Engine instance starts serving it.
Since Bazel understands the build graph, it only re-builds what is changed. Step 3 takes less than a second if I make a change that doesn’t affect the
ng_module. If I make a change in a comment, it re-builds the rollup bundle, but then since Rollup strips comments the Docker rule is not re-run, so this takes about 4s. If I make a change that does affect the app, it takes 14s to create the new Docker image and deploy it to production. We haven’t spent any time optimizing this yet — it’s just that fast in the naive first try.
Here’s a rough screencast showing how it looks. Notice that after the deployment finishes, we wait about 7 seconds for the production site to start serving from the newly launched container, I assume this is because we run behind a load balancer so Google Kubernetes Engine keeps the old version around to serve traffic during some period.
If I were really doing interactive development and needed an integration testing environment, I’d try Minikube to shorten the round-trip time by deploying to a local cluster instead of the Cloud.
If you want to dig more deeply to see how this works, you can look at https://github.com/thelgevold/angular-bazel-example/commits/346cc33c4494d9e10d4b8474204c95df93bad288
Once we get these prototype-quality bits landed, I will post some real documentation and examples.