Setup modern CI/CD for Azure Functions using .NET6 & GitHub Actions

Manuel Pinto
12 min readNov 20, 2021

You dont’t need multiple paid software to bring the modern standards of CI/CD (Continuous Delivery and/or Continuous Deployment) to your next app!

You probably work in a place where you have your code hosted in an git management server (ex: Bitbucket), use some other build management tool (TeamCity/Jenkins) and its possible that you even use some other software for deployment (ex: Octopus Deploy). This heavily increases the time and effort it takes to create and maintain new projects with all the configuration required for these options to work together in a CI/CD pipeline. This can become so hard to handle that companies usually have a dedicated DevOps team responsible for it. Not to mention the heavy licensing costs that some of these softwares have that make it impossible for your small POC project to take full advantages of modern neat CI/CD pipelines. Does this mean that your open source project is doomed from the beginning? NO, thankfully companies are keeping up and offering more and more integrated solutions for CI/CD as well as free-tier services that suffice the needs of most of every occasional hacker project.

We are in the Cloud era, so having to install and spend endless hours configuring the software until all of it works together is an approach from the past. We want to be focused on the code and expect to delegate the rest to some SaaS until our app is deployed. You probably have your code hosted in GitHub so this guide will explore the free, easy-to-use, feature rich, full CI/CD option of GitHub Actions.

We will go in detail through the multiple (painless) steps to create and deploy an Azure Function so that your latest push to GitHub is the one and only action you need to take in order for your app to be built, tested and deployed to Azure!
I tried to be as generic as possible so this guide can also be helpful for other function languages but because its much easier to explain these concepts with a practical example the source code used is based on .NET 6. Steps discussed in this guide:

  • Set up local coding environment
    * Create and configure GitHub repo
    * Set up postman to consume the endpoints
    * Set up Visual Studio Code
  • Configure Azure to host your function
    * Create Function in Azure
    * Link function with GitHub
  • Configure GitHub Actions for build, testing and automatic deployment
    * The default workflow
    * Customise the workflow
    — Build solution from non default folder
    — Run tests before deployment
  • Manage App settings
    * Manage environments
    * Testing different environments

Setup local coding environment

Create and configure Github repo

Create a github repo or clone the one from this example: azure-function-example-csharp.

In order to separate dev from production code the default main branch is renamed to master and created an additional dev branch to handle test environment code.
In the end you should have something similar to this:

Newly created repo with master and dev branch

This repo contains a full-featured azure function project subject of the medium article Create a complete Azure Function project in .NET 6 and AF v4. This will help us illustrate topics like testing and handling of secrets in the cloud that require some additional work to the default provided azure templates.

Clone this repo dev branch to your local environment using your favourite git UI or via the command line:

git clone --branch dev https://github.com/YOUR-USERNAME/YOUR-REPOSITORY

Set up Postman to consume the endpoints

Postman is a popular tool for consuming API endpoints. I will not go into detail on how to set this up but let us create 3 environments for the function:

  • Local: For local development
  • Web-Test: For cloud test environment from dev branch
  • Web-Production: For web production from master branch

Each environment is differentiated by the function’s base url. For now let us create a func-ex-base-url var with the localhost address in the Local Env.

Set up Visual Studio Code

You can use your preferred code editor for the subsequent changes. I’ll use VSCode as its free and has all the required functionality to work with functions.
Open Visual Studio Code within the cloned path.

There are extensions for deploying the code directly to Azure but for the sake of learning, let us use the local as an isolated environment and let all the CI/CD heavy lifting to GitHub. This helps us separate concerns, where the dev machine is only responsible for development and local testing so that the only output is in the form of a commit to Github.

Start your function locally and make sure that the endpoint can be consumed by Postman. Using the provided example, the function code is present in “src/Functions”. For example, function GetOpenStockPriceForSymbol:

This exposes the endpoint {func-ex-base-url}/api/stock-price/symbol/{symbol}/open

If you are using AzureFunctions OpenAPI Extension the endpoint definition is also available through the swagger page.

