Loupe

Automatiser le set up de ses paramètres d’application Azure App Service avec Azure Powershell

Dans Azure App Service, les paramètres d’application (application settings) permettent de placer les configurations applicatives directement au niveau de l’application web hébergée dans Azure.

Une application peut utiliser un nombre important de configurations, bien souvent définies dans un fichier au format json ou xml. Il est possible de stocker manuellement ces configuration sur le portail Azure, cependant ce procédé n’est pas optimal :

  1. Il faut formater les configurations sous forme de clefs / valeurs
  2. Nous pouvons oublier certaines clefs
  3. Nous pouvons faire faisons des erreurs de copier / coller

Ces petites erreurs peuvent avoir un impact non négligeable ! Par exemple le fait d’oublier de définir une chaine de connexion peut faire pointer l’application vers la mauvaise base de données...

Dans cet article, nous allons explorer comme utiliser Azure Powershell pour gérer facilement les fichiers de configurations ASP net core et les paramètres d’application dans Azure. Plus précisément, nous allons découvrir comment parser les fichiers de configuration, les formater et pousser leur contenu en tant que paramètres d’application dans Azure. Le tout en quelques lignes de Powershell !

Le contexte

Dans un projet ASP net core, les configurations applicatives sont stockées dans un fichier json nommé appsettings.json.  Ce fichier est lu au démarrage de l’application afin de pouvoir utiliser les configurations « at runtime ». Pour gérer facilement des configurations applicatives pour plusieurs environnements cible, il est possible de créer un fichier de configuration par environnement.

Dans mon cas, j’ai une simple API asp net core qui utilise des données provenant d’une base de données  Sql Azure et d’un compte de stockage Azure. L’api utilise un serveur d’authentification et est utilisée elle-même par un backoffice et un site web front. Cette architecture est déployée sur 2 environnements : développement (dev) et staging (stg).

Voici mon fichier appsettings.json :

{
 "ConnectionStrings": {
  "BlobStorage": "DefaultEndpointsProtocol=https;AccountName=trastoragedev;AccountKey=*****************************TBUYFMycci8QtjZ0Kre0qgrD2R2BW20MocAUaWTZjUvg==;EndpointSuffix=core.windows.net",
  "Database": "Server=tcp:tra-sql-dev.database.windows.net,1433;Database=tra-db-dev;User ID=tra;Password=**********;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
  },
 "Endpoints": {
  "Auth": {
   "Url": "https://tra-auth-dev.azurewebsites.net"
   }
 },
 "Log": {
  "Url": "https://tra-logs.azurewebsites.net:82",
  "UserName": "tra",
  "Password": "tra!-net",
  "EnvTarget": "dev"
 }
}

Ce fichier utilise l’environnement de développement (chaque ressource distante définie contient le mot clef « dev »).

Voici mon fichier de configuration utilisant l’environnement de staging (appsettings.stg.json) file, il écrase certaines configurations du fichier de base (appsettings.json) pour pointer sur les ressources de staging :

{
 "ConnectionStrings": {
  "BlobStorage": "DefaultEndpointsProtocol=https;AccountName=trastoragestg;AccountKey=*****************************TBUYFMycci8QtjZ0Kre0qgrD2R2BW20MocAUaWTZjUvg==;EndpointSuffix=core.windows.net",
  "Database": "Server=tcp:tra-sql-stg.database.windows.net,1433;Database=tra-db-stg;User ID=tra;Password=**********;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
  },
 "Endpoints": {
  "Auth": {
   "Url": "https://tra-auth-stg.azurewebsites.net"
  }
 },
 "Log": {
  "EnvTarget": "stg"
 }
}

Dans Azure, deux Azure App Service permettent d’héberger et d’exécuter le code :

  • tra-api-dev
  • tra-api-stg

Sur chacune des applications web, il est nécessaire de définir les paramètres d’application. C’est-à-dire que pour chaque configuration présente dans mon fichier de configuration (appsettings.json ou appsettings.stg.json), nous devons récupérer chaque clef et chaque valeur et les définir en tant que paramètre de l’application web Azure. Par exemple, pour le nœud ConnexionStrings du fichier appsettings.json, les clefs et valeurs suivantes doivent être définies au niveau de l’application web :

  1. ConnectionStrings:BlobStorage / DefaultEndpointsProtocol=https;AccountName=trastoragedev;AccountKey=*************
  2. ConnectionStrings:Database / Server=tcp:tra-sql-dev.database.windows.net,1433;Database=tra-db-dev;User ID=tra;Password=**********;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;

J’ai défini les paramètres d’application sur chaque application web manuellement et voici le résultat :

 Paramètres d’application de l’application web tra-api-dev (fig 1):1.png

Paramètres d’application de l’application web tra-api-stg (fig 2):

