Kicking the tires on Azure Static Web Apps

Azure Static Web Apps (SWA) is a relative new offering that attempts to wrap up the common needs and desires of many modern web apps and start-ups.

This service is still in “Preview”, so we have to temper our expectations a bit.

It is clearly targeting developers using the cloud-friendly Jamstack or Single Page Application (SPA) architectures, with features sitting in the intersection of those two approaches:

  • serve static assets
  • Github-based CI/CD to build and deploy static assets
  • oauth-based authentication with the common providers
  • optional serverless functions
  • consumption-based billing (probably, it is free while in “Preview”)

I’ve been working on a few projects recently that have these requirements, and here’s how it went.

Initial setup

I started out with a repo on Github, and followed the SWA setup tutorial. The docs are well done, with plenty of screenshots to help navigate the often-byzantine Azure Portal.

There were a few surprises here. The setup wizard jumps through oauth hoops with github, and directly made commits and changes to my repo:

$ git log --author microsoft
commit 77f9daf56683245fc03d17f4b2c9a36eda443ea7
Author: Azure Static Web Apps <opensource@microsoft.com>
Date:   Sat Oct 17 16:39:45 2020 -0400

    ci: add Azure Static Web Apps workflow file
    on-behalf-of: @Azure opensource@microsoft.com

The installation wizard committed a Github actions workflow and added a secret.

The setup process assigns you a randomly-generated subdomain under azurestaticapps.net (in my case, brave-flower-0d5088d0f), and hosts the app under HTTPS there. This random name is also present in the github workflow filename, and the github secret name.

Modifying my repo felt a little invasive, but was an efficient way to get a CI/CD pipeline setup. This was much faster than following a series of instructions and is really nice for start-ups who would rather spend time on their product than infrastructure.

This workflow gives a few great benefits. Any merges to the default branch build and deploy the site, providing continuous deployment.

For PRs to the default branch, it builds and deploys to a unique, temporary pre-production site, and comments on the PR with a link:

azure bot commenting on github PR
Each PR gets a test site

This lets reviewers test out the changes before they are merged. After merge, the temporary site is deleted. This looks similar to Gitlab review apps. You can only have one of these test sites live at time, but I think that’s an artificial restriction imposed while SWA is in “Preview”.

What’s in a name?

Few websites want to operate as something like https://brave-flower-0d5088d0f.azurestaticapps.net, so SWA supports custom domains, with some restrictions.

First you’ll need to define a CNAME pointing to your random SWA subdomain. I like managing infrastructure using terraform, and the azurerm_dns_cname_record has us covered:

resource "azurerm_dns_cname_record" "www" {
  name                = "www"
  zone_name           = "lazy-electron.com"
  record              = "brave-flower-0d5088d0f.azurestaticapps.net"
  // ...other settings not relevant for this article
}

Unfortunately, the is no terraform provider for SWA, so we need to poke around in the azure portal manually following Setup a custom domain to finish it up.

There has been some progress adding SWA support to terraform, but the random name and github side-effects of the install process are tough for infrastructure-as-code tools to deal with.

Terraform does support arbitrary Azure Resource Manager (ARM) templates, but we hit a brick wall there, too. The azure portal export feature errors out:

Export template operation completed with errors. Some resources were not exported. Please see details for more information.

The schema of resource type ‘Microsoft.Web/staticSites’ is not available. Resources of this type will not be exported to the template.

I assume this is part of what they mean by “Preview”.

