Create, Build, Deploy and Configure an Azure Function with Azure DevOps and Azure CLI

This post shows how to create, build, deploy and configure an Azure Function using Azure DevOps, Azure CLI and Powershell. An Azure Function is created in Azure using Azure DevOps with Azure CLI and Powershell. The Azure Function (V3) project is created and built using Visual Studio and C#. This project is deployed to the Azure infrastructure using a second Azure DevOps Pipeline. The Azure Function configuration settings is configured to use Azure Key Vault for secrets.

Create Azure Infrastruture for the Azure Function

The Azure infrastructure is created using an Azure DevOps Pipeline which connects to an Azure Key Vault and in a second task executes a Powershell script which creates the Azure infrastructure. See Use Azure Key Vault for Secrets in Azure DevOps Pipelines for details.


trigger:
- master

pool:
  vmImage: 'ubuntu-latest'

stages:
  - stage: AzureCliPowershell
    displayName: "Azure CLI with Powershell"
    jobs:
      - job: AzureCliPowershell
        steps:
        - task: AzureCLI@2
          displayName: "Create Function"
          inputs:
            azureSubscription: 'Visual Studio Enterprise(777...)'
            scriptType: 'pscore'
            scriptLocation: 'scriptPath'
            scriptPath: 'createKeyVaultExistingRG.ps1'
            

The Powershell script uses Powershell variables to create everything needed for an Azure V3 Function which runs a dotnet function. Depending on your needs, maybe some command parameters would need to be changed, for each of the commands. An identity is created for the Azure Function and this is used to allow access to the Azure Key Vault. Mark Heath has an excellent blog explaining some of the steps required to create the Azure Function.

$keyVaultName = "damienbod"
$keyVaultMySecret =  az keyvault secret show --name "MySecret" --vault-name $keyVaultName
$keyVaultMySecretId = ($keyVaultMySecret | ConvertFrom-Json).id
$location = "westeurope"
$devopsRg = "devops-damienbod-rg"
$devopsFunctionStorage = "devopsdamienbodstorage"
$devopsFunctionPlanName = "devopsdamienbodfunctionplanname"
$devopsFunctionName = "devopsdamienbodfunctionname"

Write-Host "Create resource group $devopsRg"

az group create `
	-l $location `
	-n $devopsRg

Write-Host "Check Storage account $devopsFunctionStorage"

$storageAcct = az storage account check-name --name $devopsFunctionStorage
$storageAccountNotExists = ($storageAcct | ConvertFrom-Json).nameAvailable

Write-Host "Checked Storage account $storageAcct"
Write-Host "Checked Storage account  nameAvailable = $storageAccountNotExists"

