Déployer un process Serverless avec Terraform et Azure DevOps

Nous vous avions déjà parlé de Terraform sur le blog Cellenza, pour rappel c’est un outil open source développé par HashiCorp, qui est utilisé pour provisionner et gérer des infrastructures IT dans le cloud. Écrit en Go et fonctionnant en mode Infrastructure as a Code, il permet de facilement administrer l’infrastructure grâce à du code au lieu de procéder à un ensemble d’opérations manuelles.
Pour bien débuter avec Terraform, je vous recommande la lecture de ces articles :
- Comment déployer votre infrastructure Azure en toute sécurité avec Terraform
- Provisionner votre infrastructure Azure avec Terraform
- Comment utiliser Terraform dans Azure DevOps
Dans cet article, nous allons donc voir comment déployer un processus Serverless avec Terraform et Azure DevOps. Pour rappel, le Serveless est un modèle d’architecture sans serveurs, c’est-à-dire des applications, des services ou de simples fonctions qui s’exécutent à la demande uniquement. On parle également de Function as a Service ou « FaaS », si vous souhaitez en savoir plus, nous avons traité le sujet dans notre Cell’Insights #9 – Serverless, la nouvelle révolution du Cloud ?
Rappel des objectifs pour déployer un process Serverless avec Terraform
Dans cet article, nous allons voir :
- Comment mettre en place un traitement à base d’Azure Functions,
- Utiliser conjointement Terraform et Azure DevOps dans l’optique de déployer l’infrastructure Azure, de manière automatique et continue.
Notre architecture d’exemple se compose de la façon suivante :
- 3 Azure Functions, ayant des types de trigger différents,
- 1 Blob Storage, pour persister les données reçues,
- 1 Service Bus, exposant une Queue.
Le scénario que nous allons développer est donc le suivant :
- (1) une requête HTTP/POST est initiée sur la function F1,
- (2) la function F1 enregistre le body de la requête (JSON) dans le Blob Storage et poste un message (contenant le lien du blob) sur la Queue,
- (3) la function F2 se déclenche, charge le JSON depuis le Blob Storage et fait un traitement,
- (4) la function F3 se déclenche et fait un autre traitement.
L’intérêt de faire persister les données de la requête HTTP dans un blob est double, tout d’abord, cela permet de garder une trace des requêtes pour éventuellement les rejouer en debug. L’autre avantage est que la taille des messages sur la queue du ServiceBus est limitée.
A présent nous allons :
- créer la stack Terraform pour la description déclarative de l’infrastructure,
- créer la solution Visual Studio contenant les Azure Functions,
- mettre en place la CI/CD avec Azure Pipelines pour l’intégration & le déploiement continu.
Sources de cet article
Vous pouvez retrouver les sources de la stack Terraform et de la solution Visual Studio sur ce repository.
Avant toute chose, je vous conseille de réaliser les actions ci-dessous :
- Créez un repository sur Azure DevOps,
- Uploadez les sources dessus.
Quelques prérequis avant de commencer
Avant d’aller plus loin dans cet article, vous aller avoir besoin des éléments suivants :
Note : l’installation de Terraform sur votre poste n’est pas nécessaire. Cependant, cela peut être utile pour exécuter les commandes “valide” et “plan” en local avant de procéder au commit des modifications sur le repository.
Voici également quelques outils utiles à avoir avec soi :
Création de l’infrastructure
Première partie, l’infrastructure avec Terraform
Commençons tout d’abord par définir l’infrastructure grâce à Terraform.
💡 Remarque : toutes les variables de la forme __variablename__ seront substituées lors du déploiement sur l’environnement cible.
Fichier provider.tf
Le rôle de ce fichier est de spécifier les versions de Terraform & du provider AzureRM à utiliser pour l’exécution de la stack.
Pour une exécution locale, il suffit de commenter au préalable le bloc backend (qu’il faudra évidemment dé-commenter avant tout commit).
Fichier variables.tf
Ici, le rôle de ce fichier est de définir les différentes variables.
Dans notre exemple, les variables function_f1_username & function_f1_password seront utilisées pour une Authentification Basic au niveau de la function F1.
Fichier variables.tfvars
Il est question ici d’affecter les valeurs des variables. Ici, ces variables tokeneurisées, seront substituées lors du déploiement sur l’environnement cible, lors de la Release dans Azure Pipelines.
Pour exécution locale de la stack, nous pourrons utiliser le fichier variables.local.tfvars (cf. notes dans provider.tf).
Fichier main.tf
Ce fichier contient la définition des différentes ressources Azure à créer.
Remarque : en tant que développeur, la ressource la plus intéressante est la ressource function_app / app_settings qui définie les chaînes de connexion et autres variables qui seront utilisées dans les Azure Functions.
Voici ce à quoi correspond ce fichier :
- FUNCTIONS_WORKER_RUNTIME : runtime dotnet,
- APPINSIGHTS_INSTRUMENTATIONKEY : clé Application Insights pour le monitoring des functions,
- KeyVaultName : nom du KeyVault pour accéder aux secrets,
- StorageConnectionString : chaîne de connexion pour accéder au Blob Storage,
- StorageBlobContainerName : nom du conteneur du blob dans lequel seront persistées les données des requêtes,
- ServiceBusConnectionString : chaîne de connexion pour accéder au Service Bus,
- ServiceBusQueueName : nom de la queue exposée par le Service Bus.
Comme nous pouvons le voir, la plupart de ces variables découlent de la stack Terraform, nous n’aurons donc jamais à les renseigner, ni dans les variables Terraform (variables.tfvars), ni côté Azure DevOps (variables de la Release).
Côté C# / Functions, ces variables seront injectées dans les variables d’environnement et chargées à leur habitude via l’objet IConfigurationRoot.
Nous pouvons remarquer qu’il n’y a pas de trace des variables pour l’authentification (function_f1_username & function_f1_password). En effet, elles seront protégées par le KeyVault (et chargées également par IConfigurationRoot).
Fichier complet :
Azure DevOps – Build
Voici la définition de la Build qui va tout simplement permettre de packager les fichiers Terraform.
Pour rappel, il n’y a aucune substitution de variable au niveau de la Build. Cette opération sera réalisée lors de la phase de Release.
Nous activons l’intégration continue dès qu’une modification est effectuée sur des fichiers du répertoire terraform/
Lorsque la première build est effectuée, nous obtenons l’artifact suivant :
Azure DevOps – Release
Créons à présent la phase de Release dans Azure DevOps.
💡 Important : je recommande d’utiliser ici le Host 2017. Avec le Host 2019, en cas d’erreur, Terraform n’affiche pas le message.
L’étape suivante permet de mettre en place le backend des states de Terraform sur le Blob Storage partagé (resource group shared) si celui-ci n’existe pas.
Azure CLI Script :
Note : le flag –public-access off est important afin de protéger l’accès au Storage (les states peuvent contenir des données sensibles).
Cette étape permet de récupérer l’AccessKey du Storage pour l’enregistrer dans la variable tf_storage_account_key, pour que Terraform puisse y accéder.
Important : la version 4.* est ici requise pour avoir accès aux commandes Az (AzureRm dans les versions antérieures).
Azure Powershell Script :
Les 2 étapes suivantes permettent la substitution dans les fichiers Terraform.
Pour chacune de ces 2 étapes, n’oublions pas de changer les Token prefix et suffix dans la section Advanced (avec double-underscores).
A présent, les 4 étapes suivantes permettent d’exécuter la stack Terraform.
- init : initialise le contexte avec notamment le provider AzureRM,
- validate : valide la syntaxe dans les fichiers,
- plan : affiche le delta entre la stack et le state,
- apply : applique les modifications nécessaires.
Procédons à la définition des variables :
💡 Important : il est recommandé de saisir votre propre valeur custom dans le champ application pour éviter toute collision si les ressources existaient déjà sur Azure (certaines étant globales).
Nous pouvons à présent lancer notre première Release !
Une fois la Release exécutée, 2 ressource groups sont créés :
- cellenzawkp-dev : qui contient l’ensemble des ressources pour l’environnement de DEV,
- cellenzawkp-shared : qui contient un Blob Storage pour la persistance les states de Terraform.
Contenu de cellenzawkp-dev :
Rendons-nous à présent dans la ressource cellenzawkp-dev-function-app, section Configuration :
Nous retrouvons un certain nombre de variables d’environnement que nous utiliserons dans les Azure Functions, notamment :
- KeyVaultName
- ServiceBusConnection
- StorageConnectionString
Du côté du KeyVault, nous retrouvons l’Access Policy cellenzawkp-dev-function-app qui permettra aux functions, via MSI, d’accéder au KeyVault. Cet accès sera requis pour accéder au secret d’authentification, pour la function F1.
Azure Functions
Du côté du Code
Avant d’aller plus loin, voici quelques explications concernant la solution Visual Studio.
Les dépendances
-
Microsoft.ApplicationInsights.AspNetCore : cette dépendance permet d’activer automatiquement le monitoring des Azure Functions.
-
Microsoft.Azure.Services.AppAuthentication : cette dépendance permet d’accéder au KeyVault lors de l’exécution des functions en local, avec vos credentials.
Application Insights
Les Azures Functions sont monitorées avec Application Insights. Nous avons juste ajouté la dépendance Nuget Microsoft.ApplicationInsights.AspNetCore.
La clé InstrumentationKey sera connue au travers de la variable d’environnement APPINSIGHTS_INSTRUMENTATIONKEY (vue précédemment dans la section app_settings de la stack Terraform). Il n’y a aucune action supplémentaire à réaliser.
Accès au KeyVault
Pour accéder au KeyVault lors d’une exécution en local, nous utilisons le package Microsoft.Azure.Services.AppAuthentication.
Dans notre exemple, nous utiliserons un KeyVault pour récupérer un secret d’authentification au niveau de la function F1.
Information : lors de l’exécution en local, l’erreur suivante peut survenir : Microsoft.Azure.KeyVault: Operation returned an invalid status code ‘Forbidden’
Points à vérifier :
-
Compte Azure utilisé au travers de Visual Studio :
(Tools > Options)
-
Existence d’une Access Policy au niveau du Keyvault pour votre compte :
( ⇒ Secret permissions : Get & List)
Azure DevOps – Build
Ici, rien de particulier, nous allons créer une Build prédéfinie pour un projet Core.
Changeons seulement le Host à utiliser (Hosted Windows 2019 with VS2019).
Azure DevOps – Release
La définition de la Release reste assez basique.
Cette étape permet de définir une clé statique $(F1_StaticPrivateAccessKey) associée à la function F1. Cette clé sera nécessaire pour le consommateur de la function F1 (nous pourrions créer 1 clé par consommateur, les révoquer, etc.).
💡 Important : la version 4.* est ici requise pour avoir accès aux commandes Az (AzureRm dans les versions antérieures).
Et finalement, la définition des variables.
Exécution
Exécution locale (Debug)
Nous devons ajouter avant tout le fichier local.settings.json.
Renseignons les variables d’environnement nécessaires.
Très important : ce fichier ne doit pas jamais être versionné (présent dans .gitignore par défaut).
Pour récupérer les valeurs des variables, rendons-nous dans la ressource cellenzawkp-dev-function-app, section Configuration :
Exécutons la solution et utilisons Postman pour envoyer une requête POST sur http://localhost:7071/api/F1.
Pour rappel, les credentials sont définis dans les variables de la Release de Terraform (function_f1_username / function_f1_password).
Et voici le résultat après l’exécution :
Exécution dans Azure
Pour ce faire, utilisez tout simplement l’URL suivante, avec la clé statique : https://cellenzawkp-dev-function-app.azurewebsites.net/api/F1?code=mBkJPZvTnxeH...
Conclusion
Dans cet article, nous avons vu comment provisionner une infrastructure Azure Serverless avec Terraform, le tout, déployé de manière continue au travers d’Azure DevOps 😃