Create, Build, Deploy and Configure an Azure App Service with Azure DevOps and Azure CLI

This post shows how to create, build, deploy and configure an Azure App Service using Azure DevOps, Azure CLI and Powershell. An Azure Service is created in Azure using Azure DevOps with Azure CLI and Powershell. The Azure App Service is created and built using ASP.NET Core and Visual Studio. This solution is deployed to the Azure infrastructure using a second Azure DevOps Pipeline. The Azure App Service configuration settings is configured to use Azure Key Vault for secrets and the settings directly.

Create Azure Infrastruture for the Azure App Service

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.

pool:
  vmImage: 'ubuntu-latest'

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

The Powershell script creates the Azure Infrastruture for the Azure App Service. Once to Azure App Service is created, an identity is created for this, and the identity is used then to allow the App Service to use secrets from the Key Vault.

$keyVaultName = "damienbod"
$keyVaultMySecret =  az keyvault secret show --name "MySecret" --vault-name $keyVaultName
$keyVaultMySecretId = ($keyVaultMySecret | ConvertFrom-Json).id
$location = "westeurope"
$resourceGroup = "devops-damienbod-rg"
$appServicePlan = "damienbodappserviceplan"
$appServiceName = "damienbodappservice"

Write-Host "Create resource group $resourceGroup"

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

Write-Host "Create App Service Plan $appServicePlan"
	
az appservice plan create `
	--resource-group $resourceGroup `
	--name $appServicePlan `
	--location $location `
	--sku S1 `
	--number-of-workers 2

Write-Host "Create App Service $appServiceName"

az webapp create `
	--name $appServiceName `
	--resource-group $resourceGroup `
	--plan $appServicePlan

Write-Host "Create App Service Identity $appServiceName"

$appServiceIdentity = az webapp identity assign `
	--name $appServiceName `
	--resource-group $resourceGroup

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

Write-Host "Assigned $appServiceIdentity"

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 ASP.NET Core Web API

An ASP.NET Core API project is created which will be deployed to the Azure App Service. This demo is really simple, no database and just uses a value form the Key Vault. The Microsoft.Extensions.Configuration.AzureKeyVault Nuget package is used to access the Azure Key Vault.

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <UserSecretsId>fef9bb2d-7bfb-402f-bdb7-d8bd1f8194f6</UserSecretsId>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference 
      Include="Microsoft.Extensions.Configuration.AzureKeyVault" 
      Version="3.1.3" />
  </ItemGroup>
</Project>

In the program.cs file, the application initialization is programmed, so that the local development requires no Key Vault, but only when the application is deployed. You could change this to always use a Key Vault if required.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.Services.AppAuthentication;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace AzureAppServiceApi
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
            .ConfigureAppConfiguration((context, config) =>
            {
                var builder = config.Build();
                var keyVaultEndpoint = builder["AzureKeyVaultEndpoint"];
                if (!string.IsNullOrEmpty(keyVaultEndpoint))
                {
                    var azureServiceTokenProvider = new AzureServiceTokenProvider();
                    var keyVaultClient = new KeyVaultClient(
                        new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));

                    config.AddAzureKeyVault(keyVaultEndpoint);
                }
                else
                {
                    IHostEnvironment env = context.HostingEnvironment;

                    config.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                             .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true)
                             .AddUserSecrets("fef9bb2d-7bfb-402f-bdb7-d8bd1f8194f6")
                             .AddEnvironmentVariables();
                }
            })
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            }
        );
    }
}

The Azure KeyVaultEndpointvalue is added to the local configuration for development. If this has no value, the local app.settings.json file and user secrets are used. You do not want secrets in the source code. You should use User secrets for local development and configure this in the program.cs file. Then local development uses user secrets, a deployed version uses Key Vault.

{
  "AzureKeyVaultEndpoint": "",
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

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 App Service

Now that the Azure App Service 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 App Service ASP.NET Core project 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.

# ASP.NET Core (.NET Framework)
# Build and test ASP.NET Core projects targeting the full .NET Framework.
# Add steps that publish symbols, save build artifacts, and more:
# https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core

trigger:
- master

pool:
  vmImage: 'windows-latest'

variables:
  solution: '**/*.sln'
  buildPlatform: 'Any CPU'
  buildConfiguration: 'Release'

stages:
- stage: Build
  displayName: Build stage

  jobs:
  - job: Build
    displayName: Build
    
    steps:
    - task: NuGetToolInstaller@1

    - task: NuGetCommand@2
      inputs:
        restoreSolution: '$(solution)'

    - task: DotNetCoreCLI@2
      displayName: "Publish"
      inputs:
        command: publish
        projects: "**/*.csproj"
        arguments: "--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory)/publish_output"
        zipAfterPublish: True

    - task: VSTest@2
      inputs:
        platform: '$(buildPlatform)'
        configuration: '$(buildConfiguration)'

    - task: AzureRmWebAppDeployment@4
      displayName: "Deploy to Azure"
      inputs:
        ConnectionType: 'AzureRM'
        azureSubscription: 'Visual Studio Enterprise(777...)'
        appType: 'webApp'
        WebAppName: 'damienbodappservice'
        Package: "$(Build.ArtifactStagingDirectory)/publish_output/*.zip"
        UseWebDeploy: true
        RemoveAdditionalFilesFlag: true

- stage: Configure
  displayName: "Configure App settings"
  jobs:
  - job: ConfigureAppSettings
    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. Values from the Key Vault do not need to be added here, as the Key Vault Nuget package takes care of this. The AzureKeyVaultEndpoint is added so that Key Vault is active.

$keyVaultName = "damienbod"
$keyVaultMySecret =  az keyvault secret show --name "MySecret" --vault-name $keyVaultName
$keyVaultMySecretId = ($keyVaultMySecret | ConvertFrom-Json).id
$location = "westeurope"
$resourceGroup = "devops-damienbod-rg"
$appServicePlan = "damienbodappserviceplan"
$appServiceName = "damienbodappservice"

Write-Host "Azure App Service add secrets from the key vault"

az webapp config appsettings set `
    --resource-group $resourceGroup `
    --name $appServiceName `
    --output none `
    --settings `
	AzureKeyVaultEndpoint="https://damienbod.vault.azure.net" 

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 App Service with Azure DevOps and Azure CLI (Damien Bowden) […]

  2. Francis Ducharme · · Reply

    Not entirely sure why program.cs needs to be changed. Aren’t we just asking Azure Devops to build the project, take the output and put in the created App Service ? Why does the .Net project itself have to know about devops stuff ? Should it be agnostic to that ?

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: