Deploying .Net Code to a Function App using YAML files in Azure DevOps

Bob Code
13 min readOct 18, 2023

--

Introduction

The DotNet Pipeline is part of the Continuous Delivery Process during which code is built, tested, and deployed to one or more test and production environments. Deploying and testing in multiple environments increases quality.

Deploying a .NET Function App using YAML files in Azure DevOps is a streamlined and efficient way to automate the deployment process of your serverless functions.

Azure DevOps, in combination with YAML configuration files, empowers you to define the deployment steps, dependencies, and configurations as code, making it easier to maintain and reproduce deployments. In this process, you can manage the entire lifecycle of your .NET Function App, from building and packaging your functions to deploying them seamlessly to the Azure cloud. This approach provides greater control, reliability, and scalability for your serverless applications, ensuring they’re ready to serve your users and customers without manual intervention.

Notes

This blog focuses only on the .Net code deployment, if you would like to create the function app itself, please refer to my other blog:

Terraform in Azure DevOps Pipeline

Build vs Release Pipeline

The build pipeline is meant to produce the binaries (such as executing commands like ‘dotnet publish’ or ‘ng build — prod’) as an artifact, stored in a file that the release pipeline will then download and use and deploy.

The rationale behind maintaining separate build and release pipelines is to ensure that a specific version of software is built only once, and then these identical binaries could be used across various target environments, such as development, testing, and production.

The release pipeline on the other hand will deploy the artifact that has been created by the build pipeline to the given function app.

#1 Get the correct dotnet version of your project

    steps:
- task: UseDotNet@2
displayName: 'Use DotNet 6'
inputs:
packageType: 'sdk'
version: '6.0.415'
installationPath: $(Agent.ToolsDirectory)/dotnet

First we need to install the dotnet version onto which the project run, you can run dotnet — info to find out which version you are using.

#2 Build the Project

    - task: DotNetCoreCLI@2
displayName: 'Dotnet build Release'
inputs:
command: 'build'
projects: '**/DionysosRouter.csproj'
arguments: '--configuration Release'

Run the build command to compile the .NET project.

The dotnet build command builds the project and its dependencies into a set of binaries. The binaries include the project’s code in Intermediate Language (IL) files with a .dll extension.

[Build .net core 6 projects with Azure Devops using yml files](https://briancaos.wordpress.com/2022/09/22/build-net-core-6-projects-with-azure-devops-using-yml-files/)

[dotnet build](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-build)

What does Build do?

1/ The compiler launches, it first restores all required dependencies through the Nuget Package Manager.

2/ Then the command-line tool compiles the project and stores its output in a folder called “bin” (Debug or Release)

3/ At this point the C# Code has been compiled into an executable file containing the Common Intermediate Language (CIL) Code — Assembly Level

4/ For the OS to run the CIL it needs to be converted into native code which is done by the Common Language Runtime

[How is C# Compiled?](https://freecontent.manning.com/how-is-c-compiled/)

#3 Restore dependencies

- task: DotNetCoreCLI@2
displayName: Restore dotnet tools
inputs:
command: custom
custom: tool
arguments: restore

Ensures that your project’s dependencies are correctly resolved and downloaded

With NuGet Package Restore you can install all your project’s dependency without having to store them in source control. This allows for a cleaner development environment and a smaller repository size. You can restore your NuGet packages using the NuGet restore task, the NuGet CLI, or the .NET Core CLI

[Restore NuGet packages with Azure Pipelines](https://learn.microsoft.com/en-us/azure/devops/pipelines/packages/nuget-restore?view=azure-devops&tabs=classic)

What dependencies does the command restore?

Dependency Resolution: .NET projects typically rely on various external libraries and packages (NuGet packages in the case of .NET). These dependencies are listed in your project file (e.g., .csproj file) as references. When you run dotnet restore, it scans the project file, fetches the necessary dependencies, and makes them available for use in your project. This ensures that the correct versions of these packages are available.

The ‘restore’ command is used to restore NuGet packages referenced by the specified project(s).

It ensures that the required packages are downloaded and restored to the project’s packages directory.

[DotNetCoreCLI@2 — .NET Core v2 task](https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/dotnet-core-cli-v2?view=azure-pipelines)

dotnet restore — Restores the dependencies and tools of a project.

[dotnet restore](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-restore)

The dotnet build command builds the project and its dependencies into a set of binaries. The binaries include the project’s code in Intermediate Language (IL) files with a .dll extension.

The C# compilation process has three states (C#, Intermediate Language, and native code) and two stages: going from C# to Common Intermediate Language and going from Intermediate Language to native code.

#4 Publish

- task: DotNetCoreCLI@2
displayName: 'Dotnet publish'
inputs:
command: publish
publishWebProjects: false # If this input is set to true, the projects property value is skipped, and the task tries to find the web projects (web.config file or a wwwroot folder) in the repository and run the publish command on them
arguments: '--configuration Release --output $(Build.ArtifactStagingDirectory)' ## --configuration Release or Debug, output specifies the output directory for the published files to be
zipAfterPublish: true # If this input is set to true, folders created by the publish command will be zipped and deleted.
projects: '**/DionysosRouter.csproj' # specifies the project file to be published. It uses a wildcard pattern to find any project file with the name "DionysosRouter.csproj" in any subdirectory. This allows it to work with multiple project files if they exist.

The “publish” command is used to prepare the project for deployment by generating publishable output files.

sets the build configuration to “Release” and specifies the output directory as the “$(Build.ArtifactStagingDirectory).” This means the publish output will be placed in a location that Azure DevOps uses for staging artifacts.

zipAfterPublish: true: If this input is set to “true,” it will compress the folders created by the publish command into a zip file. This can be useful for packaging the published artifacts into a single archive.

[dotnet publish](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-publish)

#5 Publish artifacts

- task: PublishBuildArtifacts@1
displayName: "Publish Artifacts"
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: 'DionysosRouter'

So that we can use them at the release stage

dotnet publish — Publishes the application and its dependencies to a folder for deployment to a hosting system.

[dotnet publish](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-publish)

DotNetCoreCLI@2 > publish

In a pipeline we use the DotNetCoreCLI@2 — .NET Core v2 task which can Build, test, package, or publish a dotnet application, or run a custom dotnet command.

What are Artifacts?

[Artifacts in Azure Pipelines](https://learn.microsoft.com/en-us/azure/devops/pipelines/artifacts/build-artifacts?view=azure-devops&tabs=yaml)

Errors

/DotNetPipeline/Build.yml (Line: 1, Col: 1): Unexpected value ‘pool’

/DotNetPipeline/Build.yml (Line: 4, Col: 1): Unexpected value ‘steps’

Solution:

The pool and steps sections should be inside a job definition.

Error:

Stage deploy_dotnet must contain at least one job with no dependencies.

Solution:

Remove the dependsOn: Build

Error:

MSBUILD : error MSB1003: Specify a project or solution file. The current working directory does not contain a project or solution file.

MSBuild version 17.7.3+8ec440e68 for .NET

MSBUILD : error MSB1003: Specify a project or solution file. The current working directory does not contain a project or solution file.

Solution: adjust the directory path

Error:

Info: .NET Core SDK/runtime 2.2 and 3.0 are now End of Life(EOL) and have been removed from all hosted agents. If you’re using these SDK/runtimes on hosted agents,

kindly upgrade to newer versions which are not EOL, or else use UseDotNet task to install the required version.

Solution: specify the right dotnet version in the first task

Release Pipeline Steps

#1 Download the artifact from the build pipeline

    - task: DownloadBuildArtifacts@0
displayName: 'Download Build Artifacts'
inputs:
buildType: 'current' # current,' meaning it will download artifacts from the current build.
downloadType: 'single' # In this case, it's set to 'single,' which means it will download a single set of artifacts
artifactName: 'DionysosRouter'
downloadPath: '$(System.ArtifactsDirectory)' # This parameter indicates the directory where the downloaded artifacts will be stored

In the last stage we uploaded the arfifacts as a zip in a directory in ADO.

To use them we first need to download them.

If you are not sure about the exact location, you can find it in ADO in your previous stage.

[DownloadBuildArtifacts@0 — Download build artifacts v0 task](https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/download-build-artifacts-v0?view=azure-pipelines)

#2 Deploy your Function App


- task: AzureFunctionApp@1
inputs:
azureSubscription: $(ServiceConnectionName) # actually the service connection name
appType: functionApp # for windows function app
appName: $(routingAppName)${{ parameters.env }}
package: $(System.ArtifactsDirectory)/DionysosRouter/Dionysos-Router.zip
deploymentMethod: 'zipDeploy' # Zip deployment involves packaging the application code and dependencies into a ZIP file and deploying it to the Azure Functions app.

The function app must have been previously created (see the terraform deployment blog > Terraform in Azure DevOps Pipeline)

This final stage will take the dotnet project file that has been built and saved in a zip file as artifact.

We now simply have to mention it in our function app task and the code will run on the function app!

How to find the package file path of the artifact in Azure DevOps?

First add $(System.ArtifactsDirectory) or $(Build.ArtifactStagingDirectory)

Then add the name of the folder you created

Finally input the name of your zip file

Errors

Error: cannot find any artifacts after DownloadBuildArtifacts

Micosoft recently updated the DownloadBuildArtifacts task

It so happens that in the UI, it would seem that there is a folder with the name of the artifact.

Actually, there isn’t, so all your files are directly available from the System.ArtifactsDirectory (or wherever you downloaded the files)

    
############################## Artifacts ##################################
steps:
- task: DownloadBuildArtifacts@1
displayName: 'Download Build Artifacts'
inputs:
buildType: 'current'
downloadType: 'single'
artifactName: ${{ parameters.solutionName }}_artifacts
downloadPath: '$(System.ArtifactsDirectory)'

# Displays the downloaded files
- powershell: |
$downloadedFiles = Get-ChildItem "$(System.ArtifactsDirectory)" -Recurse
Write-Output "Downloaded Files:"
foreach ($file in $downloadedFiles) {
Write-Output $file.FullName}
displayName: 'Display Downloaded Artifact Names'

############################## Release the API beast ##################################
- task: AzureWebApp@1
displayName: 'Release the API beast'
inputs:
azureSubscription: $(serviceConnection)
appName: 'plutus-pnp-webapp-d'
package: '$(System.ArtifactsDirectory)\Plutus.ProductPricing.API.zip'
deploymentMethod: 'zipDeploy'

Read more to see other workarounds

Error: Azure functions don’t appear/ cannot be run in the Azure portal

Try running with ZipDeploy, this might not work with .Net 8 versions at time of writing (early 2024)

Solution: use run from package

Code

For the explanations and other options, scroll after the code

#1 In your terraform code, set the appsettings for your func

resource "azurerm_windows_function_app" "pnp-function" {
name = "namefunc"
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location

storage_account_name = azurerm_storage_account.pnp-sa-functions.name
storage_account_access_key = azurerm_storage_account.pnp-sa-functions.primary_access_key
service_plan_id = azurerm_service_plan.pnp-asp-functions.id

app_settings = {
WEBSITE_RUN_FROM_PACKAGE = 1,
WEBSITE_USE_PLACEHOLDER_DOTNETISOLATED = 1,
WEBSITE_ENABLE_SYNC_UPDATE_SITE = true,
FUNCTIONS_WORKER_RUNTIME = "dotnet-isolated"
}
  • WEBSITE_RUN_FROM_PACKAGE: app will run from package instead of zip
  • WEBSITE_USE_PLACEHOLDER_DOTNETISOLATED = .Net 8+ function apps must run in dotnet isolated

#2 In your publish pipeline, Publish your csproj, keep zipAfterPublish as true

 # Publish build results Func
- task: DotNetCoreCLI@2
displayName: 'Generate Build Artifacts Func'
inputs:
command: publish
publishWebProjects: False
projects: '**\nameofyourproj.csproj'
arguments: '--configuration ${{ parameters.buildConfiguration }} --output $(Build.ArtifactStagingDirectory)'
zipAfterPublish: true

#3 In your release pipeline

    - task: AzureFunctionApp@2
displayName: 'Release Func'
inputs:
connectedServiceNameARM: $(ServiceConnectionname)
appType: functionApp # for windows function app
appName: 'nameofyourfuncapp'
package: '$(System.ArtifactsDirectory)\nameofyourproj.zip'
deploymentMethod: 'runFromPackage'

deploymentMethod: ‘runFromPackage’ = instead of running Zip, you will directly run from package

Explanations

The /wwroot folder of your app must look like this

 | - bin
| - MyFunction
| - host.json

You can find your folder structure in the portal by:

  • Go to your Function app
  • Go to App Service Editor (Preview)
  • On the top, open the Kudu Console
  • Navigate to site > /wwwroot

A bin folder contains packages and other library files that the function app requires. Specific folder structures required by the function app depend on language: C# compiled (. csproj)

Is your bin file missing? Then you have two options:

  • Manually add the bin folder to your wwwroot in your pipeline
  • Run the function directly from the deployment package file (see code example above)

Also, if you’re runninga .net 8+ function app, make sure that you have the isolated worker model enabled (https://azure.microsoft.com/en-us/updates/ga-azure-functions-supports-net-8-in-the-isolated-worker-model/)

Running from Deployment Package File

You can also choose to run your functions directly from the deployment package file. This method skips the deployment step of copying files from the package to the wwwroot directory of your function app. Instead, the package file is mounted by the Functions runtime, and the contents of the wwwroot directory become read-only.

Zip deployment integrates with this feature, which you can enable by setting the function app setting WEBSITE_RUN_FROM_PACKAGE to a value of 1. For more information, see Run your functions from a deployment package file.

https://learn.microsoft.com/en-us/azure/azure-functions/run-functions-from-deployment-package

When you set WEBSITE_RUN_FROM_PACKAGE = 1, the .zip file is mounted directly, and its contents are not extracted to the wwwroot directory. As a result, wwwroot becomes read-only. But, whether it’s “empty” is misleading; it may have other system files or old content, but it will be accurate to say that it won’t have the active content from the current .zip package. The active content is directly read from the mounted .zip package. (https://stackoverflow.com/questions/76867490/azure-function-clarifications-about-zip-deployment-and-run-from-package-file)

Manually add proj to bin

  • Done using the archive task and specifying the wwwroot folder

Read about ArchiveFiles task

Understand how Zip deployment works

Discussion about how to solve error including archive steps

Fix about the bin folder structure

Look at the Azure pipeline steps for build and publish

Documentation on publish path bin\Release\net8.0\publish\

##[error]TypeError: Cannot read properties of undefined (reading ‘PreDeploymentStep’)

Means that some of the app-settings that are in your app-service cannot be read

Solution:

Either remove some app-settings that actually are not yet supported or find the latest supported version

Error: My ServicePrincipal in Azure DevOps cannot reach the FunctionApp that sits behind a vnet/subnet/private endpoint

Solution:

  • #1 Add network rule to allow SP to deploy function app
## Add release agent to access rules of function
- task: AzureCLI@2
inputs:
azureSubscription: ${{ parameters.serviceConnection }}
connectedServiceNameARM: ${{ parameters.serviceConnection }}
scriptType: 'pscore'
scriptLocation: 'inlineScript'
addSpnToEnvironment: true # boolean. Access service principal details in script. Default: false.
inlineScript: |
$ip = (Invoke-WebRequest -uri "http://ifconfig.me/ip").Content
Write-Host Current Agent IP is $ip
az functionapp config access-restriction add -g '${{ parameters.resourceGroupName }}${{ parameters.environment }}' -n 'plutus-store-func-livediscounts-${{ parameters.environment }}' --rule-name devopsagent --action Allow --ip-address $ip --priority 200
az functionapp config appsettings set --name 'plutus-store-func-livediscounts-${{ parameters.environment }}' --resource-group '${{ parameters.resourceGroupName }}${{ parameters.environment }}' --settings WEBSITE_RUN_FROM_PACKAGE=1
  • #2 Deploy function app (see previous sections)
  • #3 Remove network rule
## Remove release agent from access rules
- task: AzureCLI@2
inputs:
azureSubscription: ${{ parameters.serviceConnection }}
connectedServiceNameARM: ${{ parameters.serviceConnection }}
scriptType: 'pscore'
scriptLocation: 'inlineScript'
addSpnToEnvironment: true # boolean. Access service principal details in script. Default: false.
inlineScript: |
$ip = (Invoke-WebRequest -uri "http://ifconfig.me/ip").Content
Write-Host Current Agent IP is $ip
az functionapp config access-restriction remove -g '${{ parameters.resourceGroupName }}${{ parameters.environment }}' -n 'plutus-store-func-livediscounts-${{ parameters.environment }}' --rule-name devopsagent

Error: C:\hostedtoolcache\windows\dotnet\sdk\8.0.204\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.ConflictResolution.targets(112,5): error NETSDK1152: Found multiple publish output files with the same relative path:

Solution, append \NameOfYourFunc to arguments output

This way the files will be saved in a bespoke folder

 # Publish build results Func LiveDiscounts
- task: DotNetCoreCLI@2
displayName: 'Generate Build Artifacts StoreServe LiveDiscounts Func'
inputs:
command: publish
publishWebProjects: False
projects: '**\Plutus.StoreServices.LiveDiscounts.Http.csproj'
arguments: '--configuration ${{ parameters.buildConfiguration }} --output $(Build.ArtifactStagingDirectory)\LiveDiscounts'
zipAfterPublish: true

And change the release package

  - task: AzureFunctionApp@1
displayName: 'Release Live discounts Func'
inputs:
azureSubscription: ${{ parameters.serviceConnection }}
#connectedServiceNameARM: ${{ parameters.serviceConnection }}
appType: functionApp # for windows function app
appName: 'plutus-store-func-livediscounts-${{ parameters.environment }}'
package: '$(System.ArtifactsDirectory)\LiveDiscounts\Plutus.StoreServices.LiveDiscounts.Http.zip'
deploymentMethod: 'runFromPackage'

Finally add to your csproj file

<PropertyGroup>
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
</PropertyGroup>

Understanding the Issues

  • When you publish multiple Azure Functions projects to the same directory, and these projects have files with the same name (like host.json, local.settings.json, etc.), the .NET SDK cannot resolve which file to keep and which to overwrite, resulting in the NETSDK1152 error.
  • In your specific scenario, both Plutus.StoreServices.LiveDiscounts.Http and Plutus.StoreServices.BulkPrices projects have their own host.json files. When you try to publish these projects to the same output directory, the conflict arises.
  • There has been a breaking change see below

--

--

Bob Code

All things related to Memes, .Net/C#, Azure, DevOps and Microservices