2.png

Avec des fichiers de configuration relativement petits, il est « possible » de définir les paramètres d’application manuellement. Cependant, au cours du développement, il est fréquent de voir des fichiers de configuration devenir de plus en plus importants ! La technique manuelle devient alors complexe et source d’erreurs. Voyons comment nous pouvons régler cette problématique avec Azure PowerShell.

Créer et mettre à jour des paramètres d’application avec Azure Powershell

La commande Azure PowerShell Set-AzureRmWebApp permet de mettre à jour une application web Azure ainsi que ses paramètres d’application.  

Pour la mise à jour, elle utilise un objet powershell de type « hashtable », le script suivant met à jour 2 paramètres d’applications sur l’application web tra-api-dev :

$subscriptionId = "******"
$rgName = "tra-rg"
$webAppName = "tra-api-dev"
 
Login-AzureRmAccount -SubscriptionId $subscriptionId
 
$appSettings = @{};
  
$appSettings.Add("ConnectionStrings:BlobStorage", "DefaultEndpointsProtocol=https;AccountName=trastoragedev;AccountKey=*****************************TBUYFMycci8QtjZ0Kre0qgrD2R2BW20MocAUaWTZjUvg==;EndpointSuffix=core.windows.net")
$appSettings.Add("ConnectionStrings:Database", "ConnectionStrings:Database / Server=tcp:tra-sql-dev.database.windows.net,1433;Database=tra-db-dev;User ID=tra;Password=**********;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;")
 
Set-AzureRmWebApp -ResourceGroupName $rgName -Name $webAppName -AppSettings $appSettings

Ce script fonctionne bien en l’état ! Cependant, notre objectif n’est pas d’écrire dans ce dernier chaque clef et valeur que nous voulons pousser en tant que paramètre d’application sur l’application web Azure. Pour solutionner cette problématique, nous pouvons écrire une fonction en Powershell qui va parser à notre place un fichier de configuration et renseigner dynamiquement la hashtable @appSettings.

La fonction « Parse »

Dans le monde .net, nous avons l’habitude de jouer avec la librairie json.net. On peut l’utiliser en C# mais également en Powershell J

L’objectif premier de la fonction suivante et de parser un fichier json. De plus, elle inspecte chaque nœud json afin de formater correctement chaque clef (elle concatène chaque niveau de nœud en utilisant le séparateur « : ») : 

[Reflection.Assembly]::LoadFile($newtonsofDllPath)
 
function Parse([string] $jsonPath){
 
 $appSettings = @{}
 
 $json = (Get-Content $jsonPath | Out-String)
 
 $jsonObject = [Newtonsoft.Json.Linq.JObject]::Parse($json)
 
 foreach($d in $jsonObject.Descendants()){
 
  if ($d.Children().HasValues -eq $false){
   $key = $d.Path.Replace(".", ":")
   $val = $d.Value.ToString()
   $appSettings.Add($key, $val)
  }
}
 
 return $appSettings
}

En utilisant cette fonction avec le fichier appsettings.json, elle peuple la hastable @appSettings avec les clefs et valeurs suivantes :3.PNG

On peut facilement l’intégrer à notre script PowerShell :

#Azure configuration
$subscriptionId = "************************"
$rgName = "tra-rg"
$webAppName = "tra-api-stg"
 
#Asp net core configuration
$newtonsofDllPath = "C:\*******\Newtonsoft.Json.dll"
 
[Reflection.Assembly]::LoadFile($newtonsofDllPath) > $null
 
# Parse and format the specified json file
function Parse([string] $jsonPath){ 
  
$appSettings = @{}
 
$json = (Get-Content $jsonPath | Out-String)
 
$jsonObject = [Newtonsoft.Json.Linq.JObject]::Parse($json)
 
foreach($d in $jsonObject.Descendants()){ 
  
 if ($d.Children().HasValues -eq $false){
  $key = $d.Path.Replace(".", ":")
  $val = $d.Value.ToString()
  $appSettings.Add($key, $val)
 }
 
}
 
 return $appSettings
 
}
 
#Log to Azure Subscription
Login-AzureRmAccount -SubscriptionId $subscriptionId
 
$appSettings = Parse -jsonPath $jsonFileToParsePath
 
#Update web app application settings
Set-AzureRmWebApp -ResourceGroupName $rgName -Name $webAppName -AppSettings $appSettings

Avec ce script nous pouvons parser nos fichiers de configuration et directement définir les clefs et valeurs en tant que paramètres d’application de nos application web Azure ! Dans mon exemple, j’utilise un fichier de configuration au format json, mais on pourrait parfaitement modifier notre fonction « Parse » pour qu’elle interprète un fichier Xml ou YAML. 

Allons plus loin !   

