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.