One particularly ugly restriction is lack of apex/naked/root domain support. You can easily serve your site at any subdomain (e.g. https://www.lazy-electron.com), but if you want to serve your site without any subdomain (e.g. https://lazy-electron.com) you’re in for a lot more setup. The official docs point at a github.io blog post talking through a Cloudflare-based solution. I have to imagine some product manager at Azure is not happy with that. I attempted an Azure-only solution, but it needed too many moving parts. We’ll walk through that circus another time.

Let’s get functional

The built-in support for Azure Functions is very convenient. Web apps with simple backends (e.g. CRUD operations to a database) are well covered by a handful of functions, and the consumption-based billing model incentivizes efficient patterns. Your functions have a few constraints to work inside SWA, but I didn’t find any of them were particularly onerous. The Add an API docs are pretty good, and cover essentials like local development and keeping secrets out of git.

Your functions don’t play too nicely with the pre-production test sites. Your server-side application settings are copied from production, so if you have anything stateful (e.g. database connection strings) it is getting shared with your test environment. This is a dangerous default; it would be terribly easy to click around in your “test” environment and affect production data. Once your pre-prod site is created, then you can change configuration settings in the azure portal:

azure
function settings environment options
The test environment settings can be edited after it is deployed. Better hurry!

This feels like another thing to work out before leaving “Preview”. It would be great if there was an option to define default pre-prod settings that were copied to each PR’s pre-prod site.

Keeping the functions in the same repo as the frontend code is convenient, but it starts feeling crowded in there as the project grows. You’re out of luck if you want to deploy the functions without the frontend (or vice versa).

Authentication

This is another nice “I just want it to work” feature from SWA. They define some magic URLs that run alongside any of your functions at URLs like /.auth/login/twitter. I didn’t get too far into this one, but it worked as advertised. There are some hoops to jump through to access user information, but nothing too bad.

This probably meets the basic identity needs of most services, without requiring anyone to write yet another “reset password” feature.

Digging into the CI/CD pipeline

The SWA tutorials have you make changes to the github workflow, so they clearly expect you to own your pipeline. I tried to make some optimizations, and ran into a few problems.

First off, the SWA jekyll tutorial has you add the ruby/setup-ruby@ec106b438a1ff6ff109590de34ddc62c540232e0 action, which uses obsolete methods and throws errors. Digging into the docs, ruby/setup-ruby@v1 is recommended, and works fine. This is a pretty simple documentation fix, and another sign of SWA’s “Preview” status.

The second problem was a little deeper. My pipeline felt slow, and looking through logs it was repeatedly downloading dependencies. Github provides a way to cache slow-moving dependencies, so I tried to follow some more docs to get it working. Here I ran into trouble due to the opacity of the pipeline.

The SWA generated pipeline uses the Azure/static-web-apps-deploy@v0.0.1-preview action, and chasing through a few more layers ends up in the Oryx build system to do the actual work.

Oryx is an ambitious project. From their README:

Oryx is a build system which automatically compiles source code repos into runnable artifacts. It is used to build web apps for Azure App Service and other platforms.

Oryx generates and runs an opinionated build script within a build container based on analysis of a codebase’s contents. For example, if package.json is discovered in the repo Oryx includes npm run build in the build script; or if requirements.txt is found it includes pip install -r requirements.txt.

So I can’t really write my own build script, I have to setup my repo in such a way that Oryx can figure out what to do. Oryx is responsible for deploying, and will only deploy if it thinks it has a successful build. There are some docs for GitHub Actions workflows for SWA on ways to customize, but it’s pretty slim.

In my case, I spent an afternoon playing guess-and-check with the pipeline and decided it wasn’t worth the time. I ended up with a ~5 minute pipeline that builds the frontend and then tells Oryx to use echo 'already built' to build my frontend. This leaves me installing ruby twice, ruby gems twice, and for some reason java? I guess Oryx knows best.

SWA creates a CI/CD golden path, and you’re in for some headaches if you step off.

I’d really like to deploy to prod when a git tag is made, but so much of the CI/CD is implicit magic in Oryx it’s difficult to suss out how to make that happen.

Another project uses git flow, and we’ve got a perpetual PR to merge from develop to main that is the only available test site, limiting the utility of SWA’s pipeline quite a bit.

Conclusion

Azure Static Web Apps feels like an useful abstraction built on top of existing Azure Storage and Azure Functions services. It has a clear goal of taking care of a lot of boring work everyone keeps having to burn time implementing:

  • authentication
  • deployment pipelines
  • test sites

I think this is a great start on a promising offering, but it is definitely a “Preview” service. I think it’s a great choice for startups and prototypes, but without better devops support I think a lot of teams will quickly outgrow it.