Deploy Statiq to Azure Static Web App
Azure Static Web Apps just went GA a few days ago. I took the opportunity to deploy my blog as a "SWA" (Static Web App). In this post, I am writing about my experience with the service and how you can deploy a Statiq-based site.
What is "Azure Static Web App"?
It is a new Azure service which lets you deploy a static app (only static resources like images, HTML, JavaScript, CSS etc.). These resources will then automatically be served from "points of Presence" (PoP) around the world to make sure every user gets the site delivered in the fastest possible way. SWA uses Azure's CDN for this.
Serverless Batteries Included
However, with your Static Web App, you can also deploy an API in the form of Azure Functions. You may either use functions deployed with your app, or reference an existing Function App.
Security
To make your site secure, SWA offers authentication with Azure AD, GitHub and Twitter, so you can cordon-off parts (or all) of your site to authenticated users in roles you define.
For this to work, SWA also supports free TLS certificates for your custom domains - even for your Apex Domains!
Great Developer Experience
With GitHub and Azure DevOps integration, you can easily build a CI/CD process with staging environments for your pull requests.
This is complemented with a Visual Studio Code extension and a CLI tool for local tests.
Pricing
The free edition should cover most of your needs. Only if you have one of the following requirements, a monthly fee and bandwidth charges (>100GB) are due:
- >2 Domains
- Authentication
- Bring your own Azure Function (separate plan, not the one included with SWA)
- Higher storage requirements
Check out the pricing page for more details.
What is "Statiq"?
Statiq is a .NET Static Generator Framework written in C#.
Essentially, you configure pieces of code which are added to Pipelines. These pipelines process input (like Markdown, images, JSON) to an output.
Statiq offers Statiq.Framework
, as the base and Statiq.Web
as the "happy path" on top to generate static sites.
I am using Static.Web
for my Blog. Since I usually do not have to change any code when publishing articles, I do not need to recompile all the time, but just run dotnet run
to re-generate my content.
Check out my previous blog post on how I use Statiq.
Setting up a GitHub Actions Workflow
If you use SWA with GitHub, SWA creates a GitHub Actions workflow in your repo. This sets up two things:
- Build and deploy for pushes into the
main
branch - Builds for opening, syncing, re-opening and closing of Pull Requests to the
main
branch
Here is the YAML that was generated for me (StructedSiteAssembler
is my subdirectory for the blog application)
name: Azure Static Web Apps CI/CD
on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened, closed]
branches:
- main
jobs:
build_and_deploy_job:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
runs-on: ubuntu-latest
name: Build and Deploy Job
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Build And Deploy
id: builddeploy
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_WHITE_PEBBLE_014EC7A10 }}
repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
action: "upload"
###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
# For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
app_location: "/StructedSiteAssembler" # App source code path
api_location: "api" # Api source code path - optional
output_location: "output" # Built app content directory - optional
###### End of Repository/Build Configurations ######
close_pull_request_job:
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: ubuntu-latest
name: Close Pull Request Job
steps:
- name: Close Pull Request
id: closepullrequest
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_WHITE_PEBBLE_014EC7A10 }}
action: "close"
This specific config is needed, because it contains all the magic to deploy to SWA, which is not exposed to the public. This bothers me a bit, because it actually locks down the platform quite a bit and makes it harder to debug. But I never mind that, as GitHub Actions provides the flexibility needed, as you will see later.
The docker image used here has another thing included: Oryx. It detects which development stack you are using and generates & runs a build script for you. The output of which is then deployed to SWA.
The actual "SWA magic" is in here:
- name: Build And Deploy
id: builddeploy
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_WHITE_PEBBLE_014EC7A10 }}
repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
action: "upload"
###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
# For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
app_location: "/StructedSiteAssembler" # App source code path
api_location: "api" # Api source code path - optional
output_location: "output" # Built app content directory - optional
###### End of Repository/Build Configurations ######
.NET is detected and built correctly, but Statiq requires to run the app afterwards to actually create the output. This is where the "out of the box experience" did not work for me.
The Solution
While Oryx correctly determines a lot of popular stacks, Statiq is not very widely adopted (yet!), so we have to do a little workaround. Thankfully, the build within the GitHub Action Azure/static-web-apps-deploy@v1
can be skipped. Thus, it is only used to upload the artifact and deploy to the Azure Static Web App.
To do this, we just need to add a few lines which
- Set up .NET
- Restore packages
- Build the app
- Run the app to generate output
between - uses: actions/checkout@v2
and - name: Build And Deploy
:
- uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.203
- run: dotnet restore
- run: dotnet build ./StructedSiteAssembler/StructedSiteAssembler.csproj --configuration Release --no-restore
- run: dotnet run --project ./StructedSiteAssembler/StructedSiteAssembler.csproj --output output
- name: Build And Deploy
However, as we still need the Build And Deploy
step to deploy to SWA - but not the actual "Build" step with Oryx - we have the option to disable the build. Looking at the docs, we can learn that you can use skip_app_build: true
to skip the build. But it also requires us to set the app_location
to the actual output directory. The value of output_location
is ignored, if you set skip_app_build: true
.
Given my folder structure:
.
├───.github
│ └───workflows
└───StructedSiteAssembler
├───bin
├───input
│ ├───images
│ ├───posts
│ └───scss
├───output
│ ├───images
│ ├───img
│ ├───js
│ ├───posts
│ ├───scss
│ ├───tags
│ └───vendor
├───theme
│ └───input
│ ├───img
│ ├───js
│ ├───scss
│ └───vendor
└───wwwroot
my config would looks like this:
app_location: "/StructedSiteAssembler/output" # App source code path
api_location: "" # Api source code path - optional
output_location: "" # Built app content directory - optional
skip_app_build: true
An here is the full Action setup for my blog:
name: Azure Static Web Apps CI/CD
on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened, closed]
branches:
- main
jobs:
build_and_deploy_job:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
runs-on: ubuntu-latest
name: Build and Deploy Job
steps:
- uses: actions/checkout@v2
with:
submodules: true
- uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.203
- run: dotnet restore
- run: dotnet build ./StructedSiteAssembler/StructedSiteAssembler.csproj --configuration Release --no-restore
- run: dotnet run --project ./StructedSiteAssembler/StructedSiteAssembler.csproj --output output
- name: Build And Deploy
id: builddeploy
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_WHITE_PEBBLE_014EC7A10 }}
repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments)
action: "upload"
###### Repository/Build Configurations - These values can be configured to match your app requirements. ######
# For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig
app_location: "/StructedSiteAssembler/output" # App source code path
api_location: "" # Api source code path - optional
output_location: "" # Built app content directory - optional
skip_app_build: true
###### End of Repository/Build Configurations ######
close_pull_request_job:
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: ubuntu-latest
name: Close Pull Request Job
steps:
- name: Close Pull Request
id: closepullrequest
uses: Azure/static-web-apps-deploy@v1
with:
azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_WHITE_PEBBLE_014EC7A10 }}
action: "close"
Conclusion
Azure Static Web Apps is a great service to host a site, or even a browser-based game! With it's very fast response times all over the World thanks to Azure's CDN, the tight integration with Azure Functions and built-in authentications makes it a great, flexible and very low cost choice for many use-cases.
I am certain we will be seeing tighter integration with more frontend frameworks and improved monitoring in the coming months.