Use Azure Pipelines to deploy Function App

Andrey Kukharenko
10 min readOct 22, 2023

--

The story about using Azure Pipelines (YAML) to build and deploy to Azure Function App.

Assumptions

  1. Be familiar with Git.
  2. Be familiar with Azure Function App development.
  3. Be familiar with Azure cloud.
  4. Have configured Azure DevOps with repository and pipelines activated for the project.
  5. Have active account for Azure with subscription.

Parts of the cycle:

  1. Use Azure Pipelines to deploy .NET application to Azure App Service
  2. Use Azure Pipelines to deploy Angular application to Azure App Service
  3. Use Azure Pipelines to deploy Function App

Introduction

Sometimes need to create the special apps like Azure Function App to do some background jobs or as part of the serverless architecture. This app need to be deployed (host) on Azure App Service — but as Function App (because all the apps on azure works as App Service). There are also service Azure DevOps that provided complete functionality to develop the application and manage the work with CI/CD process.

So basically the flow as we can see here:

  1. Azure Repos for Git repo.
  2. Azure Pipelines for build and deployment.
  3. Azure cloud to host the application.

The article provide many details about configuring Azure DevOps, Azure and etc. So we skip some steps here to do not duplicate them.

Here we will discuss the next things:

  1. Azure DevOps — in general.
  2. Azure Pipelines — as one of the functionality that used for CI/CD to use to deploy Function App and invoke it.

Let’s take a look in more details.

1 Azure DevOps

This is the main service in the development process, will be used for track the work, code, CI/CD, artifacts, tests. Assumption here to have the repository with the code (at least basic started web app) for sample application (Function App).

For our purposes and work we need:

  1. Azure Repos — for Git repositories with the code.
  2. Azure Pipelines — for CI/CD stuff.

What’s need to be configured:

  1. Service Connections for Azure — to able to deploy to Azure with special credentials.
  2. Git Repository — to store the code.

More details provided in the Part 1 — how to configure service connection.

2 Microsoft Azure

To start with the new Azure Function App we need to create this resource on Azur portal. It can be created manually or by using some automated scrips.

Usually to create the new new function need the next resources:

  1. Function App — actual app to use to host the functions.
  2. Storage account — to store logs and locks for function runs.
  3. Application Insights — monitoring and logs.
  4. App service Plan — to able to run on proper machine, aggregate pricing and select availability level.
New function app and app service plan

After all resources created we can move to the next step — create and configure pipeline.

3 Azure Pipelines

All the steps are the same as we discussed in the Part 1:

  1. Have a Git repository in Azure Repos.
  2. Create environments for deployment — prepare the configuration for environment definitions to deploy the application, configure them (like approvals, etc.).
  3. Add to the library new (or update existed) variable groups — to use variables in the pipeline and can be edit without code changes.
  4. Create the pipeline — prepare files in the repo and select them for new pipeline.
  5. Configure security — access to the library, pipeline permissions, service connection for environment to able to consume them.

Here will be provided needed steps and actions specific for function. And only one here is template code with YAML files for pipeline.

3.1 Create the pipeline

Sample pipeline for .NET application as Function App contains few files in the repo. All the files placed in the build folder in the repository root with solution and some other folders and items.

File structure with templates uses in pipeline

Content of all the files provided bellow. Most of the values and settings can be used as it and some of them simply to change.

File azure-pipelines.yml:

# Parameters for pipeline
parameters:
- name: environment
displayName: Environment
type: string
default: BuildOnly
values:
- BuildOnly
- Development
- name: projectName
displayName: ProjectName
type: string
default: DevTestPlayground
- name: skipLint
displayName: SkipLint
type: boolean
default: false

# automatically triggers
trigger:
- dev

variables:
- group: Deploy-Environment-Shared
- ${{ if eq(variables['Build.Reason'], 'PullRequest') }}:
- name: why
value: pr
- ${{ elseif eq(variables['Build.Reason'], 'Manual' ) }}:
- name: why
value: manual
- ${{ elseif eq(variables['Build.Reason'], 'IndividualCI' ) }}:
- name: why
value: indivci
- ${{ else }}:
- name: why
value: other
- ${{ if eq(parameters.Environment, 'Development') }}:
- name: envShortName
value: dev
- ${{ else }}:
- name: envShortName
value: none

# custom name for the build
name: $(Date:yyyyMMdd)$(Rev:.r)_$(SourceBranchName)_$(why)_$(envShortName)

# stages - list of the jobs to do step by step
stages:
# Build and Lint (and other stuff) stage
- template: pipelines/build.yml
parameters:
Environment: ${{ parameters.Environment }}
ProjectName: ${{ parameters.ProjectName }}
SkipLint: ${{ parameters.SkipLint }}

# Deploy application to single specific environment
- ${{ if not(eq(parameters.Environment,'BuildOnly')) }}:
- template: pipelines/release.yml
parameters:
Environment: ${{ parameters.Environment }}
ProjectName: ${{ parameters.ProjectName }}

File pipelines/build.yml:

stages:
- stage: Build
displayName: Build${{ parameters.ProjectName }}

