Azure Functions with Docker and Container Registry
Updated: Aug 18, 2022
Azure Functions is a great solution based on the serverless architecture. With this approach you can execute your code on the cloud as a classic web service but maintain less infrastructure, and save on costs. When you create your own web service, you should organise the deployment and servers maintaining, whereas with Azure Functions, the cloud infrastructure provides all the up-to-date resources needed to keep your applications running.
There are plenty of tutorials explaining how to create your first Hello World application using Azure Functions, but the goal of this article is to cover a little bit more advanced topic of setting your application app and running using Docker and Azure Container Registry. Up we go.
Azure CLI version 2.4 or later.
Python 3.8 (64-bit), Python 3.7 (64-bit), Python 3.6 (64-bit), which are supported by Azure Functions.
Azure container registry - Create a container registry in your Azure subscription. For example, use the Azure portal, the Azure CLI, or Azure PowerShell.
Docker CLI - You must also have Docker installed locally. Docker provides packages that easily configure Docker on any macOS, Windows, or Linux system.
This article is inspired by an excellent tutorial on the Microsoft's official website, but for container orchestration it suggests using Docker Hub, whereas in our case, we will publish everything to Azure Container Registry.
In this tutorial we suppose the reader already has a basic understanding of Azure Functions and is able to create such resources as Container Registry and Docker account, but if not, don't worry, just follow the Quickstart tutorials bellow:
Create an Azure Container Registry using the Azure portal
Docker quick start guide
Create an Azure Function with Python
Why may this be useful?
You typically use a custom image when your functions require a specific language version or have a specific dependency or configuration that isn't provided by the built-in image. For instance, you may want to configure your function to perform some image processing, but you keep having the following error:
ImportError: libGL.so.1: cannot open shared object file: No such file or directory
So, if you had a direct access to your container configuration, you could easily solve this problem by installing the required linux packages (see below).
Another advantage is the ability to test your web service locally, and be sure that it will work the same way on the cloud, which is not necessarily the case with the classical deployment. For instance, on your local environment you have no problem with converting your pdf files into images, but on the cloud, your application won't work, as it will require additional tools such as poppler-utils etc.
Finally, for scaling up your application, it is always better to virtualise your working environment, especially if you are dealing with AI packages, and create a web service for inference.
At this step you should already have a working Azure Function (if not, please refer to the tutorial above), and your folder structure should look something like this:
<project_root>/ | - .venv/ | - .vscode/ | - my_first_function/ | | - __init__.py | | - function.json | | - example.py | - my_second_function/ | | - __init__.py | | - function.json | - shared_code/ | | - __init__.py | | - my_first_helper_function.py | | - my_second_helper_function.py | - tests/ | | - test_my_second_function.py | - .funcignore | - host.json | - local.settings.json | - requirements.txt
Now, let's configure our environment. As you remember, in the previous section we discussed the problem of installing OpenCV on the cloud, due to missing libraries such as libgl. Suppose we want to create a web service based on OpenCV but we are unable to import the library. On the linux environment, you would execute the following command to resolve the issue:
apt-get install ffmpeg libsm6 libxext6 apt-get install libgl1-mesa-glx
and then enter y to confirm the download.
In our case, we need to create a Docker file, and configure the environment as the list of commands.
Firstly, select the base image for your Azure Function. To do so, go the docker hub and select the image that best fits your needs. Currently the following base images are available:
mcr.microsoft.com/azure-functions/python:4-python3.8 mcr.microsoft.com/azure-functions/python:4-python3.9 mcr.microsoft.com/azure-functions/python:4-python3.10
mcr.microsoft.com/azure-functions/python:3.0 mcr.microsoft.com/azure-functions/python:3.0-python3.6 mcr.microsoft.com/azure-functions/python:3.0-python3.7 mcr.microsoft.com/azure-functions/python:3.0-python3.8
I suggest using v4 images, but you can choose any of them, because in some cases, you may need to have Python 3.6 to use specific version of tensorflow, for instance.
Once you've chosen your base image, you need to create your Dockerfile (a file with .dockerfile extension) and pull the base image.
Then define the root and enable logging
ENV AzureWebJobsScriptRoot=/home/site/wwwroot \ AzureFunctionsJobHost__Logging__Console__IsEnabled=true
Now, you are ready to install custom linux packages, like libgl
RUN apt-get update RUN apt-get install ffmpeg libsm6 libxext6 -y RUN apt-get install libgl1-mesa-glx -y
Almost done, the only thing left is to install the python packages from your requirements.txt and copy everything to the function's root
COPY requirements.txt / RUN pip install -r /requirements.txt COPY . /home/site/wwwroot
Now your full Docker file looks like this:
FROM mcr.microsoft.com/azure-functions/python:4-python3.9 ENV AzureWebJobsScriptRoot=/home/site/wwwroot \ AzureFunctionsJobHost__Logging__Console__IsEnabled=true RUN apt-get update RUN apt-get install ffmpeg libsm6 libxext6 -y RUN apt-get install libgl1-mesa-glx -y COPY requirements.txt / RUN pip install -r /requirements.txt COPY . /home/site/wwwroot
And your working directory looks like that:
<project_root>/ | - .venv/ | - .vscode/ | - my_first_function/ | | - __init__.py | | - function.json | | - example.py | - my_second_function/ | | - __init__.py | | - function.json | - shared_code/ | | - __init__.py | | - my_first_helper_function.py | | - my_second_helper_function.py | - tests/ | | - test_my_second_function.py | - .funcignore | - host.json | - local.settings.json | - requirements.txt | - Dockerfile
Testing your Docker image is quite straightforward. In the root project folder, run the docker build command, provide a name as azurefunctionsimage, and tag as the version, for instance v1.0.0. It is commonly recommended to replace <DOCKER_ID> with your Docker Hub account ID, but you can put whatever you want. This command builds the Docker image for the container.
docker build --tag <DOCKER_ID>/azurefunctionsimage:v1.0.0 .
N.B. Don't forget to start Docker locally before running the build command.
When the command completes, you can run the new container locally. To test the build, run the image in a local container using the docker run command, replace <docker_id> again with your ID, and add the ports argument as -p 8080:80:
docker run -p 8080:80 -it <docker_id>/azurefunctionsimage:v1.0.0
If your function has an HttpTrigger, after the image starts in the local container, browse to http://localhost:8080/api/<YourFunctionName>?parameterName=Value and you'll se the result.
At this step you should have a Function App that is up and running on Docker, and an Azure Container Registry. If you don't have one, please refer to the tutorial above.
First, login to the registry you've previously created. The recommended method when working in a command line is with the Azure CLI command az acr login. For example, to log in to a registry named myregistry, log into the Azure CLI and then authenticate to your registry:
az login az acr login --name myregistry
Then, use docker tag to create an alias of the image with the fully qualified path to your registry. This example specifies the samples namespace to avoid clutter in the root of the registry.
docker tag <docker_id>/azurefunctionsimage:v1.0.0 myregistry.azurecr.io/samples/opencvimage
N.B. the format for your alias should be <YourContainerRegistryName>.azurecr.io/namespace/image
Now that you've tagged the image with the fully qualified path to your private registry, you can push it to the registry with docker push
docker push myregistry.azurecr.io/samples/opencvimage
Finally, you need to connect your function app to the container registry. In the Azure portal, navigate to the management page for your App Service app. From the left menu, click Deployment Center > Settings.
Choose the deployment source depends on your scenario. In our case, we will select Container registry to set up CI/CD between your container registry and App Service.
Select Azure Container Registry. The Registry dropdown displays the registries in the same subscription as your app. Select the registry you want. select the Image and Tag to deploy. If you want, type the start up command in Startup File. App Service appends the string in Startup File to the end of the docker run command (as the [COMMAND] [ARG...] segment) when starting your container.
App Service supports CI/CD integration with Azure Container Registry. To enable it, selectOn in Continuous deployment. When you enable this option, App Service adds a webhook to your repository in Azure Container Registry or Docker Hub. Your repository posts to this webhook whenever your selected image is updated with docker push. The webhook causes your App Service app to restart and run docker pull to get the updated image. Save your settings by clicking on Save.
This is it.
In this tutorial we've created a Docker image for our function app, configured the environment with custom linux packages, deployed the image to Azure Container Registry and connected our function to it. Hope this was useful.