
Dans ce nouvel article, je vais montrer comment il est possible de migrer des Fonctions Azure sous la forme de Conteneur Docker. Vous vous demanderez certainement pourquoi j’en suis arrivé là ? Mais vous verrez qu’il y a quelques applications qui sont intéressantes …
Azure Function
Pour rappel Azure Function est une solution ServerLess permettant d’exécuter du code sans avoir à se soucier du provisionnement de ressources pour pouvoir l’exécuter. Ce type d’approche est extrêmement intéressant lorsque vous souhaitez exécuter des briques de code qui seront déclenchés par évènements (Ex : Requête HTTP, Event Hub, …), dans ce cas c’est Azure qui se charge de mettre le nombre d’instances nécessaires de votre fonction pour que votre application respecte les performances attendues.
Azure Function, c’est donc également un SDK permettant de structurer et d’accélérer les développements basés sur des évènements. Le SDK permet en effet d’intégrer de façon déclarative (sans connaissance des classes techniques consommées) des input/output/triggers.
Vous trouverez sur cette page la liste des Triggers et Liaisons disponibles avec Azure Function : Triggers and bindings in Azure Functions | Microsoft Docs.
Enfin sachez que Azure Function est disponible pour plusieurs langages :
- C# (évidement)
- JavaScript/TypeScript
- Java
- Python
- Powershell
- F#
Mais pourquoi migrer en conteneur docker ?
On peut effectivement se poser la question de pourquoi migrer sous forme de conteneur au vu des intérêts qu’apportent les solutions FaaS (Function As a Service). Dans mon cas voici les raisons qui m’ont poussé à explorer cette piste :
- Une brique applicative développée avec le SDK Azure Function
Cette brique que j’ai utilisée était disponible uniquement sous la forme de Fonction Azure. Or pour des raisons de déploiement (sécurité, mis à disposition de ressources Cloud dans un environnement contraint, …) il me fallait l’exécuter également on-premise. Je me refusais également à modifier le code source de cette application puisque je voulais bénéficier des mises à jour de cette brique sans le moindre effort.
- Développement de Function et Scalling
Dans le cadre de développement custom, j’ai utilisé le SDK Azure Function pour réaliser des traitements d’évènements. Cela se passe bien, mais maintenant j’ai du scalling à faire, ma fonction s’exécute de plus en plus. Le server-less fait son travail de multiplication d’instance, mais forcément le retour de la médaille la facturation augmente en conséquence. J’ai de part ailleurs une infrastructure Kubernetes sur le même projet, qui est provisionnée et qui a suffisamment de ressources pour exécuter en plus ma fonction. Mais cette infrastructure ne supporte (pas encore) les briques FaaS. Migrer mon code, sans modification sur un conteneur me permet alors de déplacer ma charge de travail et de rationaliser mes coûts d’infrastructure sans nécessiter de développements supplémentaires.
- Développement IoT Edge
C’est une des solutions que j’utilise. En effet il est possible également sous IoT Edge de réaliser des briques de transformation des évènements IoT à l’aide de fonction Azure (Exécutées sur le cloud) et en Edge lorsque c’est nécessaire. Utiliser la même technologie pour exécuter, sur les deux environnements, des tâches similaires est un plus en termes d’accélérateur.
- Le coût
L’utilisation de Azure Function en tant que SDK pour être hébergée en self-hosted n’implique aucun coût de License. Vous ne paierez donc que l’infrastructure nécessaire pour exécuter votre fonction…
Migration d’une fonction existante
Pour réaliser une migration d’une fonction azure existante en conteneur docker, vous le verrez, Microsoft nous mâche bien le travail en nous fournissant des images de bases que nous pourrons utiliser pour embarquer nos fonctions :
- .NET Core : mcr.microsoft.com/azure-functions/dotnet
- Java : mcr.microsoft.com/azure-functions/java
- Node : mcr.microsoft.com/azure-functions/node
- Python : mcr.microsoft.com/azure-functions/python
Toutes ces images sont disponibles pour le runtime 2.0 et 3.0 de Azure Function avec des images de bases différentes en fonction de vos besoins.
Dans mon cas, je vais partir d’une fonction Azure en runtime 3.0 réalisée en .NET Core 3.1.
Dans mon cas j’ai souhaité démarrer en générant une image Docker à partir d’un code source présent dans un autre repository afin de séparer les responsabilités (le repository était un repo github public de microsoft…).
Je réalise donc le build de la fonction à partir du SDK de dotnetCore 3.1 comme suit :
FROM mcr.microsoft.com/dotnet/sdk:3.1 AS build-env
WORKDIR /src
RUN git clone -b ${GIT_TAG} https://github.com/<project>.git && mkdir -p /home/site/wwwroot
WORKDIR /src/<project-name>
RUN dotnet publish -c Release --output /home/site/wwwroot
Puis je réalise l’image docker de mon runtime à partir du stage précédent :
FROM mcr.microsoft.com/azure-functions/dotnet:3.0
ENV AzureWebJobsScriptRoot=/home/site/wwwroot
WORKDIR ${AzureWebJobsScriptRoot}
COPY --from=build-env /home/site/wwwroot ./
Une fois mon Dockerfile complet, je peux maintenant tenter de générer l’image tant attendue et enfin l’exécuter :
docker build . -t az-func-self-hosted
docker run -d -p 8080:80 az-func-self-hosted
Vous pouvez également réaliser cela simplement avec un fichier docker-compose.yml :
version: "3.2"
services:
lora-key-manager-facade:
image: "az-func-self-hosted:${VERSION}"
build:
context: .
args:
- "GIT_TAG=v${VERSION}"
restart: always
environment:
- FUNCTIONS_EXTENSION_VERSION=~3
ports:
- 8080:80
Pour compléter donc cette utilisation voici quelques configurations qui vous seront peut-être nécessaires.
Host secrets
La première chose que j’ai dû changer ce sont les secrets de mes fonctions qui seront appelées. Cela permet par exemple de déployer la fonction et de mettre une authentification minimale avec un code unique qui sera partagé uniquement entre l’appelant et la fonction (un API-Key en sommes).
Pour cela, il suffit de créer un fichier hosts_secrets.json contenant les informations suivantes:
{
"masterKey": {
"name": "master",
"value": "master-key",
"encrypted": false
},
"functionKeys": [
{
"name": "default",
"value": "function-key",
"encrypted": false
}
]
}
Vous pouvez ensuite monter le fichier dans votre conteneur en ajoutant le point de montage comme suit :
volumes:
- ./host_secrets.json:/azure-functions-host/Secrets/host.json:ro
Puis ajouter le paramètre AzureWebJobsSecretStorageType qui indiquera à Azure Function que vos secrets sont stockés sous la forme de fichiers disponibles dans les points de montage du conteneur :
environment:
- AzureWebJobsSecretStorageType=files
Port de Bind
Par défaut la fonction Azure va binder le port 80 du conteneur, charge à vos déploiements (docker-compose, k8s, …) de router ce port sur le port que vous souhaiterez. Parfois certaines contraintes sur les infrastructures font que vous n’aurez pas le droit d’ouvrir certains ports (dont le 80) même sur le conteneur. Il nous advient alors de changer ce port d’écoute dans le conteneur. Dans mon cas je suis en .NET Core et le paramètre se trouve être un standard dans ASP.NET Core que vous pouvez mettre sous la forme de variable d’environnement :
environment:
- ASPNETCORE_URLS=http://*:8080
Override du fichier host.json
Azure Function permet également de configurer le conteneur au travers d’un fichier host.json disponible à la racine de votre application de fonction. Vous aurez donc la possibilité de monter un fichier sur
En ce qui me concerne, je préfère généralement utiliser des variables d’environnement (Article 3 du 12-Factor App : The Twelve-Factor App (12factor.net)).
Le runtime permet cela en créant des variables d’environnement préfixées par : AzureFunctionsJobHost.
Plus d’infos sur : host.json reference for Azure Functions 2.x | Microsoft Docs
Conclusion
Effectivement, migrer une Fonction Azure c’est aussi simple que cela. Cependant, j’avoue que tout n’est pas entièrement documenté. Dans mon cas, il a fallut également que je modifie le comportement du conteneur pour prendre en charge les secrets (ceux qui servent notamment à l’authentification par code aux appels HTTP) ou encore le changement du port de bind du conteneur pour respecter des contraintes de sécurité et même le logging.
Il n’y a là rien d’insurmontable, au contraire. Finalement nous remarquons qu’il est possible de gagner du temps lors de la conception de notre application évènementielle. En utilisant un SDK prévu pour le cloud mais exécuté sur un environnement CaaS sans aucune modification ni adaptation : seul le packaging a changé.
De plus si vous souhaitez utiliser des fonctions azure dans le but de les migrer un jour dans une infra CaaS, sachez que Azure supporte déjà la possibilité d’exécuter votre fonction sous la forme de conteneur, vous permettant de vous mettre dès maintenant dans une configuration de release vous permettant de migrer rapidement…