I needed to make a small change to an old Azure function, and ended up tinkering a bit on how to automate the deployment process with some of the latest options. And I just fell in love with Azure (DevOps) Pipelines & Azure Functions Run-from-Package functionality.
I don’t consider myself an expert on Azure DevOps (also formerly known as Visual Studio Team Services = VSTS or Visual Studio Online = VSO) – quite the opposite, as I usually have the possibility to ask from real experts. But this time I just started experimenting a bit. An old Azure function used to implement a complex integration interface against a SaaS system needed a new feature. In the early days we hadn’t created automation, mostly because the customer didn’t at the time have their own VSTS tenant, and things were busy at the time – so we decided to postpone this, but now…
The rest of this blog goes to technical “details”, but the key message is: automating deployments is very easy with Azure DevOps! Even if you are just a beginner as the default templates are a good starting point! Easy way to save your nerves (and yours or your customer’s money with every future deployment).
Azure Function Versioning
First of all I wanted to make the version numbering automated to be able to check easily which version I’m running. There are many ways to achieve this, but I’ve been using GitVersion lately because it gives possibility to use Semantic Versioning – and mostly to be able to bump version numbers with commit messages without extra manual work. I started using GitVersion with Logic App ARM templates after I saw Teemu Tapanila’s presentation at IglooConf 2018 (link at the end) because it just seemed to fit the purpose well.
This function project was created before that presentation, so first question was how difficult would it be to include it later to the v1 function project. Well…not difficult at all. Basically I just followed documentation and ran one package manager command:
Install-Module GitVersionTask
…and that’s it. I added the CheckVersion function below to my app to verify this:
public static class CheckVersion { [FunctionName("CheckVersion")] public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)]HttpRequestMessage req, TraceWriter log) { string productVersion = FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).ProductVersion; return req.CreateResponse(HttpStatusCode.OK, "Function version: " + productVersion); } }
And the result started looking good:
“Function version: 1.2.0-alpha.3+Branch.dev.Sha.420e006df651a45cca1c3a903af03bb25dc27b7f”
Azure DevOps Build Pipeline
Originally this v1 function was deployed with Function App internal deployment. It checked repository for changes and started building if changes were committed to certain branches. But I wanted to try to make this deployment as quick as possible and easy to roll back, so the run-from-package option was the way to go.
So – first I needed to create build pipeline that creates zip package that can be then used in release.
And I just created a new build pipeline with the “Use the visual designer” option, chose my repository and branch, and chose the ready C# Function template and…basically, that’s it.
Just added the GitVersion task (already had it installed from the Marketplace) with default variables, although I noticed that it’s not even needed as the version I installed to the project also updates the version number in pipeline.
Then I got another idea – should I also have a debug build available – for example to deploy that to dev & test environments and release version with less traces to production. Or have the possibility to change to debug build if necessary to debug remotely. And another surprise for me how easy this was.
First, add wanted configurations to BuildConfiguration variable…
..then add a variable reference to Pipeline Multiplier field after selecting “Multi-configuration” from above:
…and finally change the package naming in the Archive Files task:
And now both packages are published to the same artifact:
After the build pipeline ran (two builds), the artifact contents were just like I wanted:
Now I decided to tweak a bit more with the build number to have a bit more info than the semantic version like “1.2.0+4”. So I added a PowerShell Script task to run the following Inline-script to add repository name, date and BuildId (mostly for this pipeline debugging purposes) to the final Build Number.
So basically this out-of-the-box pipeline worked for build.
Deploying with Azure DevOps Release Pipeline
And again I just started with a new release pipeline and added an empty stage for dev environment:
Then I linked the build artifact and gave it a shorter alias…
Originally I used two separate tasks, Azure File Copy and Azure CLI. But as the CLI command prompt is not very good with variables, I decided to combine both tasks to a Azure PowerShell script that is more familiar to me. I think bash could also work well, must try it another time. (I’ve been trying to quit command prompt after my EDIFACT years a long, long time ago – after once triggering about 3000 prompt windows by accidental error in script before server ran out of memory…)
First I set up some variables to be used in the pipeline.
And then some stage dependent variables related to the Function App to Variable Groups (not going into those details here), so in final pipeline I can have all environments with the same task but link variables to selected group in that stage. Had to hide these function
And then I added the actual PowerShell script task, with some inline code…
The main steps in the script are:
- Connect to storage
- Upload build zips
- Create SAS URIs with read-only access
- Update Function’s AppSettings with new blob links
And the actual script to do the magic is below (Download):
$conf = "$(configToRun)" ### Connect to storage $ctx = (Get-AzureRmStorageAccount -ResourceGroupName $(ZipStorageRG) -Name $(ZipStorageName)).Context ### Enter source dir Set-Location $(System.DefaultWorkingDirectory)/_func/Some.Functions.AppName ### Upload blobs $blobDbg = "$(Release.Artifacts._func.BuildNumber)/$(Release.ReleaseName)/debug.zip" $blobRel = "$(Release.Artifacts._func.BuildNumber)/$(Release.ReleaseName)/release.zip" Set-AzureStorageBlobContent -File .\debug.zip -Container $(ZipContainerName) -Blob $blobDbg -Context $ctx Set-AzureStorageBlobContent -File .\release.zip -Container $(ZipContainerName) -Blob $blobRel -Context $ctx ### Create SAS URIs $endTime = (Get-Date).AddYears(100) $uriRel = (New-AzureStorageBlobSASToken -Context $ctx -Container $(ZipContainerName) -Blob $blobRel -Permission r -Protocol HttpsOnly -ExpiryTime $endTime -FullUri).ToString() $uriDbg = (New-AzureStorageBlobSASToken -Context $ctx -Container $(ZipContainerName) -Blob $blobDbg -Permission r -Protocol HttpsOnly -ExpiryTime $endTime -FullUri).ToString() ### Update Function AppSettings ### Get current settings first $webApp = Get-AzureRmWebApp -ResourceGroupName $(varFuncRG) -Name $(varFuncAppName) $oldAppSettings = $webApp.SiteConfig.AppSettings ### Copy existing settings to new hashtable $newAppSettings = @{} ForEach ($s in $oldAppSettings) { $newAppSettings[$s.Name] = $s.Value } ### Set new zip values $newAppSettings["WEBSITE_RUN_FROM_ZIP_DBG"] = $uriDbg $newAppSettings["WEBSITE_RUN_FROM_ZIP_REL"] = $uriRel if($conf.ToLower() -eq "debug") { $newAppSettings["WEBSITE_RUN_FROM_ZIP"] = $uriDbg } else { $newAppSettings["WEBSITE_RUN_FROM_ZIP"] = $uriRel } ### Finally store values Set-AzureRmWebApp -ResourceGroupName $(varFuncRG) -Name $(varFuncAppName) -AppSettings $newAppSettings
Naturally after trying the Az CLI way, I forgot to read the old settings first and ended up deleting all existing values. This step could be also done with ARM template in a safer way.
But that’s it – after running dev stage, my Function App showed nicely the new settings and the CheckVersion function visible (and returning the same SemVer number that was generated in build phase). Having those both settings ready helps a lot if you want to change between release and debug at some point.
And the storage looked like this – the path tells it all. In final pipeline I’ll probably leave the release number out, but here it just helped with testing a lot.
For the first try I was happy with the results. Now it’s just a matter of making this even more useful with our real Azure DevOps experts. Hopefully someone gets the same inspiration as I did and starts learning. Don’t think I’ll ever use any other method to deploy my functions…
References & more information:
- Azure (DevOps) Pipelines
- Azure Functions Run-from-Package
- Azure DevOps Variable Groups
- GitVersion
- Semantic Versioning
- Teemu Tapanila @ IglooConf 2018 (YouTube)
