Power Umbraco with a bit of Azure

With the release of Umbraco 9 a whole new era is born and really gives you the opportunity to run your Umbraco solution in new ways.

In this blog we dive into the Azure building block you can use to run Umbraco 9 on Azure. We look at Docker containers, Azure WebApps, networking and scaling, storage and Azure SQL. Finally we dive in how you can combine those buildings block to fit your specific scenario.

Part 1 - Setup your Azure Resources

To get started we need to create a few Azure Resources. For this we will use the Azure CLI, but you can also create them from the Azure Portal or use ARM of BICEP templates.

# Login to your Azure Subscription
az login

# Create a resource group
az group create \
    --location westeurope \
    --resource-group <RESOURCE_GROUP_NAME>

Part 1.2 - Create an Azure SQL Database

For Umbraco to work we need an SQL Database, in the code below we will create an Azure SQL database in the S1 service tier. This is enough for development or low traffic website. If you run a heavy website it is recommended to go for the P tier.

In the example below we create first an SQL Server, an SQL Server can hold multiple databases. Firewall and security is managed on the database level. Second we create the database for Umbraco and finally we create a firewall rule that enables connections to the server from any Azure Resource. If you need access from your dev environment you have to add your IP also to the firewall.

Read more about Azure SQL on Microsoft Docs.

# Create the SQL Server
az sql server create \
    --name <SERVER_NAME> \ 
    --admin-user <ADMIN_USER> \
    --admin-password <ADMIN_PASSWORD> \
    --location westeurope \
    --resource-group <RESOURCE_GROUP_NAME>

# Create the database on the server
az sql db create \
    --server <SERVER_NAME> \
    --name DATABASE_NAME> 
    --service-objective S1 
    --resource-group <RESOURCE_GROUP_NAME> 

# Grant Azure Resources access to server
az sql server firewall-rule create \
    --server <SERVER_NAME> \
    --name AllowAzureServices \
    --start-ip-address 0.0.0.0 \
    --end-ip-address 0.0.0.0 \
    --resource-group <RESOURCE_GROUP_NAME>  

# Show the connection string
az sql db show-connection-string --client ado.net \
    --server <SERVER_NAME> \
    --resource-group <RESOURCE_GROUP_NAME>  

Part 1.3 - Create a storage account

To store the media from Umbraco we are using an Azure Storage Account, this make sure that images are stored outside of Umbraco and helps to run Umbraco in containers.

Read more about Azure Storage Accounts on Microsoft Docs.

# Create the storage account
az storage account create \
    --name <STORAGE_ACCOUNT_NAME> \
    --resource-group <RESOURCE_GROUP_NAME> \  
    --location westeurope \
    --sku Standard_GRS \
    --encryption-services blob

# Create a public container on the storage account
az storage container create \
    --name <STORAGE_CONTAINER_NAME> \
    --public-access blob \
    --account-name <STORAGE_ACCOUNT_NAME>  \
    --resource-group <RESOURCE_GROUP_NAME>   

# Show the connection string
az storage account show-connection-string \ 
    --name <STORAGE_ACCOUNT_NAME> 
    --query "connectionString" -o tsv

Part 1.4 - Add a Content Delivery Network

By default an Azure Blob Storage account is readable from one region, to put images close to the website visitors we can add a Content Delivery Network in front of the storage account.

Example
If we have storage account called umbraco9.blob.core.windows.net and publicly accessible container assets.

We can create a CDN umbraco9.azureedge.net that points to hostname umbraco9.blob.core.windows.net and path: /assets/.

Note that the CDN will not work if you don't add the origin-host-header parameter.

Read more about Content Delivery Networks on Microsoft Docs.

# Create A CDN Profile
az cdn profile create \
    --name <CDN_PROFILE_NAME> \
    --resource-group <RESOURCE_GROUP_NAME>   
    --sku Standard_Microsoft

# Create a CDN Endpoint for the Storage Container
az cdn endpoint create \
    --name <ENDPOINT_NAME> \
    --profile-name <CDN_PROFILE_NAME> \
    --origin <STORAGE_ACCOUNT_NAME>.blob.core.windows.net \
    --origin-path "/<STORAGE_CONTAINER_NAME>/" \
    --origin-host-header <STORAGE_ACCOUNT_NAME>.blob.core.windows.net \
    --resource-group <RESOURCE_GROUP_NAME>  

Part 2 - Setup Umbraco 9.x

In the previous steps we created the resources we needed before we could get started with Umbraco, a database and storage account.

Now we can start with setting up Umbraco 9.

Read more about installing Umbraco on Umbraco Docs.

Before you continue create a GitHub repository

# First create a directory
mkdir source

# Create the dotnet solution and project
dotnet new -i Umbraco.Templates
dotnet new sln -n <SOLUTION_NAME>
dotnet new umbraco -n <PROJECT_NAME> --connection-string "<SQL_CONNECTION_STRING>"
dotnet sln add <PROJECT_NAME>
cd <PROJECT_NAME>

# Add the Umbraco Azure Blob Storage Provider package
dotnet add package Umbraco.StorageProviders.AzureBlob

