Implementing CI/CD for Azure Functions with GitHub Actions, Python, and Windows Self-Hosted Runner — A Step-by-Step Guide

Paulo Cesar Rodrigues da Silva
9 min readJan 13, 2024

--

Learn how to set up Azure Functions CI/CD using GitHub Actions, Python and Self-Hosted Runner.

Introduction

As software development technology increased, the demand for delivering also increased and the need for automated, clean, and dynamic way of delivering software naturally improved. In result, tools like code versioning have become mandatory. Soon after, tools and concepts like continuous integration and continuous delivery took place and have become essential for the delivery or release of a software in an automated, dynamic, and transparent way.

For those who wish to go deeper on this subject, I’d recommend reading Luana Oliveira de Melo article: CI/CD with github actions: an end-to-end guide on how to automatically generate release notes from commits. | by Luana Oliveira de Melo | Dec, 2023 | Medium

My scenario and how I made it

  1. Function App Runs within a Private Endpoint:

I have an Azure Function App and the application runs on a private endpoint, that is, setting up workflow using ubuntu, windows or macos will not work due to the function being secured by a private endpoint. So, in this case, the best approach would be to use a self-hosted runner from the machine where the function can connect.

2. GitHub Actions + Windows + Python = Not supported

Believe it: configuring azure functions with GitHub Actions, Windows and Python, the top 5 programming languages in the world actually, is still not supported. But wait, nothing is lost. I will show you how I overcome this and it’s pretty simple.

Step-by-step

Step 1: Configure repository and enable azure functions to deploy via CI/CD

Create your repository on GitHub and push your function.

When you build your functions via package, it’s already a GitHub repository, in which, when the deployment process happens, packages it and uploads to the cloud resource. What you need to do is just create a repository on GitHub and push your function onto it.

Set up Function App to deploy via CI/CD

1. Go to portal.azure.com > {your function app} you will se this screen. On your left hand side, land to Deployment Center

2. After that, you will see a screen to configure CI/CD for the first time. Choose GitHub as code source.

3. Click authorize, to authenticate and enable azure to connect to your GitHub repository.

4. After that, new form is open. Complete it with the infos of your repository.

5. Lastly, choose Basic authentication as Authentication type and click Preview file

6 You will see a yaml workflow file and the path where the workflow file will be on the repository. Close this window and click save on the top left corner.

The azure will create the file, commit and push it tp the repository, and will also create a secret on GitHub repository with a publish profile data, a secret the workflow will use to authenticate and connect to your azure function app. automatically, so you will not need to create it manually.

That being done, now you can see it on .github/workflows/

Step 2: Create a self-hosted runner and configure the machine

Remember what I told in the beginning of this article, that the function runs on a private endpoint and as result, none of the hosted runners GitHub provides will work. So, we need a self-hosted runner.

What is a self-hosted runner?

Self-hosted runner is a physical or virtual machine that you set up and manage to run the jobs instead of the GitHub hosted-runners. It can be used in case you need some specific configuration or, in my case, run in a private environment. To create a self-hosted runner, you have to follow some steps on your GitHub and finish on your machine.

  1. Go to your repository > Settings > Actions > Runners > new self-hosted runner.

You will see a screen with instructions on how to set up your machine to turn it into a hosted runner.

2. Select the operating system and architecture. In this case, select Windows, and x64 as architecture.

After that, you will see a screen like this.

  1. Go to your machine
  2. Open Powershell as adminstrator and run each command as oriented.
  3. The configure part is where you will finally create your runner and make it active, and lastly, run it.

Heads-up: choosing No (N) to run as a service caused some issues like doing some operations that require admin privileges. Choosing Yes (Y) to run as a service solved for me. If you choose Yes, it’ll just ask for email and password to configure the runner.

Now, if you go to your {Repository} > Settings > Runners, you will see your recently created runner.

Congratulations, you’ve set up a machine to be a self-hosted runner to run your deployment pipeline!

Step 3: Configure YAML workflow file

When we configure CI/CD in the Azure Portal, it automatically generates and pushes the workflow config file. This is a YAML file. It contains all the steps responsible for building all the contents and deploying it to the cloud. It basically contains two jobs:

  • Build: Responsible for packaging the project into a zipped filed ready for deploy. This is where it connects to the host, configure environment and build the project.
  • Deploy: Uploads and update the built project to the cloud.

I’ve commented some key steps of the workflow as # — to diferentiate from the auto-generated workflow file from azure, as follows:

# Docs for the Azure Web Apps Deploy action: https://github.com/azure/functions-action
# More GitHub Actions for Azure: https://github.com/Azure/actions
# More info on Python, GitHub Actions, and Azure Functions: https://aka.ms/python-webapps-actions

name: Build and deploy Python project to Azure Function App - my-func-test

on:
push:
branches:
- develop #-- branch where the workflow will execute on
workflow_dispatch:

env:
AZURE_FUNCTIONAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root
PYTHON_VERSION: '3.11' # set this to the python version to use (supports 3.6, 3.7, 3.8)

jobs:
build:
runs-on: ubuntu-latest #-- GitHub hosted runner: the list are :
steps: #-- ubuntu, windows, macos
- name: Checkout repository
uses: actions/checkout@v4 #-- checkout the branch

- name: Setup Python version
uses: actions/setup-python@v1 #-- setup python on runner
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Create and start virtual environment
run: | #-- 'run' is used to run commands on environment
python -m venv venv
source venv/bin/activate

- name: Install dependencies
run: pip install -r requirements.txt #-- run the file of requirements

# Optional: Add step to run tests here

- name: Zip artifact for deployment
run: zip release.zip ./* -r #-- package the project

- name: Upload artifact for deployment job
uses: actions/upload-artifact@v3 #-- upload the project to be deployed
with:
name: python-app
path: |
release.zip
!venv/

deploy:
runs-on: ubuntu-latest
needs: build
environment:
name: 'Production'
url: ${{ steps.deploy-to-function.outputs.webapp-url }}

steps:
- name: Download artifact from build job
uses: actions/download-artifact@v3
with:
name: python-app

- name: Unzip artifact for deployment
run: unzip release.zip

- name: 'Deploy to Azure Functions'
uses: Azure/functions-action@v1 #-- connect to cloud and deploy
id: deploy-to-function
with:
app-name: 'my-func-test'
slot-name: 'Production'
package: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}
scm-do-build-during-deployment: true
enable-oryx-build: true
publish-profile: ${{ secrets.AZUREAPPSERVICE_PUBLISHPROFILE_6974ED55C8FD4EE198DFCB2239DFAB5F }}

Making changes on workflow file

You will have to make some changes on the workflow in order to work:

  • Replace the value on the command runs-on from ubuntu-latest to our self-hosted runner
runs-on: self-hosted

And on the command uses: actions/setup-python@v1 change the the versionv1 to a higher version. This is because thev1 will only check if the python is already installed, whereas the higher version automatically downloads and setup python if not found. The v2 is enough but if you prefer, you can opt to a higher versions

uses: actions/setup-python@v2

I am still getting error. Why?

If you commit the file, it will trigger automatically. However, it will still fail and it’s happening on this part:

source venv/bin/activate

This happens because, as the Azure Function App doesn’t support Python on Windows, it generated the workflow file targeting Linux system. So this commands are from Linux. We have to convert this command to a windows command. Here’s how we must change:

Line 29

# Linux
- name: Create and start virtual environment
run: |
python -m venv venv
source venv/bin/activate
# Shell
- name: Create and start virtual environment
run: |
python -m venv venv
.\venv\Scripts\Activate

Line 39

# Linux
- name: Zip artifact for deployment
run: zip release.zip ./* -r
# Shell
- name: Zip artifact for deployment
run: Compress-Archive -Path .\* -DestinationPath .\release.zip -Force

Line 63

# Linux
- name: Unzip artifact for deployment
run: unzip release.zip
# Shell (Windows)
- name: Unzip artifact for deployment
run: |
Write-Host "Extracting release.zip..."
Expand-Archive .\release.zip -DestinationPath .\Extracted

Complete workflow file

# Docs for the Azure Web Apps Deploy action: https://github.com/azure/functions-action
# More GitHub Actions for Azure: https://github.com/Azure/actions
# More info on Python, GitHub Actions, and Azure Functions: https://aka.ms/python-webapps-actions

name: Build and deploy Python project to Azure Function App - brewdat-bees-ghq-sales-commperf-funcapp-p

on:
push:
branches:
- main
workflow_dispatch:

env:
AZURE_FUNCTIONAPP_PACKAGE_PATH: '.' # set this to the path to your web app project, defaults to the repository root
PYTHON_VERSION: '3.12.1' # set this to the python version to use (supports 3.6, 3.7, 3.8)

jobs:
build:
runs-on: self-hosted-prod
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Python version
uses: actions/setup-python@v4
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Create and start virtual environment
run: |
python -m venv venv
.\venv\Scripts\Activate

- name: Install dependencies
run: pip install -r requirements.txt

# Optional: Add step to run tests here

- name: Zip artifact for deployment
run: Compress-Archive -Path .\* -DestinationPath .\release.zip -Force

- name: Upload artifact for deployment job
uses: actions/upload-artifact@v3
with:
name: python-app
path: |
release.zip
!venv/

deploy:
runs-on: self-hosted-prod
needs: build
environment:
name: 'Production'
url: ${{ steps.deploy-to-function.outputs.webapp-url }}

steps:
- name: Download artifact from build job
uses: actions/download-artifact@v3
with:
name: python-app

- name: Unzip artifact for deployment
run: |
Write-Host "Extracting release.zip..."
Expand-Archive .\release.zip -DestinationPath .\Extracted

- name: 'Deploy to Azure Functions'
uses: Azure/functions-action@v1
id: deploy-to-function
with:
app-name: 'brewdat-bees-ghq-sales-commperf-funcapp-p'
slot-name: 'Production'
package: ${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}
scm-do-build-during-deployment: true
enable-oryx-build: true
publish-profile: ${{ secrets.YOURAZUREPUBLISHPROFILESECRET }}

--

--