In part one and part two of the series, you have learned what makes installing Umbraco on Azure different from installing it on-prem and avoiding creating all the needed resources manually using ARM templates.
In the third part of the series, you will automate the deployment of both code and infrastructure using GitHub Actions.
While this article is related to Umbraco deployment to Azure, the concepts and workflow used here can be used to deploy any .NET website.
Introduction to GitHub Actions
Actions is the name of the CI/CD workflow of GitHub. If you used Azure DevOps, this is the feature that corresponds to DevOps Pipelines.
Actions are event-driven, so you can run a workflow with a specific series of operations whenever an event happens: for example, build the project for each pull request that is created.
GitHub Actions is very wide topic, so I'll not cover it here in detail. Still, if you are interested in going beyond the content of this article, I recommend you deep dive into the official documentation on Github.com.
The Build and Deploy Workflow
We want to build the website and deploy it to Azure every time code is pushed to the main branch. In a real-life situation, the conditions might be different, with approval steps before deploying to production and with multiple environments involved, but let's keep it simple for this article.
Let's see each of the logical steps that are needed to achieve this goal.
Setting up the workflow
Each workflow runs on a separate and isolated virtual machine that GitHub provisions. For this reason, some steps need to be performed to configure the virtual machine for the tasks that we need to perform.
One of the first lines of the file specifies which type of "base" runner (windows, Linux or macOS) we need for our workflow.
runs-on: windows-latest
This is just a basic windows VM, with no tools installed. Our next step is installing and configuring the ones we need:
- nuget
- msbuild
Luckily, there is an action for that: setup-msbuild(which sets up both nuget and msbuild).
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v1.0.3
We also want to get the latest code of the repository. It seems obvious, but since actions can also be used for running other types of workflows that don't require the repo's content (for example, on new an issue or PR is created), we need to get the latest version of the code.
- uses: actions/checkout@v2
Building the Project
After the runner setup is complete, we need to download all the dependencies (using nuget) and build the project.
We don't need any action from the marketplace, but we can run the msbuild command with the required parameters.
- name: Restore NuGet packages
working-directory: ${{env.GITHUB_WORKSPACE}}
run: nuget restore ${{env.SOLUTION_FILE_PATH}}
- name: Build
working-directory: ${{env.GITHUB_WORKSPACE}}
run: msbuild /m /p:Configuration=${{env.BUILD_CONFIGURATION}} ${{env.WEBSITEPROJ_FILE_PATH}}
Notice the usage of the ${{env.***}} syntax through the configuration. They are used to refer to variables defined internally in the workflow or coming from the environment where the workflow is running.
Environment variable are defined at job level and it's a good practice to define file name and path at the top of the workflow in a centralized location, especially if they are reused multiple times.
env:
# Path to the solution file relative to the root of the project.
SOLUTION_FILE_PATH: ./src/UmbracoDevOps.sln
WEBSITEPROJ_FILE_PATH: ./src/UmbracoDevOps.Web/UmbracoDevOps.Web.csproj
BUILD_CONFIGURATION: Release
Authenticating to Azure
With the code packaged, ready to be deployed, we now have to setup the Azure infrastructure. But to do so, we need to authenticate our workflow to Azure. To do so, we need the Azure/login action.
- name: Azure login
uses: Azure/login@v1.4.0
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
When calling this action, we are passing as credentials a value stored in the secrets management section of GitHub. In this case, the value is the JSON returned by the AZ CLI when you create a service principal using the command:
az ad sp create-for-rbac --name "myApp" --role contributor \
--scopes /subscriptions/{subscription-id}/resourceGroups/{resource-group} \
--sdk-auth
The output is in the format:
{
"clientId": "<GUID>",
"clientSecret": "<GUID>",
"subscriptionId": "<GUID>",
"tenantId": "<GUID>",
"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/"
}
Provisioning the Azure Resources
After all the setup, it's finally time to deploy the ARM templates. This is done via the Azure/arm-deploy action.
- uses: azure/arm-deploy@v1
id: deploy
with:
resourceGroupName: ${{env.RESOURCE_GROUP}}
template: ${{env.ARM_TEMPLATE_FILE_PATH}}
parameters: ${{env.ARM_TEMPLATE_PARAMS_FILE_PATH}} sqlServerPassword=${{ secrets.SQL_SERVER }}
deploymentName: deployment-from-actions
As parameters, you can specify both the parameter file or override some variables directly in the workflow. In this case, you don't want to commit the SQL Server password in the parameters' file, but you can inject it, taking the value from the secrets.
If you remember from the second part of the series, the deployment template returns an object with the publishing profiles for the web apps.
"publishProfile": {
"type": "object",
"value": "[list(concat('Microsoft.Web/sites/', variables('webAppFEName') ,'/config/publishingcredentials'), '2019-08-01')]"
},
"publishProfileBO": {
"type": "object",
"value": "[list(concat('Microsoft.Web/sites/', variables('webAppBOName') ,'/config/publishingcredentials'), '2019-08-01')]"
}
With GitHub actions we can reference to the output of various steps using steps.deploy.outputs.publishProfile, where deploy is the id used from the deployment step.
Deploy the solution
The final step is taking the package created during the build and deploying it to the web app just created.
The action to use for this task is azure/webapps-deploy.
- name: Deploy frontend website
uses: azure/webapps-deploy@v2
with:
app-name: ${{ steps.deploy.outputs.webAppFEName }}
publish-profile: ${{ steps.deploy.outputs.publishProfile }}
package: ${{env.WEBSITEPROJ_PATH}}
Since our Umbraco project is set up with two web apps, the front and backend, you have to run the deployment twice.
You can see the complete version of workflow on the repository on GitHub.
Conclusions
In this article we have connected the dots and setup a fully automated and repeatable CI/CD pipeline that builds the code committed, provisions the Azure resources needed to host the site and finally deploys the code.
This workflow still lacks one of the main features promised in the introduction to the series: it still does not support multiple environments. Deployments to multiple environments will be covered in a bonus part that will be published at the beginning of 2022.
List of GitHub Actions Used
If you want to learn more about each action used in this example, here is the list of all of them.
Articles in this series
- Umbraco DevOps, Part 1: How to Configure Umbraco to Run on Azure
- Umbraco DevOps, Part 2: Automating Azure Resources Creation with ARM Templates
- Umbraco DevOps, Part 3: Implementing a CI/CD Workflow Using GitHub Actions