Next let's enable the Umbraco.StorageProviders.AzureBlob in Umbraco.

Add the lines below to the method ConfigureServices in file startup.cs

    .AddAzureBlobMediaFileSystem() 
    .AddCdnMediaUrlProvider()

Add the lines below to the method Configure after ** u.UseWebsite();** in file startup.cs

    u.UseAzureBlobMediaFileSystem();

Add the section Storage to appsettings.json and appsettings.Development.json. For developerment:

  • Add appsettings.Development.json to your .gitignore file.
  • Add the connectionstring / containername / and CDN url to the appsettings.Development.json file.
{
  "Umbraco": {
    "Storage": {
      "AzureBlob": {
        "Media": {
          "ConnectionString": "",
          "ContainerName": "",
          "Cdn": {
            "Url": ""
          }
        }
      }
    }
  }
}

Now everything is ready to launch Umbraco and follow the setup and initialize the Umbraco Database.

dotnet run

Part 3 - Docker

In this section we are going to package up our Umbraco installation in a container, so later we can deploy these containers to Azure.

Don't have Docker on your machine, the easiest way is to install Docker Desktop.

Read more about Containers on Microsoft Docs

Part 3.1 - Dockerfile

Create an empty file "Dockerfile" on the same level as the source directory and add the content below.

Replace UmbracoTogether.Web.dll with the name of your dll.

FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /source

# copy csproj and restore
COPY ./source/ /source/
RUN dotnet restore

RUN dotnet build -c Release

# publish app and libraries
RUN dotnet publish -c release -o /app --no-restore

# Build runtime image
FROM mcr.microsoft.com/dotnet/aspnet:5.0
EXPOSE 80
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "UmbracoTogether.Web.dll"]

Part 3.2 - Build the container

Next we build the container.

docker build -t umbraco:latest .

Part 3.3 - Run the container locally

To run the container you need to specify the environment variables, for this you can use the -e parameter. The -p command maps a port inside the container to a public port. In the example below we expose Umbraco on port 80.

docker run -p 80:80 -t umbraco:latest 
    -e "ConnectionStrings:umbracoDbDSN"="<SQL_CONNECTION_STRING>" 
    -e Umbraco:Storage:AzureBlob:Media:ConnectionString="<STORAGE_CONNECTION_STRING>" 
    -e Umbraco:Storage:AzureBlob:Media:ContainerName="<STORAGE_CONTAINER_NAME>" 
    -e Umbraco:Storage:AzureBlob:Media:Cdn:Url="<CDN_PROFILE_URL>"

Part 4 - Run the containers in Azure

In step 3 we created a container. This container is now only available on your dev machine. The next thing now we have to do is store in in a central place.

Part 4.1 - Create and Azure Container Registry

In Azure you can create a Azure Container Registry here you can store privately your container image.

In the sample below we create a registry, enable admin with username and password and perform an login using the CLI.

Read more about Azure Container Registries on Microsoft Docs.

# Create the Azure Container registry
az acr create 
    --name <ACR_NAME> 
    --sku Basic 
    --admin-enabled true 
    --resource-group <RESOURCE_GROUP_NAME>   

# Retrieve the credentials
az acr credential show --name <ACR_NAME> --query "passwords[0].value"
az acr credential show --name <ACR_NAME> --query "username"

# Login to the registry
az acr login -n <ACR_NAME>
# Build and tag
docker build . -t <ACR_NAME>.azurecr.io/umbraco:latest

# Push to image to the ACR
docker push <ACR_NAME>.azurecr.io/umbraco:latest

Now your image is in your registry and you can try and run it on a different machine.

Part 5 - CI & CD to Azure using Github Actions

The final step is to setup a basic CI / CD pipeline in GitHub actions.

In this action we will:

  • Monitor commits on the Master Branch
  • Build the container
  • Push the container to our ACR
  • Deploy the container to an Azure Container Instance in West Europe
  • Deploy the container to an Azure Container Instance in North America
  • Add the ACI's to a Traffic manager profile
  • Clean up the old container instances (TODO)

Learn more about GitHub Actions on GitHub Docs.

Learn more about Azure Traffic Manager on Microsoft Docs.

Part 5.1 - Create a service principle

For GitHub actions to access resources in Azure you need create a service principle and grant this service principle access to a resource group.

Use this bash script below to generate a a service principle.

#!/bin/bash
set -e

# Set the following
spName = <NAME>
subName = <AZURE_SUBSCRIPTION_NAME>
subscriptionId = <AZURE_SUBSCRIPTION_GUID>
resourceGroup = <<RESOURCE_GROUP_NAME>

# set the subscription
az account set --subscription "$subName" 

# Create a service principal
    echo "Creating service principal..."
    spInfo=$(az ad sp create-for-rbac --name "$spName" \
            --scopes /subscriptions/$subscriptionId/resourceGroups/$resourceGroup \
            --role contributor  \
            --sdk-auth)

    # save spInfo locally
    echo $spInfo > auth.json        

    if [ $? == 0 ]; then
        
        echo '========================================================='
        echo 'GitHub secrets for configuring GitHub workflow'
        echo '========================================================='
        echo "AZURE_CREDENTIALS: $spInfo"
        echo '========================================================='
    else
        "An error occurred. Please try again."
         exit 1
    fi