# Use multiple jobs, so the linter can work in parallel to the build.
# This also allows to run the Linter on Linux whereas you build can run on Windows or Mac.
jobs:
# Lint the code with Super-Linter from GitHub
- job: lint
displayName: Lint code base
condition: and(eq(${{ parameters.SkipLint }}, false), ne(variables['Build.Reason'], 'IndividualCI'))
pool:
vmImage: 'ubuntu-latest'
steps:
- script: docker pull github/super-linter:latest
displayName: Pull GitHub Super-Linter image
- script: >-
docker run \
-e RUN_LOCAL=true \
-e VALIDATE_JSCPD=false \
-e VALIDATE_MARKDOWN=false \
-e VALIDATE_EDITORCONFIG=false \
-v $(System.DefaultWorkingDirectory):/tmp/lint \
github/super-linter
displayName: 'Run GitHub Super-Linter'
continueOnError: true

# Do main build action
- job: build
displayName: Build and Test, create artifacts
pool:
vmImage: 'windows-latest'
variables:
BuildConfiguration: 'Release'
steps:
- template: build-dotnet.yml
parameters:
ProjectServiceName: ${{ parameters.ProjectName }}
RestoreBuildProjects: '**/*.csproj'
TestProjects: 'tests/**/*.csproj'
DotnetVersion: '$(DotnetVersion)'
SimpleFunctionApp: '**/DevTestPlayground.SimpleFunction.csproj'

File pipelines/build-dotnet.yml:

steps:
- checkout: self
clean: true

# Install anc cache .NET SDK
- task: UseDotNet@2
displayName: 'Install Dotnet Core cli'
inputs:
version: ${{ parameters.DotnetVersion }}

# Restore NuGet packages
- task: DotNetCoreCLI@2
displayName: 'Dotnet restore'
inputs:
command: 'restore'
projects: |
${{ parameters.RestoreBuildProjects }}
${{ parameters.TestProjects }}
feedsToUse: 'config' # use it in case if needed to have custom feeds!
nugetConfigPath: NuGet.config # use it in case if needed to have custom feeds!

# Build the project
- task: DotNetCoreCLI@2
displayName: 'Dotnet build'
inputs:
command: 'build'
projects: |
${{ parameters.SimpleFunctionApp }}
arguments: --configuration $(BuildConfiguration) --no-restore

# Run the tests
# - task: DotNetCoreCLI@2
# condition: ne('${{ parameters.TestProjects }}', '')
# displayName: 'Dotnet Test'
# inputs:
# command: 'test'
# projects: ${{ parameters.TestProjects }}
# arguments: '--configuration $(BuildConfiguration) --no-restore --collect "Code coverage"'

# Publish web application (to use later for deployment)
- task: DotNetCoreCLI@2
displayName: Publish
inputs:
command: 'publish'
publishWebProjects: false
zipAfterPublish: true
projects: |
${{ parameters.SimpleFunctionApp }}
arguments: '--configuration $(BuildConfiguration) --output $(Build.ArtifactStagingDirectory) --no-restore'

# Publish artifacts after job finished within pipeline
- task: PublishPipelineArtifact@1
displayName: Publish pipeline artifacts
condition: ne(variables['Build.Reason'], 'PullRequest')
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)'
artifactName: '${{ parameters.ProjectServiceName }}'
publishLocation: 'pipeline'

File pipelines/release.yml:

stages:
# Main deployment stage per one selected environment
- stage: Deploy
displayName: Deploy${{ parameters.ProjectName }}
variables:
- group: Deploy-Environment-${{ parameters.Environment }}
jobs:
# Do actual function app deployment to Azure - 1) Azure Functions
- deployment: DeployFunction
displayName: Deploy Azure Function App to Azure
pool:
vmImage: 'windows-latest'
environment: ${{ parameters.Environment }}
strategy:
runOnce:
deploy:
steps:
# Deploy Azure function (SimpleFunctionApp)
- template: deploy-function.yml
parameters:
Environment: ${{ parameters.Environment }}
ProjectName: ${{ parameters.ProjectName }}
FunctionPackageName: 'DevTestPlayground.SimpleFunction'
AppServiceName: '$(AppServiceNameSimpleFunction)'

# Run the separate job to process Function logic before web app deploy
- job: DeployAgentlessJob
displayName: Run function app
dependsOn: DeployFunction
pool: server
steps:
# Invoke an Azure Function.
- task: AzureFunction@1
displayName: 'Invoke Azure Function to run the function'
inputs:
function: '$(SimpleFunctionUrl)'
key: '$(SimpleFunctionKey)'
method: 'POST'
waitForCompletion: 'false'
body: '{ "name": "AzurePipeline" }'

File pipelines/deploy-function.yml:

steps:
- checkout: self
clean: true

# Download pipelines artifacts
- task: DownloadPipelineArtifact@2
inputs:
artifactName: '${{ parameters.ProjectName }}'
buildType: 'current'
targetPath: '$(Build.ArtifactStagingDirectory)\${{ parameters.ProjectName }}'

# Deploy Azure Functions
- task: AzureFunctionApp@2
displayName: 'Azure Function App Deploy'
inputs:
azureSubscription: '$(AzureServiceConnection)'
appType: 'functionApp'
appName: '${{ parameters.AppServiceName }}'
package: '$(Build.ArtifactStagingDirectory)\${{ parameters.ProjectName }}\${{ parameters.FunctionPackageName }}.zip'

Some notes here:

  1. in the build template in restore step used custom config with NuGet.confog file. So in case if needed to use only standard nuger.org feed this change is not needed. But if you use also some private feed in Azure DevOps — Artifacts — so it can have a place to be able to properly build the project.
  2. Pipeline can be used for PR build and manual ones — added some special logic to identify how it called and do some steps. For PR it can be build only without deployment and can be used to make sure that all the code are pushed is working fine (code builds, tests are passed).
  3. Deployment step use special job to invoke the Function App. Sometimes it needed to do the post-deployment things or run some process during the pipeline but before main application is ready (like prepare some data, DB updates, etc.). This step need to run not as Agent job but as Agentless — this is the main difference between other tasks!

The simple project structure provided on the image bellow for function app. Here only one project for function and nothing else.

The project file structure (sample function app)

3.1.2 Create pipeline

Need to push all the files for your project to repository. After this when creating the pipeline select Azure Repos → Select Repo → Existing Azure Pipeline YAML file.

It will open the main file and allow to review and save. Usually the name with be the same as repository so later it need to be renamed (if needed) — in one project can be more that one repository and each of them can contains one or more pipeline.

Created pipeline for Azure Function project

This pipeline has default name and use /build/azure-pipelines.yml file as entry point.

3.2 Create the library

In pipelines used some variables form the library. So to make sure that pipeline will works need to create the variable groups (for each environment) and setup the values.

Some description to variable groups:

  1. Deploy-Environment-Shared — some shared variables that can be used across all environments, like common for all.
  2. Deploy-Environment-Development — variables for Development environment.
Created vatable groups
Shared variable group
Variables for Development environment

For all variables need to add permission — Pipeline permissions.

Select pipelines that can use variables

3.3 Run pipeline

To run the pipeline it need to go to selected pipeline and click on “Run pipeline” button.

Because of pipeline use parameters it will show them to user in the UI to able to select and change if needed. It able to add more values to do some additional steps or decide which flow to use inside (like “SkipLint” will not run lining job).

Run pipeline options

When pipeline successfully validated compiled YAML it will show the stage and jobs.

After running the job we can see the list of steps in the process with all logs and statuses for each step. So it can simple to look into each of the step with more details or even download raw log file.

Details on pipeline run
Pipeline steps and activity

If any environment selected as for this sample we can see the name and status on Environments tab in Build results.

Deployment to Development
All deploys to Development

3.4 Pipeline review

The visual dependencies for files provided on the next image — here we can see what are stages here and jobs. All of them in the separate files.

Files in template

On the next image provided detailed view on the actual template for pipeline for building Function app as .NET applications.

All this steps is general and needed but in your personal projects it can be configured to add more steps and things if needed. This is mostly about minimal setup that can be used for all projects.

3.5 Check the function app

The pipeline use special job step that run the function after deployment. This needed sometimes to make sure that function is working or to do some post-deployment stuff. In the sample used HttpTrigger for function so it means the function can be called by HTTP request and get some result.

This details provided in the Azure Portal.

Function app with list of deployed functions (can be many)
Monitoring result and run details (log)

Based on results here we can decide if all working fine and whole pipeline is working fine and correct.

Note: not all the function need to run. So in such case just need to remove job in file /build/pipelines/release.yml.

Conclusion

Using Azure Pipelines is a great way to implement CI/CD to your projects and even very simple. Here in the article we saw how can we implement the pipeline for Azure Function App (at least in this article — only one function) using a repository in Azure DevOps (Repos), configuration (parameters, variables), usage of templates (YAML files with shared logic), environments, libraries.

Also this sample can be used to configure and use for web app build/deployment as well because one solution or repository can contains web app with function app. So in such case pipeline can be more complex and powerful. But it help to have in place all the things and in code as well.

Links

  1. https://learn.microsoft.com/en-us/azure/azure-functions/functions-how-to-azure-devops?tabs=csharp%2Cyaml&pivots=v1
  2. https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/azure-function-app-v1?view=azure-pipelines
  3. https://learn.microsoft.com/en-us/azure/devops/pipelines/library/service-endpoints?view=azure-devops&tabs=yaml
  4. https://learn.microsoft.com/en-us/azure/azure-functions/functions-versions?tabs=isolated-process%2Cv4&pivots=programming-language-csharp
  5. https://learn.microsoft.com/en-us/azure/azure-functions/create-first-function-vs-code-csharp
  6. https://marcusfelling.com/blog/2019/trigger-an-azure-function-powershell-from-an-azure-devops-pipeline/
  7. https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/azure-function-v1?view=azure-pipelines
  8. https://learn.microsoft.com/en-us/azure/devops/pipelines/process/invoke-checks?view=azure-devops

--

--