Running Python Azure Functions Locally on an M1/M2 Mac
--
Python-based Azure functions are a powerful tool for executing pieces of code or ‘functions’ in response to specific events. However, for developers who are using the latest M1 and M2 Mac machines, running Azure functions locally has posed a challenge due to the shift from Intel to ARM architectures. The latest Azure Function Core Tools still do not support the ARM architecture (Azure Function Core Tools v4).
It is not too easy to find a solution by googling and even Azure forums do not help much. Three solutions have been identified,
- Use a virtual machine environment that has the capability of emulating an x86/amd64 machine and running an x86/amd64 VM. This is not ideal as currently, Parallels does not support x86/amd64 emulation. This leaves users with less popular options on MacOS like QEMU.
- Install Homebrew twice. It is possible to run the MacOS terminal and some other terminals under Rosetta 2 emulation. This allows an x86/amd64 version of Homebrew to be installed. Then homebrew can be used to install an x86/amd64 version of the Azure Function Core Tools. Again, not the cleanest solution, especially if Homebrew is used for ARM also. Fortunately, x86/amd64 and ARM Homebrew installs are in different places. Some specific settings will be required to pick up the right version of the tools for development. A very good write-up on this method can be found at http://issamben.com/running-python-azure-function-locally-on-an-m1-m2/.
- Use Docker with Rosetta 2 emulation. This appears to be the cleanest solution. This is especially true if docker is already familiar. The rest of this article focuses on this approach.
Install and Configure Docker Desktop
Docker Desktop for MacOS can be obtained from www.docker.com and installed normally using guidance on the same site. Once installed a couple of settings need to be applied. These settings are done in the Docker Desktop UI.
Go to the docker settings and check the “Use Virtualization framework” option on the “General Tab”
Then select the “Features in development” tab and select the “Use Rosetta for x86/amd64 emulation on Apple Silicon” option.
Docker is now ready to be able to run x86/amd64 images.
Create a Dockerfile
Create a directory for the Dockerfile and some other files. To run the func
command, a Python image is required. The Azure Function Core Tools v4 supports Python 3.7, 3.8, 3.9, and 3.10. This example will base the image on the standardpython:3.10
image available on Docker Hub. The Dockerfile should look like this,
FROM python:3.10
COPY pip.conf /etc/pip.conf
ADD start.sh /
RUN chmod +x /start.sh
RUN wget -qO- https://packages.microsoft.com/keys/microsoft.asc | \
gpg --dearmor > microsoft.asc.gpg && \
mv microsoft.asc.gpg /etc/apt/trusted.gpg.d/ && \
wget -q https://packages.microsoft.com/config/debian/10/prod.list && \
mv prod.list /etc/apt/sources.list.d/microsoft-prod.list && \
chown root:root /etc/apt/trusted.gpg.d/microsoft.asc.gpg && \
chown root:root /etc/apt/sources.list.d/microsoft-prod.list && \
apt-get update && \
apt-get install azure-functions-core-tools-4
RUN pip install --upgrade pip
WORKDIR /function
CMD ["/start.sh"]
Starting with the standard python:3.10 image, first, a pip.conf
file and a start.sh
script are added. The pip.conf
will likely depend on the development environment. For example, if artifacts are pulled through Artifactory for example, some pip config will be needed here.
The start.sh
file will be explained shortly. The massive wget
command is simply setting up the apt-get repository required to install the azure-function-core-tools-4
package from Microsoft.
After that pip
is upgraded and the working directory is created. The working directory is where the code for the Azure function will reside. More on that later.
Finally, the command which runs when the container is created from the image is set to start.sh
. Ultimately, the start.sh
script will run func start.
The start.sh
file should look like this,
#!/bin/bash
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
func start
This script is pretty straightforward and should look familiar to what existing documentation suggests when running Azure functions locally. Basically, a venv
is created and activated, the packages defined inrequirements.txt
are installed and finally, the func start
command is run. This command is run in the image, which will be run under Rosetta 2 emulation.
The start.sh
script can be tweaked for the targeted project. For example, it's likely there will be local packages and/or whl files to be installed, the script could install these if they exist with minor tweaks. It's just a bash script after all.
Build the Image
To build the image, use the following command
docker build --platform linux/amd64 -f Dockerfile -t python310func .
This command will process the docker file and create a docker image with the Azure Function Core Tools v4 installed. Note the --platform
argument. This is what tells docker to run under Rosetta 2 emulation
Run the Container
To create a container and actually run the func start
command use the following command
cd /path/to/azure/function/project
docker run --platform linux/amd64 -v $(PWD):/function -p 7071:7071 \
--name azfuncproject python310func
There are some things to note here. First, the --platform
argument is to ensure docker runs under emulation again. The -v
will actually map the source code into the /function
directory in the image. When it runs, it will create a .venv
directory and persist it where it would if it were just running locally. This ensures the second execution runs quickly actually. The -p
makes the host port to the container port so that the function can be accessed via http://localhost:7071
as normal also. Finally the --name
argument gives it a name. This is useful as after the first execution, it's possible to start and stop the container in Docker Desktop directly without dealing with randomly generated names.
Conclusion
This approach can support multiple Python versions, and support multiple concurrent Azure Function Projects simply by tweaking images and naming images/containers appropriately. It's close to the natural command line workflow also. Instead of running func start
, a docker run
command is executed which in turn runs func start
.