Part 5.2 - Add GitHub secrets

Add the contents of auth.json to the GitHub Secrets AZURE_CREDENTIALS

After you added he content remove the auth.json file and make sure you leave it out of your version control.

Add the following other secrets to GitHub:

Read more about secrets in GitHub on GitHub Docs

ACR_USERNAME
ACR_PASSWORD
SQL_CONSTR
STORAGE_CONN_STRING
STORAGE_CONTAINER
CDN_URL
TRAFFIC_MANAGER_DNS

Part 5.3 - Create the GitHub Action

Add the content below to the file: .github/workflows/build_and_deploy.yml to create the github action.

Adjust the variables under env

on:
  push:
    branches:
      - main

name: Umbraco 9 Build & Deploy

env:
  # basic  
  resourceGroup: My_Resource_Group
  location: westeurope
  subName: "my-subscription-name"

  # app specific
  acrName: xxxx.azurecr.io

  # aci
  image_name: umbraco

jobs:
  build:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v2

      - uses: azure/docker-login@v1
        with:
          login-server: ${{ env.acrName }}
          username: ${{ secrets.ACR_USERNAME }}
          password: ${{ secrets.ACR_PASSWORD }}

      - run: |
          docker build . -t ${{ env.acrName }}/${{ env.image_name }}:${{ github.sha }}
          docker push ${{ env.acrName }}/${{ env.image_name }}:${{ github.sha }}
  deploy:
    name: Deploy
    runs-on: ubuntu-latest
    needs: build

    steps:

      - name: 'Login via Azure CLI'
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: 'Deploy to Europe Azure Container Instances'
        uses: 'azure/aci-deploy@v1'
        with:
          resource-group: ${{ env.resourceGroup }}
          dns-name-label: ${{ github.sha }}-eu
          image: ${{ env.acrName }}/${{ env.image_name }}:${{ github.sha }}
          registry-login-server:  ${{ env.acrName }}
          registry-username: ${{ secrets.ACR_USERNAME }}
          registry-password: ${{ secrets.ACR_PASSWORD }}
          name: umbraco9-eu-${{ github.sha }}
          secure-environment-variables: ConnectionStrings__umbracoDbDSN="${{ secrets.SQL_CONSTR }}" Umbraco__Storage__AzureBlob__Media__ConnectionString="${{ secrets.DEV_STORAGE }}" Umbraco__Storage__AzureBlob__Media__ContainerName="${{secrets.STORAGE_CONTAINER}}" Umbraco__Storage__AzureBlob__Media__Cdn__Url="${{secrets.CDN_URL}}" 
          location: westeurope
          cpu: 1
          memory: 2gb
          ports: 80

      - name: 'Deploy to West US Azure Container Instances'
        uses: 'azure/aci-deploy@v1'
        with:
          resource-group: ${{ env.resourceGroup }}
          dns-name-label: ${{ github.sha }}-us
          image: ${{ env.acrName }}/${{ env.image_name }}:${{ github.sha }}
          registry-login-server:  ${{ env.acrName }}
          registry-username: ${{ secrets.ACR_USERNAME }}
          registry-password: ${{ secrets.ACR_PASSWORD }}
          name: umbraco9-us-${{ github.sha }}
          secure-environment-variables: ConnectionStrings__umbracoDbDSN="${{ secrets.SQL_CONSTR }}" Umbraco__Storage__AzureBlob__Media__ConnectionString="${{ secrets.STORAGE_CONN_STRING }}" Umbraco__Storage__AzureBlob__Media__ContainerName="${{secrets.STORAGE_CONTAINER}}" Umbraco__Storage__AzureBlob__Media__Cdn__Url="${{secrets.CDN_URL}}" 
          location: eastus
          cpu: 1
          memory: 2gb
          ports: 80
          
      - name: 'Add to Traffic Manager'
        run: |
          az network traffic-manager profile create --name ${{ secrets.TRAFFIC_MANAGER_DNS }} \
            --routing-method Weighted \
            --path "/" \
            --protocol HTTP \
            --unique-dns-name ${{ secrets.TRAFFIC_MANAGER_DNS }} \
            --ttl 10 \
            --port 80 \
            --resource-group ${{ env.resourceGroup }}
          az network traffic-manager endpoint create -g ${{ env.resourceGroup }} \
              -n ${{ github.sha }}-eu \
              --profile-name ${{ secrets.TRAFFIC_MANAGER_DNS }} \
              --type externalEndpoints \
              --weight 1 \
              --target ${{ github.sha }}-eu.westeurope.azurecontainer.io
          az network traffic-manager endpoint create -g ${{ env.resourceGroup }} \
              -n ${{ github.sha }}-us \
              --profile-name ${{ secrets.TRAFFIC_MANAGER_DNS }} \
              --type externalEndpoints \
              --weight 1 \
              --target ${{ github.sha }}-us.eastus.azurecontainer.io

Now you have a globally available, load balanced Umbraco 9 that can be deployed with a GitHub action.