Since its a GET method you can open that link directly in the browser or through curl

curl --location --request GET 'http://localhost:7071/api/stock-price/symbol/AAPL/open'

But it is advisable to use an API platform like Postman to better organise the different requests/environments.

Set the {func-ex-base-url} variable for Local env in postman to the address displayed in VSCode console when the function was started. In this example it would be “localhost:7071”.

Hit the endpoint and make sure you get a successful result (symbol used: Apple Inc. (AAPL))

Your local environment is all set. Let us jump to the cloud configurations!

Configure Azure to host your function code

Create Function in Azure

Let us create a function app for the Development environments. Go to the Azure Portal > Create Resource > Create Function App

I’m using the following basic configurations:

You will need to create a Resource Group and a Storage Account. Make sure your subscription plan allows you to create these entities. Optional but advised is to enable AppInsights to help monitor the application.

Link function with GitHub

Jump to the dashboard of the newly created function and use the vertical navigation bar to go to Deployment > Deployment Centre. Here you can configure a source control trigger for automatic deployment.

Link your Github Account and select the repo as well as the branch depending on the target environment of the azure function.

Once you press Save, Azure commits a YAML file to the selected branch of your project with instructions to be interpreted by GitHub Actions with the deployment pipeline.

Configure GitHub Actions

If you followed the linking step, you should have a new commit file in .github/workflows/. This is the folder where the multiple instruction workflows sit in the form of .yml files that are automatically picked up by Github.

Let us have a look at the newly created <project_name>.yml file.
This is a workflow file that can be divided in four sections: Events, Environment, jobs and steps.

Events

The on instruction acts as the event trigger where we instruct GitHub when and in what circumstances to run this workflow. This simple example uses a push to dev event meaning that even if this file is merged to master it wont trigger any workflow process. We can see this already in working if you have a look into your GitHub repository > Actions

you can see that the workflow is triggered. If you created the function using the default templates you may get a successful run of the workflow. The example I’m using has non default source paths so we can understand how to configure the workflow file in order to adapt to different code structures.

Environment

The env command will detail the environment variables that are going to be used by the jobs. In the example we are setting the DOTNET_VERSION and the AZURE_FUNCTIONAPP_PACKAGE_PATH.

Jobs

Jobs contain groups of steps sharing the same server and by default run in parallel. The azure generated template only has one job: build-and-deploy.

Steps

Steps are individual tasks that run inside a job and server. Running in the same server means that they can share context within each other. There are two types of tasks:

  • Shell Commands
    example: $dotnet build — configuration Release — output ./output
  • Actions
    Actions are encapsulated scripts to facilitate the usage of common CI/CD processes which are shared between the GitHub Community. This greatly reduces the complexity of creating advanced workflows. Feel free to browse the market place or even create your own. The user can interact with function parsing arguments via the with command.

These are the basic building blocks of your workflow, if you want to know more, have a look at GitHub Actions documentation.

The default workflow

Let us have a look at the jobs section of the workflow file to try and figure out how it works.

Ok, so we have a job named build-and-deploy running on a windows-latest server and then a bunch of sequential steps. Lets divide the steps and try to visualise them in a diagram:

  • Checkout Github Action

This checks out our function code into the build server directly from GitHub through the action /checkout@v2. One of the advantages of having the server within GitHub is that there is no need to explicitly provide the repository url as its available through an environment variable $GITHUB_WORKSPACE.

  • Setup DotNet Environment

In order to interact with our checked out code the server needs to install the dependent language stack. This example uses dotnet so the stack is returned through the action setup-dotnet@v1. Be sure to provide the dotnet version using with: dotnet-version: <dotnet-version>
(env: DOTNET_VERSION: ‘v6.0’ in the example)

  • Resolve Dependencies

This is the step required to build the solution. No GitHub action is used, instead the shell command dotnet build outputs the results into the /Output folder.

  • Deploy Azure Function:

Finally, the compiled code is published to Azure using the action Azure/functions-action. The unique publish profile key generated by Azure during workflow creation links the stream to the correct cloud function.

Modify and visualise workflows in VSCode

Although you can easily see the workflow execution and logs in GitHub there is also a very easy way to visualise it without leaving VSCode.
The extension GitHub Actions

provides you with all the features you need when developing workflows. There is Auto Complete, workflow log and ability to trigger workflows directly from VSCode.

Customise the workflow

The default workflow only works out of the box for a template-created basic function. Once the project starts to grow and you are forced to reorganise the folder structure, add tests, version control, … then you will also need to adapt the workflow file. Thankfully, with a basic understanding of the workflow’s building blocks it becomes easy to know what and where to modify it.

The sample code has the following simplified folder structure (taken from article)

├── src
│ ├── Function.Domain
│ │ ├── (...)
│ ├── Functions
│ │ ├── GetCloseStockPriceForSymbol.cs
│ │ └── GetOpenStockPriceForSymbol.cs
│ ├── Program.cs
│ ├── azure-function-example-csharp.csproj
│ ├── host.json
│ └── local.settings.json
└── tests
└── unit-tests
├── Function.Domain
│ └── (...)
└── unit-tests.csproj

There are two projects: function azure-function-example-csharp.csproj and unit test unit-tests.csproj sitting in /src /tests folders respectively.

Build solution from non default folder

To build the project we need to modify the workflow so it points the build target to the /src folder instead of project root.

This will output the build results into the /src/output folder to be deployed to Azure

Run tests before deployment

We want to deploy the application only when we are absolutely sure all tests have passed. The testing should run between the build and before the deployment so we just need to add an extra step:

- name: Test
run: |
pushd './${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}/tests/unit-tests'
dotnet test
popd

Similar to building, here we are invoking the dotnet command to run the tests.

A failed test prevents the execution of subsequent steps.

Go ahead, try to run the workflow and the Test step should be successful.

Manage App settings

Imagine you have some secret API key in your solution that you do not want to share in your code or even different settings for local and cloud environments. This is why we put the file local.settings.json has part of .gitignore. These are the settings that will be used when running the function locally, but once its hosted in Azure how does the function know which settings to use? Thankfully, Azure makes this configuration pretty straightforward.

Let us pick up from the example, the stock data is retrieved through a secret API key sent in the header with the request.

The GetEnvironmentVariable method looks for the setting key in local.appsettings.json

"Values": {
"AzureWebJobsStorage": "",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"finhub_api_baseUrl": "https://finnhub.io/api/v1/",
"finhub_api_token": "XXXXXXXXXXXXXXXXXXXXXX"
}

To manage environmental variables in Azure, go to Settings > Configuration > New Application Setting

You can now add secret and/or environment-dependent variables.

Manage Environments

We have now a working function in the cloud but we want to separate development/test code from production. Usually production code is linked with a master/main whereas dev/test sits in a different branch.

The first step is to create a function in azure for production code. This is similar to the dev function. Let’s add the prefix ‘prod’ to differentiate the functions. At this stage we have:

The way we link different branches with different functions is through the deployment centre similarly to the the dev setup.

Remember that this step will push the workflow file into our repo in master branch.

Let us merge our changes from the dev workflow so that only the source branch and app-name is different between these files. Be careful not to replace the key in the publish-profile step as each function has its own deployment key.

on:
push:
branches:
- master

Now, every push to the master branch is going to trigger function deployment to production. There is no harm in keeping both workflow files as each will only be called when there is a push to the specific branch.

This approach has its limitations but it only purpose here is to show the deployment process to different branches, see Azure Deployment slots for more info on best practices for production deployment.

Testing with different environments

With our Functions up and running the easiest way to test is to use Postman’s environments.

Each environment will have a different value of the base function url {func-ex-base-url}. This way we can reuse the requests and test each environment individually.

Summary

Hope this will help you understand the basics of CI/CD development using GitHub so that you can easily integrate it in your next project! Happy coding :)

--

--