if ($storageAccountNotExists)
{   
    az storage account create `
		--name $devopsFunctionStorage `
		--resource-group $devopsRg `
		--location $location `
		--kind StorageV2 `
		--sku Standard_LRS
}
else
{
    Write-Host "$devopsFunctionStorage storage account in $Location location already exists, skipping creation"
}

Write-Host "Create function plan $devopsFunctionPlanName"

az functionapp plan create `
	--resource-group $devopsRg `
	--name $devopsFunctionPlanName `
	--location $location `
	--sku EP1

Write-Host "Create function $devopsFunctionName"

$createdFunction = az functionapp create `
	--name $devopsFunctionName `
	--functions-version 3 `
	--resource-group $devopsRg `
	--storage-account $devopsFunctionStorage `
	--runtime dotnet `
	--plan $devopsFunctionPlanName 

# Write-Host "Created function $createdFunction"
Write-Host "Create identity $devopsFunctionName"

$functionappIdentity = az functionapp identity assign `
	--name $devopsFunctionName `
	--resource-group $devopsRg

$objectId = ($functionappIdentity | ConvertFrom-Json).principalId
Write-Host "Created identity $objectId"

Write-Host "Assigned $functionappIdentity"
Write-Host "Azure az keyvault set-policy using $objectId"

az keyvault set-policy `
    --name $keyVaultName `
    --secret-permissions get list `
    --output none `
    --object-id $objectId
	

Create the Azure Function

Now that the Azure infrastructure is created, an Azure Function project can be created. A simple .NET Core 3.1 Azure V3 Function was created in Visual Studio. This can be tested and run locally, if all the required development tools are installed. See the Azure Functions getting started docs. A simple HTTP API was added which has anonymous access and uses the Environment.GetEnvironmentVariable method to display the value from the secret in the HTTP Get response.

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace FunctionApp1
{
    public static class Function1
    {
        [FunctionName("Function1")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = null)] 
            HttpRequest req, 
            ILogger log)
        {
            var mySecret = Environment.GetEnvironmentVariable("MySecret");
            return new OkObjectResult($"MySecret =  {mySecret}");
        }
    }
}

For local development, the local.settings.json file is used to add your configurations. User secrets can also be used if required. No secrets should be committed to the repository.

Build, Deploy and Configure the Azure Function

Now that the Azure Functions project is created and added to a git repository, a second Azure DevOps Pipeline is created for build and deployment. The Pipeline builds, deploys and configures the Azure Function in different steps. Azure Key Vault and Powershell is used in a third step to configure the app settings. When creating the Pipeline, choose the Azure infrastucture created in the infrastucture Pipeline.

# .NET Core Function App to Windows on Azure
# Build a .NET Core function app and deploy it to Azure as a Windows function App.
# Add steps that analyze code, save build artifacts, deploy, and more:
# https://docs.microsoft.com/en-us/azure/devops/pipelines/languages/dotnet-core

trigger:
- master

variables:
  # Azure Resource Manager connection created during pipeline creation
  azureSubscription: 'd1495f6c-c2bf-4cb7-b75d-3740e3f1a058'

  # Function app name
  functionAppName: 'devopsdamienbodfunctionname'

  # Agent VM image name
  vmImageName: 'vs2017-win2016'

  # Working Directory
  workingDirectory: '$(System.DefaultWorkingDirectory)/FunctionApp1'

stages:
- stage: Build
  displayName: Build stage

  jobs:
  - job: Build
    displayName: Build
    pool:
      vmImage: $(vmImageName)

    steps:
    - task: DotNetCoreCLI@2
      displayName: Build
      inputs:
        command: 'build'
        projects: |
          $(workingDirectory)/*.csproj
        arguments: --output $(System.DefaultWorkingDirectory)/publish_output --configuration Release

    - task: ArchiveFiles@2
      displayName: 'Archive files'
      inputs:
        rootFolderOrFile: '$(System.DefaultWorkingDirectory)/publish_output'
        includeRootFolder: false
        archiveType: zip
        archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
        replaceExistingArchive: true

    - publish: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
      artifact: drop

- stage: Deploy
  displayName: Deploy stage
  dependsOn: Build
  condition: succeeded()

  jobs:
  - deployment: Deploy
    displayName: Deploy
    environment: 'development'
    pool:
      vmImage: $(vmImageName)

    strategy:
      runOnce:
        deploy:

          steps:
          - task: AzureFunctionApp@1
            displayName: 'Azure functions app deploy'
            inputs:
              azureSubscription: '$(azureSubscription)'
              appType: functionApp
              appName: $(functionAppName)
              package: '$(Pipeline.Workspace)/drop/$(Build.BuildId).zip'

- stage: Configure
  displayName: "Configure App settings"
  jobs:
  - job: AzureCliPowershell
    steps:
    - task: AzureCLI@2
      displayName: "Add settings"
      inputs:
        azureSubscription: 'Visual Studio Enterprise(777...)'
        scriptType: 'pscore'
        scriptLocation: 'scriptPath'
        scriptPath: 'AddAppSettings.ps1'

The Powershell script uses the Azure Key Vault and the Azure CLI to set the configuration values. Each configuration, app setting would be added in this script. If you update and create new values in the Key Vault, the configuration would need to be deployed again to use the new values. This is because the Key Vault setting has an exact reference to a value in the secret. 2 ^^ chars are also required in Key Vault Powershell configuration, which seems strange.

$keyVaultName = "damienbod"
$keyVaultMySecret =  az keyvault secret show --name "MySecret" --vault-name $keyVaultName
# $keyVaultMySecretValue = ($keyVaultMySecret | ConvertFrom-Json).value
$keyVaultMySecretId = ($keyVaultMySecret | ConvertFrom-Json).id

# Write-Host $keyVaultMySecret
# Write-Host "Value: $keyVaultMySecretValue"
# Write-Host "Id: $keyVaultMySecretId"

$devopsRg = "devops-damienbod-rg"
$devopsFunctionName = "devopsdamienbodfunctionname"

Write-Host "Azure Function add secrets from the key vault"

az functionapp config appsettings set `
	--resource-group $devopsRg `
	--name $devopsFunctionName `
	--output none `
	--settings MySecret="@Microsoft.KeyVault(SecretUri=$keyVaultMySecretId)^^" 

When the Pipeline is run, and deployed, the API can be opened in the browser, and the secret will be displayed as a demo.

Links:

https://dev.azure.com/

https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/deploy/azure-key-vault?view=azure-devops

https://docs.microsoft.com/en-us/cli/azure/keyvault/secret?view=azure-cli-latest#az-keyvault-secret-show

https://docs.microsoft.com/en-us/azure/key-vault/quick-create-cli

https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/package/nuget?view=azure-devops

https://docs.microsoft.com/en-us/azure/devops/pipelines/ecosystems/dotnet-core?view=azure-devops

https://zimmergren.net/using-azure-key-vault-secrets-from-azure-devops-pipeline/

https://markheath.net/post/managed-identity-key-vault-azure-functions

https://markheath.net/post/deploying-azure-functions-with-azure-cli

2 comments

  1. […] Create, Build, Deploy and Configure an Azure Function with Azure DevOps and Azure CLI (Damien Bowden) […]

  2. […] Create, Build, Deploy and Configure an Azure Function with Azure DevOps and Azure CLI – Damien Bowden […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: