Deploy Azure Static Web Apps With Bicep

Wednesday, Jun 29, 2022 7 minute read Tags: azure devops

In an effort to constantly tinker with things that probably don’t need to be tinkered with, I’ve decided that it’s time to do an upgrade to my CI/CD pipeline so that I can use Bicep to deploy Azure Static Web Apps (I’m also using this as a way to learn more about Bicep as well).

Because I’m using VS Code I’ve gone ahead and installed the Bicep Extension so that I get some nice syntax highlighting in the editor.

Writing Bicep

With the editor ready, let’s write some Bicep! Create a new file named swa.bicep and paste the following code into it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
param name string
@allowed([ 'centralus', 'eastus2', 'eastasia', 'westeurope', 'westus2' ])
param location string
@allowed([ 'Free', 'Standard' ])
param sku string = 'Standard'

resource swa_resource 'Microsoft.Web/staticSites@2021-01-15' = {
    name: name
    location: location
    tags: null
    properties: {}
    sku: {
        name: sku
        size: sku
    }
}

This is about as simple as you can get when it comes to deploying SWA with Bicep, as it’s not deploying a backend service, but rather a static website.

First up, there’s three parameters defined, the name of the SWA instance, the location and what sku to use (and the sku is defaulted to Standard). There’s also an allow-list of values for location and sku, since they have some restrictions on them and this will reduce the possibility of invalid values.

Then we define the resource that we’re deploying with the symbolic-name swa_resource (that we could use elsewhere if required) and the resource name plus the version.

This is the minimum fields that you need to provide to Bicep for defining the resource, with tags and properties essentially ignores in ours, but you need to include them or the deployment will fail with a rather obscure error message. The tags property is pretty straight forward, but let’s talk about properties for a bit.

Understanding properties in our Bicep file

For my use-case, I’ve got a highly customised deployment pipeline, and as such, I’m essentially just uploading a pre-built application to SWA, but that’s not the most common approach, instead, you’re more likely wanting to get the resource configured for deployment as part of the Bicep template, and we control that with the properties section.

Here’s a more expanded definition, adapted from the Bicep docs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
  properties: {
    branch: 'string'
    buildProperties: {
      apiBuildCommand: 'string'
      apiLocation: 'string'
      appArtifactLocation: 'string'
      appBuildCommand: 'string'
      appLocation: 'string'
      githubActionSecretNameOverride: 'string'
      outputLocation: 'string'
      skipGithubActionWorkflowGeneration: bool
    }
    provider: 'string'
    repositoryToken: 'string'
    repositoryUrl: 'string'
  }

Including values for this will instruct Azure to provision the GitHub Actions workflow for you, and you can use the branch and repositoryUrl to specify what to build, and the other options will configure how it’s built (where the app and API are in the repo, etc.), so I could have set it like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
resource swa_resource 'Microsoft.Web/staticSites@2021-01-15' = {
    name: name
    location: location
    tags: null
    properties: {
        branch: 'main',
        repositoryToken: tokenParam,
        repositoryUrl: 'https://github.com/aaronpowell/aaronpowell.github.io',
        buildProperties: {
            appLocation: './',
            apiLocation: './api',
            outputLocation: './output'
        }
    }
    sku: {
        name: sku
        size: sku
    }
}

(That’s not entirely reflective of my repo setup as it’s highly customised, but it gives an overview.)

Modularlising the Bicep file

The swa.bicep file is really all we need, but let’s put some good practices in place and modularise it so that if we want to add other services in the future, we can do that without too much hassle. For this, create another file, main.bicep, which will be our entrypoint:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
param location string = resourceGroup().location

param swaName string
@allowed([ 'Free', 'Standard' ])
param swaSku string = 'Free'

module staticWebApp 'swa.bicep' = {
    name: '${deployment().name}--swa'
    params: {
        location: location
        sku: swaSku
        name: swaName
    }
}

We’ve got the same set of params defined as the other file, with the only difference being the location is being derived from the resource group that we’re deploying to using the resourceGroup() function.

To call the swa.bicep file, we’re defining it as a module and passing in the param values that it needs, and giving it a dynamically generated name.

With the files setup, it’s time to use them from GitHub Actions.

Deploying with GitHub Actions

Since Bicep is a DSL over ARM (Azure Resource Manager), we can use the azure/arm-deploy GitHub Action to deploy it as well, since the Action will determine if we’re deploying a Bicep or ARM file.

But before we can deploy that, we’re going to need to log into Azure, which we can do with the azure/login Action:

1
2
3
4
5
6
- name: Azure Login
  uses: azure/login@v1
  with:
    client-id: ${{ secrets.AZURE_CLIENT_ID }}
    tenant-id: ${{ secrets.AZURE_TENANT_ID }}
    subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

There’s different ways that you can provide credentials to Azure for the Action to authenticate with, my preference is to use the OIDC Connect method as its setup is the most straight forward to me.

Note: Changing the permissions of the GITHUB_TOKEN is required but it may cause an unexpected side effect that PR comments won’t work. Check out this post for how to address it.

Follow the guide on setting up via Portal, CLI or PowerShell, I prefer Azure CLI myself:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
app=$(az ad app create --display-name blog-deployer -o json)
appId=$(echo $app | jq -r '.appId')
objectId=$(echo $app | jq -r '.id')

sp=$(az ad sp create --id $appId -o json)
assigneeObjectId=$(echo $sp | jq -r '.id')

subscriptionId=...
resourceGroupName=...
az role assignment create --role contributor --subscription $subscriptionId --assignee-object-id  $assigneeObjectId --assignee-principal-type ServicePrincipal --scope /subscriptions/$subscriptionId/resourceGroups/$resourceGroupName

credentialName=github-deploy
subject=repo:aaronpowell/aaronpowell.github.io:environment:production
az rest --method POST --uri "https://graph.microsoft.com/beta/applications/$objectId/federatedIdentityCredentials" --body "{\"name\":\"$credentialName\",\"issuer\":\"https://token.actions.githubusercontent.com\",\"subject\":\"repo:organization/repository:environment:production\",\"description\":\"Deploy from GitHub Actions\",\"audiences\":[\"api://AzureADTokenExchange\"]}"

echo Values for GitHub secrets:
echo Client ID: $appId
echo Tenant ID: $(echo $sp | jq -r '.appOwnerOrganizationId')
echo Subscription ID: $subscriptionId

This script will go through the steps to create the app in Azure AD, setup a Service Principal and then provision the identity to talk to GitHub, before dumping out the three bits of information you’ll need to authenticate the Action with.

You’ll need to provide the right subscriptionId and resourceGroupName, and the only other value you may wish to change is subject. For this pipeline, we’ll use GitHub Environments to deploy, which is denoted by environment:<environment name>, where environment name is production. Give that a name and then call the az rest comand to create the credential (this feature is still in preview, in the future there may be a direct Azure CLI command to use).

Now, plug the three values it outputs into GitHub secrets, so the Action can use them.

Running Bicep from GitHub Actions

With a step for azure/login setup, the next step needs to run the Bicep template with the azure/arm-deploy Action.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
- name: Ensure resource group exists
  uses: azure/CLI@v1
  with:
    inlineScript: |
      az group create -g ${{ secrets.RESOURCE_GROUP }} -l ${{ secrets.RESOURCE_GROUP_LOCATION }}      
- name: Deploy Bicep
  uses: azure/arm-deploy@v1
  with:
    resourceGroupName: ${{ secrets.RESOURCE_GROUP }}
    subscriptionId: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
    template: ./deploy/main.bicep
    parameters: swaName=${{ secrets.SWA_NAME }}
    failOnStdErr: false

Well, first we’re using azure/CLI to ensure that the resource group exists, and then we’re using azure/arm-deploy to deploy the Bicep template.

The path to the main.bicep template is set as an argument to the Action, along with the parameters that it needs. Since we’re using default values for location and sku, we don’t need to pass those in.

With this added to your workflow before the action azure/static-web-apps-deploy, you now have Infrastructure as Code for deploying your static web apps.

Conclusion

Throughout this post we looked at how to create a simple Bicep template that will deploy a Static Web App, and how to use it from GitHub Actions. We also saw the process for setting up the authentication from GitHub Actions to Azure, using the OIDC connect method.

Now that this is added, we have a completely reproducable pipeline that can be used to deploy your static web apps, from resource provisioning to deployment, in the case we ever need to start from scratch again.

You can check this out in action by looking at the GitHub Actions for my blog which I have refactored to use Bicep (and it only took half a dozen deployments!).