Deploy WebAssembly from GitHub to Azure Storage Static Websites with Azure Pipelines
Free automated builds to inexpensive hosting with examples in C, C#, Go and Rust
I wrote a series of articles to document my investigation of WebAssembly beginning with a unique framework called Blazor. Start with the .NET Framework — including the Common Language Runtime (CLR) — build it on top of WebAssembly, then add functionality to render and manage Single Page Applications using Razor templates and C#, and the result is Blazor (Browser + Razor templates). My first step in learning the framework was to port an existing Angular 2 app. I then created a Blazor presentation with several example applications:
Get the presentation, source code and step-by-step demo instructions for a session that covers how to run C# and .NET in the browser without plugins using Blazor over WebAssembly.
After recording and publishing a video series about Azure DevOps for .NET Developers, I immediately realized that because the demo apps are Single Page Apps deployed as a set of static assets, I can host them on storage and automate the build and deployment.
1️⃣ First Step: Add Azure Pipelines 🚀
To begin, open your GitHub project and navigate to the marketplace.
Search for and choose Azure Pipelines.
Follow the prompts to create your Azure DevOps account. After you’ve created or selected a project, you are prompted to apply to all projects or pick specific ones. I recommend one Azure DevOps project per GitHub repository to start with. When prompted, choose public for your project. That doesn’t mean anyone will be able to see your secrets or deployment credentials; it gives them access to the build status and details. The deployment will be separate.
After you select the GitHub repository, a default
azure-pipelines.yml file is generated based on preliminary inspection of your source code. For example, a .NET Core project will automatically generate a .NET Core pipeline. The initial pipeline is just a skeleton, you can choose “save and run” then throw away the results. The pipeline is checked into source control (without any secrets) so that users who fork your project can easily create their own automated builds. You can also use an existing pipeline as a starter template for new builds you create.
2️⃣ Second Step️: Configure the Build Yaml ⚙
The first build process I created was for my Blazor examples. There are several examples in the same repository with separate solutions, so I created multiple build steps. You can inspect the build pipeline here.
The start of the configuration looks like this:
# ASP.NET Core trigger: - master pool: vmImage: 'Ubuntu-16.04' variables: buildConfiguration: 'Release'
The build will automatically trigger based on a commit to the
master branch. It uses a hosted Linux image (Ubuntu 16.04) and is configured for “release” or production.
📃 Learn more about hosted agents, including how they are configured and what software is installed, here: Microsoft-hosted agents for Azure Pipelines.
The next section of the Yaml file details the build steps.
steps: - task: [email protected]2 inputs: version: '3.0.100-preview4-011223' - script: dotnet build --configuration $(buildConfiguration) ReusableComponents/ReusableComponents.sln displayName: 'dotnet build $(buildConfiguration) ReusableComponents' - script: dotnet publish --output $(Build.ArtifactStagingDirectory)/ReusableComponents ReusableComponents/ReusableComponents.sln displayName: 'dotnet publish ReusableComponents' - script: dotnet build --configuration $(buildConfiguration) MvvmPattern/MvvmPattern.sln displayName: 'dotnet build $(buildConfiguration) MvvmPattern' - script: dotnet publish --output $(Build.ArtifactStagingDirectory)/MvvmPattern MvvmPattern/MvvmPattern.sln displayName: 'dotnet publish MvvmPattern' - script: dotnet build --configuration $(buildConfiguration) LibrariesInterop/LibrariesInterop.sln displayName: 'dotnet build $(buildConfiguration) LibrariesInterop' - script: dotnet publish --output $(Build.ArtifactStagingDirectory)/LibrariesInterop LibrariesInterop/LibrariesInterop.sln displayName: 'dotnet publish LibrariesInterop' - script: dotnet build --configuration $(buildConfiguration) CodeBehind/CodeBehind.sln displayName: 'dotnet build $(buildConfiguration) CodeBehind' - script: dotnet publish --output $(Build.ArtifactStagingDirectory)/CodeBehind CodeBehind/CodeBehind.sln displayName: 'dotnet publish CodeBehind' - task: [email protected]1 inputs: ArtifactName: 'blazordist'
Blazor is currently in preview, so I use a .NET Core task to install the correct preview version. Next, each project is built and published into its own directory. Notice the use of the
$(Build.ArtifactStagingDirectory) to reference where distributions should reside.
📃 You can read the full list of Azure Pipelines predefined variables.
Notice the last step publishes the artifacts. This places them in a compressed archive that you can inspect and download after the build. It also makes the artifacts available to the release pipeline that will deploy your assets. More on that later.
The Chaos Game with C
Someone wiser than me once said that to truly learn a technology, you should always go at least one layer beneath the surface. Underneath Blazor is WebAssembly, so I set out to learn as much as I could. There are several ways to build WebAsssembly, and the original tool chain that allows you to compile C and C++ projects to Wasm is called Emscripten.
I wrote about my experiments here:
Installing the tool chain is not straightforward and involves several steps. Fortunately, the entire development environment is configured and hosted in a Docker container. I chose to take advantage of that for my Emscripten build pipeline.
Gophers Meet Plasma
I wrote about it here:
Go also provides a fully configured Docker container. Here is the build pipeline:
# Go # Build your Go project. # Add steps that test, save build artifacts, deploy, and more: # https://docs.microsoft.com/azure/devops/pipelines/languages/go trigger: - master pool: vmImage: 'Ubuntu-16.04' steps: - task: [email protected]0 inputs: dockerVersion: '17.09.0-ce' - script: docker run -i --rm -v $(pwd):/usr/src/plasma -w /usr/src/plasma -e GOOS=js -e GOARCH=wasm golang:latest go build -o plasma.wasm plasma.go displayName: 'Build WASM' - script: docker run -i --rm -v $(pwd):/usr/src/plasma -w /usr/src/plasma golang:latest cp /usr/local/go/misc/wasm/wasm_exec.js /usr/src/plasma displayName: 'Copy current wasm_exec.js' - script: | cp wasm_exec.js $(Build.ArtifactStagingDirectory) cp plasma.js $(Build.ArtifactStagingDirectory) cp plasma.wasm $(Build.ArtifactStagingDirectory) cp index.html $(Build.ArtifactStagingDirectory) displayName: 'Copy assets to aritfacts staging' - task: [email protected]1 inputs: ArtifactName: 'plasmawasmgo'
Go is available as a build step/environment as well. I could have skipped the Docker image, configured Go on the build agent and built everything directly. However, I already was using a Docker-based build so it was faster for me to use the same steps for my build pipeline. Choices are good.
Plasma Gets Rust-y
The Rust 🦀 language is ideally suited to WebAssembly development. Unlike Go and C# that both require runtimes to execute (the Go file is over two megabytes in size as a result), Rust generates low-level platform-ready code like C/C++ but with a mature syntax and many built-in features that provide security and thread safety. Rust also embraced WebAssembly very early and built a set of tools to support Wasm development. As a result, Rust makes it much easier to build and package WebAssembly apps while producing streamlined byte code (the Rust Wasm file is only 61 kilobytes or about 3% the size of what Go generates).
I wrote about the experience here:
Building a plasma canvas effect using Wasm compiled from Rust.
The Rust build pipeline is significantly different that the previous ones.
steps: - task: [email protected]0 inputs: versionSpec: '10.x' displayName: 'Install Node.js' - task: [email protected]2 inputs: script: | curl -f -L https://static.rust-lang.org/rustup.sh -O sh rustup.sh -y displayName: 'Install Rust' - script: 'echo "##vso[task.setvariable variable=PATH]$PATH:$HOME/.cargo/bin"' displayName: 'Update path' - script: 'cargo install wasm-pack' displayName: 'Install wasm-pack' - script: 'wasm-pack build' displayName: 'Build WebAssembly' - script: 'npm link' displayName: 'npm link' workingDirectory: '$(Build.Repository.LocalPath)/pkg' - script: | npm install npm link plasma-wasm-rust npm run build displayName: 'npm install and build' workingDirectory: $(Build.Repository.LocalPath)/www - task: [email protected]1 inputs: PathtoPublish: '$(Build.Repository.LocalPath)/www/dist' ArtifactName: 'plasmawasmrust' displayName: 'Publish artifacts'
The environment is setup to use Node.js. A command-line task installs the Rust toolchain (“Rustup”), followed by “wasm-pack” that is used to build Wasm apps.
The need to install Rustup manually will go away soon. As of this writing, a pull request is in review to add Rust as a first-class pipeline task.
One folder contains the Rust project. That project is built using
npm build step packages everything together as a distinct set of assets to deploy, and the last task publishes artifacts directly from the distribution folder.
Now artifacts exist for multiple flavors of WebAssembly projects. The next step is to host them using inexpensive Azure Storage.
Badge of Honor
At this stage if you’re like me, you’re probably excited about getting automated builds up and running and want to share it with the world. Azure Pipelines makes this easy for you. Navigate from “Azure Pipelines” to “Builds” and select the three dots in the upper then click on “status badge.”
The resulting dialog will allow you to specify some parameters and provides both a link to the status badge and markdown you can easily cut and paste into your
3️⃣ Third Step: Create an Azure Storage Account
The next few steps require an Azure subscription. If you don’t have one already, you can grab a free Azure account. Use the ➕ in the upper left to add a new resource and choose Storage account.
Give the storage account a name, pick your resource group, and be sure to select “Storage V2 (general purpose v2)” as the account kind. Pick a replication option that suits you (higher availability comes at a higher cost). Review, then create the storage account and wait for the notification that is ready and available.
4️⃣ Fourth Step: Configure Your Static Website
Static websites are not enabled by default. To enable them, choose the “static websites” option, flip to “enabled” and optionally enter a default document and error document (what will be served when files aren’t found).
After you click “save” a special blob storage container named
$web is created. This is the “root” of your static website. You will also be presented with the URL you can use to access the site.
5️⃣ Fifth Step: Deploy
Navigate to “Releases” under “Azure Pipelines.” You will be presented with the option to create a new pipeline. Click “new pipeline” and choose “empty job.” You can name the first stage of your deployment pipeline. I’m using mine for demo assets, so I only have one stage and name it “static websites.”
Next, add an artifact to feed into the release pipeline. See the “add” link.
A new dialog will appear. Select your project and source, and the default artifacts will populate. Click “Add” to add this artifact.
To enable continuous deployment (that will be triggered after every successful build) click on the little “trigger” icon in the upper right.
Finally, flip “continues deployment trigger” to “enabled.”
Next, click “view stage tasks” to begin building the deployment.
Set the Base URL
I’m using a single static website to host multiple projects. The projects live under a folder path and not at the root of the website, so I need to specify the base URL using the
<base href=""> HTML tag. The easiest way to do this is to click the little ➕ next to the agent job to add a new task then go the marketplace and install the free RegExReplace Build Task. In the resulting dialog, I specify the path to the main HTML file that hosts the app. The regular expression varies from project to project. In the case of the Rust app, there is no existing tag to replace. Therefore, I search for the end of a
style tag I know exists and append the
base tag to the end of it. The regular expression and replacement look like this:
Notice that I put my Wasm apps under the
wasm folder with each project in its own folder, in this case
PlasmaWasmRust. I do this for every project that is deployed. The advantage of applying this transformation during the deployment phase is that the exact same build artifacts can be used for any environment. If my staging truly was “staging” and required another step for production, I can use the same build artifact and apply a different transformation to target the production URL.
Copy the Assets to Storage
To copy the artifacts, use the “Azure File Copy Task.”
Give the task a name and click the ellipses after “source” to navigate to your artifact folder. Here I’ve selected
Pick “Azure Resource Manager” for the type and choose your subscription from the drop-down.
Note: if your subscription does not display automatically, you may need to use a service account to connect to Azure. To learn how, read: Connect to Microsoft Azure.
Pick “Azure Blob” as the destination type, then select the name of your storage account. The container name will be
$web and the blob prefix is where you specify the folder path to place the artifacts in (it is the same “path” as the base URL but without the leading slash). Here is the task for the Rust project:
This step works for copying all of the main files into your static website, but one additional step is needed. The file copy task by default sets MIME types for files based on their extensions. This ensures they load correctly in your web browser. WebAssembly is a newer content type, so the current version of the task uses a default type that prevents it from loading correctly.
Set the MIME Type for Wasm Files
To adjust the default behavior, copy the
.wasm files a second time and pass an argument that specifies the content type. Start by adding another Azure File Copy task. This time, set the source to a pattern that matches only your Wasm files:
Next, add the optional arguments to set the file content type.
This last and final step should be all that is needed to successfully deploy your app. Save it and launch the stage (or commit to master and follow the entire pipeline from build through deploy) and see your app come out the other side!
You have a build badge, so why not a deployment one? To add your deployment badge, open the release pipeline and navigate to “options” then “integrations.” Enable the status badge and you’ll receive the appropriate link.
Azure Pipelines is free for open source projects hosted on GitHub. The goal of Azure Pipelines is to build any language, on any platform, and automate deployment to any destination. Do you have a mobile app? That’s no problem because Azure Pipelines will build your iOS code on a macOS agent (or Android code using a Java-based SDK) and automatically deploy it for review to the app store. Does your app have a comprehensive unit test suite? No problem! Azure Pipelines will run your tests and publish results, including code coverage, to your project dashboard.
Do you have an open source project that will benefit from automation?
- Docker Containers at Scale with Azure Web App on Linux (Docker)
- Introduction to Azure Cloud Shell (Docker)
- MongoDB on Windows in Minutes with Docker (Docker)
- The Year of Angular on .NET Core, WebAssembly, and Blazor: 2019 in Review (Webassembly)
- Webassembly for C, Rust, Go, and C# in 45 Minutes (Webassembly)
This post originally appeared at Deploy WebAssembly from GitHub to Azure Storage Static Websites with Azure Pipelines