Pour ma part, quand je vais sur portail Azure pour vérifier les paramètres d’application d’une application web Azure, je veux être certain que l’ensemble des configurations utilisées à l’instant par l’application sont visibles en coup d’œil.

Sur la figure n°2, nous pouvons observer qu’il y a seulement 4 paramètres d’application Azure définies, alors qu’il y’en a bien plus sur la figure 1. L’ensemble des configurations ne sont pas définies en tant que paramètres d’application Azure pour l’api de staging ! Lorsque l’api de staging aura besoin de la valeur de la clef « Log:Password », elle va rechercher dans un premier temps dans ses paramètres d’application Azure, sans succès. Elle va donc utiliser la valeur par défaut définie dans le fichier appsetting.json. De mon point de vue, c’est problématique car l’application web utilise des configurations stockées dans deux endroits différents et en cas de problèmes de configuration c’est plus compliqué d’aller vérifier des configurations à plusieurs endroits.

La solution consiste à parser les fichiers de configuration de base et le fichier de configuration spécifique à l’environnement avant de fusionner (« merger » !) les valeurs par défaut et les valeurs spécifiques à l’environnement.

Créons une fonction « Merge » qui compare deux tables de clefs / valeurs et qui en retourne une seule avec les données fusionnées :

function Merge([hashtable] $appSettings, [hashtable] $appSettingsOverrides){

 $apps = @{}

 foreach($appSettingKey in $appSettings.Keys){
   
   $val = $appSettings[$appSettingKey];

   if ($appSettingsOverrides.ContainsKey($appSettingKey)){
      $val = $appSettingsOverrides[$appSettingKey]
   }

    $apps.Add($appSettingKey, $val)
 }

 return $apps

} 

Maintenant, nous pouvons combiner l’utilisation de la fonction « Parse » et de la fonction « Merge » :

function ParseAppSettings([string] $appSettingsFile, [string] $envTarget){   
      
   $appSettings = Parse -jsonPath $appSettingsFile 

   if (![string]::IsNullOrEmpty($envTarget)){
       $appSettingsOverrides = Parse -jsonPath $appSettingsFile.Replace("appsettings.json", "appsettings.$($envTarget).json")
       $appSettings = Merge -appSettings $appSettings -appSettingsOverrides $appSettingsOverrides  
   }

   return $appSettings
} 

Cette dernière fonction ParseAppSettings parse deux fichiers de configuration : celui par défaut et celui spécifique à un environnement avant de fusionner leurs clefs / valeurs.

Enfin, voici le script Powershell complet utilisant les fonctions, Parse, Merge et ParseAppSettings :

#Azure configuration
$subscriptionId = "******************"
$rgName = "tra-rg"
$webAppName = "tra-api-stg"

#Asp net core configuration
$newtonsofDllPath = "C:\Dev\ps + json.net\Newtonsoft.Json.dll"
$jsonFileToParsePath = "C:\Dev\ps + json.net\appsettings.json"
$envTarget = "stg"

[Reflection.Assembly]::LoadFile($newtonsofDllPath) > $null

function Parse([string] $jsonPath){   
    
 $appSettings = @{}

 $json = (Get-Content $jsonPath | Out-String)

 $jsonObject = [Newtonsoft.Json.Linq.JObject]::Parse($json)

 foreach($d in $jsonObject.Descendants()){  
      
      if ($d.Children().HasValues -eq $false){
        $key = $d.Path.Replace(".", ":")
        $val = $d.Value.ToString()
        $appSettings.Add($key, $val)
      }
  }

 return $appSettings

}

function Merge([hashtable] $appSettings, [hashtable] $appSettingsOverrides){

 $apps = @{}

 foreach($appSettingKey in $appSettings.Keys){
   
   $val = $appSettings[$appSettingKey];

   if ($appSettingsOverrides.ContainsKey($appSettingKey)){
      $val = $appSettingsOverrides[$appSettingKey]
   }

    $apps.Add($appSettingKey, $val)
 }

 return $apps

}

function ParseAppSettings([string] $appSettingsFile, [string] $envTarget){   
   
   $appSettings = Parse -jsonPath $appSettingsFile 

   if (![string]::IsNullOrEmpty($envTarget)){
       $appSettingsOverrides = Parse -jsonPath $appSettingsFile.Replace("appsettings.json", "appsettings.$($envTarget).json")
       $appSettings = Merge -appSettings $appSettings -appSettingsOverrides $appSettingsOverrides  
   }

   return $appSettings
}

Login-AzureRmAccount -SubscriptionId $subscriptionId

$appSettings = ParseAppSettings -appSettingsFile $jsonFileToParsePath -envTarget $envTarget

Set-AzureRmWebApp -ResourceGroupName $rgName -Name $webAppName -AppSettings $appSettings 

Happy coding :) 

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus