Skip to main content

Migrating from GitHub Pages to Azure Static Web Apps

· 8 min read

You can use Bicep and GitHub Actions to build and deploy to a static website on Azure Static Web Apps. This post demonstrates how.

title image reading "Migrating from GitHub Pages to Azure Static Web Apps" with GitHub and Azure Static Web Apps logos

Why migrate?

This blog has been hosted on GitHub Pages for some time. It also makes use of Netlify for deployment previews. These are both great, but it's always niggled that there's two mechanisms in play; each separately configured. It's time to simplify.

Azure Static Web Apps supports both hosting static websites and deployment previews (known as "staging environments"). So we're going to migrate across to use Static Web Apps in place of both of GitHub Pages and Netlify. I'm choosing to use Bicep to do this as I tend towards using infrastructure as code. If you wanted to roll with a more "point and click" approach in the Azure Portal, you could do that too. Simply ignore the Bicep related portions of the post.

Bicep

The first thing we're going to need is a Bicep template to deploy our SWA. In our GitHub repo we're going to add a infra folder, and in there we'll create a main.bicep file:

param location string
param branch string
param name string
param tags object
@secure()
param repositoryToken string
param customDomainName string

resource staticWebApp 'Microsoft.Web/[email protected]' = {
name: name
location: location
tags: tags
sku: {
name: 'Free'
tier: 'Free'
}
properties: {
repositoryUrl: 'https://github.com/johnnyreilly/blog.johnnyreilly.com'
repositoryToken: repositoryToken
branch: branch
provider: 'GitHub'
stagingEnvironmentPolicy: 'Enabled'
allowConfigFileUpdates: true
buildProperties:{
skipGithubActionWorkflowGeneration: true
}
}
}

// resource customDomain 'Microsoft.Web/staticSites/[email protected]' = {
// parent: staticWebApp
// name: customDomainName
// properties: {}
// }

output staticWebAppDefaultHostName string = staticWebApp.properties.defaultHostname // eg gentle-bush-0db02ce03.azurestaticapps.net
output staticWebAppId string = staticWebApp.id
output staticWebAppName string = staticWebApp.name

Most of the Bicep template above is self-explanatory. There's a few things to highlight:

  • We're using the "Free" SKU which means we don't have to pay to run our website.
  • We need to provide a repositoryToken - this is a little surprising as you'll see later in the template that we supply the skipGithubActionWorkflowGeneration: true which means we're not requiring our SWA to interact with GitHub on our behalf - but it seems that there's a requirement for a GitHub token anyway. We'll roll with it.
  • We're enabling deployment previews / staging environments with stagingEnvironmentPolicy: 'Enabled'
  • The branch is always set to main - we have to let Azure know this so it knows which branch is the primary branch and hence which other ones will have staging environments.
  • It also includes a section for the custom domain which is commented out - we'll uncomment that later once we've set up our custom domain / DNS.

Setting up a resource group

With our Bicep in place, we're going to need a resource group to send it to. We're going to create ourselves a resource group in West Europe:

az group create -g rg-blog-johnnyreilly-com -l westeurope

Secrets for GitHub Actions

We're aiming to set up a GitHub Action to handle our deployment which depends upon some secrets.

AZURE_CREDENTIALS - GitHub logging into Azure

First a AZURE_CREDENTIALS secret that facilitates GitHub logging into Azure. We'll use the Azure CLI to create this:

az ad sp create-for-rbac --name "myApp" --role contributor \
--scopes /subscriptions/{subscription-id}/resourceGroups/{resource-group} \
--sdk-auth

Remember to replace the {subscription-id} with your subscription id and {resource-group} with the name of your resource group (rg-blog-johnnyreilly-com if you're following along). This command will pump out a lump of JSON that looks something like this:

{
"clientId": "a-client-id",
"clientSecret": "a-client-secret",
"subscriptionId": "a-subscription-id",
"tenantId": "a-tenant-id",
"activeDirectoryEndpointUrl": "https://login.microsoftonline.com",
"resourceManagerEndpointUrl": "https://management.azure.com/",
"activeDirectoryGraphResourceId": "https://graph.windows.net/",
"sqlManagementEndpointUrl": "https://management.core.windows.net:8443/",
"galleryEndpointUrl": "https://gallery.azure.com/",
"managementEndpointUrl": "https://management.core.windows.net/"
}

Take this and save it as the AZURE_CREDENTIALS secret in GitHub.

WORKFLOW_TOKEN - Azure accessing the GitHub container registry

We also need a secret for updating workflows from Azure. Azure Static Web Apps can update your workflow - they need access to do this when we're deploying. To facilitate this we'll set up a WORKFLOW_TOKEN secret. This is a GitHub personal access token with the workflow scope. Follow the instructions here to create the token.

Ironically, we're not planning to use this functionality, but the validation for the Bicep template will fail if it isn't supplied.

Deploying with GitHub Actions

With our secrets configured, we're now well placed to update our GitHub Action. We'll tweak the content of .github/workflows/build-and-deploy.yaml file in our repository to the following:

name: Build and Deploy

on:
push:
branches:
- main
pull_request:
types: [opened, synchronize, reopened, closed]
branches:
- main

env:
RESOURCE_GROUP: rg-blog-johnnyreilly-com
LOCATION: westeurope
STATICWEBAPPNAME: blog.johnnyreilly.com
TAGS: '{"owner":"johnnyreilly", "email":"[email protected]"}'

jobs:
build_and_deploy_swa_job:
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
runs-on: ubuntu-latest
name: Build and deploy
steps:
- uses: actions/[email protected]
with:
submodules: true

- name: Azure Login
uses: azure/[email protected]
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}

- name: Set Deployment Name
id: deployment_name
run: |
REF_SHA='${{ github.ref }}.${{ github.sha }}'
DEPLOYMENT_NAME="${REF_SHA////-}"
echo "::set-output name=DEPLOYMENT_NAME::$DEPLOYMENT_NAME"

- name: Static Web App - change details
id: static_web_app_what_if
if: github.event_name == 'pull_request'
uses: azure/[email protected]
with:
inlineScript: |
az deployment group what-if \
--resource-group ${{ env.RESOURCE_GROUP }} \
--name "${{ steps.deployment_name.outputs.DEPLOYMENT_NAME }}" \
--template-file ./infra/main.bicep \
--parameters \
branch='main' \
location='${{ env.LOCATION }}' \
name='${{ env.STATICWEBAPPNAME }}' \
tags='${{ env.TAGS }}' \
repositoryToken='${{ secrets.WORKFLOW_TOKEN }}' \
customDomainName='${{ env.STATICWEBAPPNAME }}'

- name: Static Web App - deploy infra
id: static_web_app_deploy
if: github.event_name != 'pull_request'
uses: azure/[email protected]
with:
inlineScript: |
az deployment group create \
--resource-group ${{ env.RESOURCE_GROUP }} \
--name "${{ steps.deployment_name.outputs.DEPLOYMENT_NAME }}" \
--template-file ./infra/main.bicep \
--parameters \
branch='main' \
location='${{ env.LOCATION }}' \
name='${{ env.STATICWEBAPPNAME }}' \
tags='${{ env.TAGS }}' \
repositoryToken='${{ secrets.WORKFLOW_TOKEN }}' \
customDomainName='${{ env.STATICWEBAPPNAME }}'

- name: Static Web App - get API key for deployment
id: static_web_app_apikey
uses: azure/[email protected]
with:
inlineScript: |
APIKEY=$(az staticwebapp secrets list --name '${{ env.STATICWEBAPPNAME }}' | jq -r '.properties.apiKey')
echo "::set-output name=APIKEY::$APIKEY"

- name: Static Web App - build and deploy
id: static_web_app_build_and_deploy
uses: Azure/static-web-apps-[email protected]
with:
azure_static_web_apps_api_token: ${{ steps.static_web_app_apikey.outputs.APIKEY }}
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: '/blog-website' # App source code path
api_location: '' # Api source code path - optional
output_location: 'build' # Built app content directory - optional
###### End of Repository/Build Configurations ######

- name: Static Web App - get preview URL
id: static_web_app_preview_url
uses: azure/[email protected]
with:
inlineScript: |
DEFAULTHOSTNAME=$(az staticwebapp show -n '${{ env.STATICWEBAPPNAME }}' | jq -r '.defaultHostname')
echo $DEFAULTHOSTNAME

PREVIEW_URL="https://${DEFAULTHOSTNAME/.[1-9]./-${{github.event.pull_request.number }}.${{ env.LOCATION }}.1.}"
echo $PREVIEW_URL

echo "::set-output name=PREVIEW_URL::$PREVIEW_URL"

outputs:
preview-url: ${{steps.static_web_app_preview_url.outputs.PREVIEW_URL}}

close_pull_request_job:
if: github.event_name == 'pull_request' && github.event.action == 'closed'
runs-on: ubuntu-latest
name: Cleanup Pull Request staging environment
steps:
- name: Azure Login
uses: azure/[email protected]
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}

- name: Get API key for deployment
id: apikey
uses: azure/[email protected]
with:
inlineScript: |
APIKEY=$(az staticwebapp secrets list --name '${{ env.STATICWEBAPPNAME }}' | jq -r '.properties.apiKey')
echo "::set-output name=APIKEY::$APIKEY"

- name: Close Pull Request
id: closepullrequest
uses: Azure/static-web-apps-[email protected]
with:
azure_static_web_apps_api_token: ${{ steps.apikey.outputs.APIKEY }}
action: 'close'

The above workflow does the following:

  • For main branch deployments it releases our static web app making use of Bicep. For pull requests it tells us if there's any changes that the current PR would make to our SWA as a consequence.
  • It acquires an API Key from Azure which can then be used to perform a deployment.
  • It deploys using the dedicated GitHub Action for SWAs
  • It calculates the preview URL for a given pull request (it isn't used as yet, but could be)
  • When a pull request is closed it triggers the GitHub Action to clean up the preview environment.

DNS and custom domains

Once our GitHub Action has run for the first time on the main branch, we'll be deploying to Azure Static Web Apps.

Once we've started deploying there, we want to get our custom domain set up to point to it. To do this, we're going to fire up the Azure Portal and go to add a custom domain:

screenshot of the Azure Portal Add Custom Domain screen

We're going to add a TXT record for my blog. Azure generates a code for us:

screenshot of the Azure Portal Add Custom Domain screen

We need to take that code and go a register it with our DNS provider. In my case that's Cloudflare, so we can go there and add it:

screenshot of Cloudflare

After a while (I think about twenty minutes in my case), this lead to the domain name being validated:

screenshot of the Azure Portal Add Custom Domain screen with domain validated

Now that we have a custom domain set up in Azure, we want to uncomment the resource customDomain portion of the Bicep template now as well:

resource customDomain 'Microsoft.Web/staticSites/[email protected]' = {
parent: staticWebApp
name: customDomainName
properties: {}
}

This will mean that subsequent deployments to Azure do not wipe out our newly configured domain name.

We're now ready to start pointing our DNS to the Static Web Apps instance. We jump back across to Cloudflare and we amend the CNAME record that currently points to johnnyreilly.github.io, and switch it to point to the auto-generated domain in Azure:

screenshot of Cloudflare with the CNAME record set

And just like that, we're hosted on Static Web Apps!