Alle Zügel in einer Hand

Konfigurationen übergreifend verwalten mit Azure App Configuration
31
Aug

Alle Zügel in einer Hand

Viele Azure Services, wie z. B. Azure App Services, Azure Functions und Azure Kubernetes Services ermöglichen es, Konfigurationen beim Deployment von .NET-Konfigurationssettings zu übernehmen und in Produktion dann bei den ressourcenspezifischen Konfigurationen Einstellungen vorzunehmen. Anstatt jeden Azure App Service und jede Azure Function eigens zu konfigurieren, kann aber auch ein zentraler Konfigurations-Service verwendet werden: Azure App Configuration.

Dank Azure App Configuration ist es möglich, alle Konfigurationen einer gesamten Lösung zu verwalten. Azure App Configuration bietet eine zentrale Stelle für die Konfigurationen, ermöglicht das Nutzen unterschiedlicher Konfigurationen für verschiedene Environments und bietet auch die Möglichkeit einer Abstraktionsschicht für konfigurierte Secrets in Azure Key Vault. Nebenbei bietet Azure App Configuration auch Featuremanagement

 

.NET Configuration

Im Gegensatz zu .NET-Framework-Applikationen bietet .NET seit der ersten .NET-Core-Version eine flexible Konfiguration, in der unterschiedliche Provider konfiguriert werden können, um auf Konfigurationen aus unterschiedlichen Sources zuzugreifen, z. B. JSON-, XML-, und INI-Files, Environment Variables und Applikationsargumente. Die Sample-Applikation [1] wurde über diesen .NET CLI Command erstellt:

 

> dotnet new webapp

 

Beim Verwenden der Host-Klasse und der Methode CreateDefaultBuilder (Listing 1) werden für das Lesen der Applikationskonfigurationen diese Provider vordefiniert:

 

  • JSON Provider für die Datei json
  • JSON Provider für die Datei {EnvironmentName}.json
  • Environmental Variable
  • Command-Line-Argumente
  • User Secrets (nur im Develop Environment, wenn User Secrets konfiguriert wurden)

 

Listing 1: Program.cs

public static IHostBuilder CreateHostBuilder(string[] args) =>

  Host.CreateDefaultBuilder(args)

  .ConfigureWebHostDefaults(webBuilder =>

    {

      webBuilder.UseStartup<Startup>();

  });

 

Um Konfigurationswerte von der Applikation zu lesen, wird der Config1 Key mit einem Wert in die Konfigurationsdatei appsettings.json (Listing 2) geschrieben.

 

Listing 2: appsettings.json

{

  "AppConfigurationSample": {

    "Settings": {

      "Config1": "value 1 from appsettings.json"

    }

  }

}

 

Für einen Konfigurationswert, der im Development Environment gelesen wird, wird der Konfigurationswert in der Datei appsettings.Development.json überschrieben. appsettings.{EnvironmentName}.json wird nach der Datei appsettings.json gelesen und damit werden im Development Environment die Konfigurationswerte, die in dieser Datei definiert sind, überschrieben (Listing 3).

 

Listing 3: appsettings.Development.json

{

  "AppConfigurationSample": {

    "Settings": {

      "Config1": "value 1 from appsettings.Development.json"

    }

  }

}

 

Um die Konfiguration im C#-Code zu lesen, wird die IndexAppSettings-Klasse definiert. Der Property-Name entspricht dem Key-Namen, um die Konfigurationswerte automatisch zu übernehmen (Listing 4).

 

Listing 4: IndexAppSettings.cs

public class IndexAppSettings

{

  public string? Config1 { get; set; }
}

 

Um die IndexAppSettings-Klasse mit dem IOptions-Interface injecten zu können, wird die IndexAppSettings-Klasse im Dependency-Injection-(DI-)Container konfiguriert (Listing 5).

 

Listing 5: Startup.cs

public void ConfigureServices(IServiceCollection services)

{

  services.Configure<IndexAppSettings>(

    Configuration.GetSection(

      "AppConfigurationSample:Settings"));

  services.AddRazorPages();

}

 

Mit Inject des generic IOptions Interfaces (Listing 6) werden die Konfigurationen in der Index-Page geladen.

Listing 6: Index.cshtml.cs

public class IndexModel : PageModel

{

  private readonly ILogger<IndexModel> _logger;

 

  public IndexModel(IOptions<IndexAppSettings> options,

    ILogger<IndexModel> logger)

  {

    _logger = logger;

    Setting1 = options.Value.Config1 ?? "no value configured";

  }

  public string Setting1 { get; init; }

}

 

Die Applikation ist bereit, die Konfigurationen aus irgendeiner Konfigurationsquelle zu laden. Mit Ändern eines Environments (Development, Staging, Production) können Konfigurationen auch entsprechend überschrieben werden. Um jetzt Azure App Configuration zu nutzen, sind nur kleine Anpassungen erforderlich.

 

Erstellen einer Azure App Configuration Resource

Ein Azure App Configuration Service kann über das Azure Portal [2], das Azure CLI, oder über PowerShell-Skripte erstellt werden. Hier wird gezeigt, wie die Azure CLI dafür genutzt werden kann. Im Sample-Source-Code-Repository gibt es ein Bash-Skript zum Erstellen dieser Resource.

Listing 7 zeigt die Azure CLI Commands. Mit dem Command az appconfig create wird die Azure App Configuration Resource erstellt. Der Command az appconfig kv set erstellt Key-Value-Paare.

 

Listing 7

rg=rg-appconfigsample

loc=westeurope

conf=configsample

key1=AppConfigurationSample:Settings:Config1

val1="configuration value for key 1 from Azure App Configuration"

devval1="configuration value for the Development environment"

 

az group create --location $loc --name $rg

az appconfig create --location $loc --name $conf --resource-group $rg

az appconfig kv set -n $conf --key $key1 --value "$val1" --yes

 

az appconfig kv set -n $conf --key $key1 --label Development --value "$devval1" –yes

 

Azure App Configuration mit .NET

Nachdem der Azure App Configuration Service erstellt wurde, braucht es jetzt nur kleine Anpassungen, um die .NET-Applikation [3] damit zu verknüpfen. Für ASP.NET-Core-Applikationen sollte das NuGet Package

Microsoft.Azure.AppConfiguration.AspNetCore hinzugefügt werden. Dieses Package hat eine Dependency auf Microsoft.Extensions.Configuration.AzureAppConfiguration, das für andere .NET-Applikationen reicht.

Alles was es jetzt braucht, um die Applikationskonfiguration aus dem Azure Service zu laden, ist es, den Azure Configuration Provider einzutragen. Um vom Development-System die Connection herzustellen, kann der Connection-String verwendet werden. Da der aber ein Secret beinhaltet, ist es am besten, diesen Connection-String nicht im Source-Code-Repository zu speichern, sondern mit den User Secrets zu speichern. User Secrets können über die .NET CLI konfiguriert werden (Listing 8).

 

Listing 8: dotnet user-secrets
> dotnet user-secrets init
> dotnet user-seccrets set AzureAppConfigurationConnection "enter your connection string"

 

Secrets sollten nie Teil des Source Codes sein. Mit User Secrets werden geheime Konfigurationen aus dem Userprofil gelesen. Das steht nur während der Entwicklung zur Verfügung. In Produktion müssen für Secrets andere Funktionalitäten genutzt werden, wie z. B. der Azure Key Vault.

Beim Set-up der Host-Klasse können weitere Configuration Provider mit der Methode ConfigureAppConfiguration (Listing 9) hinzugefügt werden. Mit dem Aufruf von config.Build werden die bisherigen Provider genutzt, um den Connection-String aus den User Secrets auszulesen. Der Connection-String wird der Extension-Methode AddAzureAppConfiguration mitgegeben und mit diesem Provider werden auch schon die Konfigurationen aus dem Azure Service gelesen.

 

Listing 9: Program.cs
public static IHostBuilder CreateHostBuilder(string[] args) =>
  Host.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((context, config) =>
      {
        var settings = config.Build();
        var connectionString = 
          settings["AzureAppConfigurationConnection"];
        config.AddAzureAppConfiguration(connectionString);
    })
    .ConfigureWebHostDefaults(webBuilder =>
      {
        webBuilder.UseStartup();
    });

 

Die User Secrets funktionieren jetzt auf dem Development-System. Um diese initiale Konfiguration jetzt auch direkt von einem Azure Service nutzten zu können, sollten hierfür Managed Identities genutzt werden.

 

Default Azure Credentials mit Azure App Configuration

Um in Produktion von einem Azure App Service oder einer Azure Function auf Azure App Configuration zugreifen zu können, können Managed Identities [4] genutzt werden. Ein Overload der AddAzureAppConfiguration-Methode (Listing 10) definiert den AzureAppConfigurationOptions-Parameter. Damit ist es möglich, die Connection zu Azure App Configuration mit einem Endpoint (der kein Secret beinhaltet) und einem TokenCredential-Objekt zu definieren. Für das Erzeugen des TokenCredential-Objekts kann die DefaultAzureCredential-Klasse verwendet werden (definiert im NuGet Package Azure.Identity).

DefaultAzureCredential verwendet unterschiedliche Credential Types. Der ManagedIdentityCredential Type kann verwendet werden, wenn die Applikation in einem Azure App Service konfiguriert wird. DefaultAzureCredential verwendet aber auch VisualStudioCredential, VisualStudioCodeCredential und AzureCliCredential. Der Account, der mit Visual Studio verwendet wird, um auf Azure Services zuzugreifen, benötigt hierfür unter Azure App Configuration eine Zuweisung zur Rolle App Configuration Data Reader. Damit kann der gleiche Code im Development Environment und in Produktion genutzt werden.

 

Listing 10
.ConfigureAppConfiguration((context, config) =>
{
  var settings = config.Build();

  config.AddAzureAppConfiguration(options =>
  {
    DefaultAzureCredential credential = new();
    var endpoint = settings["AzureAppConfigurationEndpoint"];
    options.Connect(new Uri(endpoint), credential);
  });
})

 

Environments mit Azure App Configuration

Mit .NET können Konfigurationen je nach Environment überschrieben werden, z. B. im Production, Staging und Developer Environment. Bei Azure App Configuration gibt es Labels, die für das Mapping zu den Environments verwendet werden können, wie in der nächsten Version der Applikation [5] gezeigt wird.

Beim Erstellen des Azure App Configuration Services wurde ein Konfigurationswert mit dem Label Development erstellt. Um die Funktionalität auf die .NET Environments zu übertragen, kann die Select-Methode der AzureAppConfigurationOptions-Klasse verwendet werden. Mit dem ersten Parameter dieser Methode kann ein Key-Filter spezifiziert, um nicht alle Konfigurationswerte zu laden, sondern nur die Werte, die dem Filter entsprechen. Im Sample Code (Listing 11) werden alle Keys geladen, die mit dem String AppConfigurationSample: starten.

Wenn der Azure App Configuration Service Konfigurationen für mehrere Applikationen beinhaltet, ergibt es Sinn, diesen Filter zu definieren, um nicht alle Konfigurationen zu laden. Mit dem zweiten Parameter wird ein Labelfilter spezifiziert. Der erste Aufruf der Filter-Methode verwendet die vordefinierte Null-Property der LabelFilter-Klasse. Damit werden alle Konfigurationswerte ohne Label geladen. Mit dem zweiten Aufruf der Select-Methode wird ein Labelfilter mit dem Namen des Environments genutzt. Die Konfigurationen mit einem entsprechenden Label überschreiben die Konfigurationswerte ohne Label. Bei ASP.NET-Core-Applikationen wird der Name des Environments über die ASPNETCORE_ENVIRONMENT-Environment-Variable gesetzt.

 

Listing 11: Program.cs
config.AddAzureAppConfiguration(options =>
  {
    DefaultAzureCredential credential = new();
    var endpoint = settings["AzureAppConfigurationEndpoint"];
    options.Connect(new Uri(endpoint), credential)
      .Select("AppConfigurationSample:*", LabelFilter.Null)
      .Select("AppConfigurationSample:*", context.HostingEnvironment.EnvironmentName);
});

 

Development, Staging und Production sind übliche Environment-Namen, in denen unterschiedliche Konfigurationswerte, z. B. unterschiedliche Datenbank-Connection-Strings genutzt werden können. Mit einem Cloud-Environment ist es sinnvoll, das Developer-Environment über unterschiedliche Subscriptions komplett von der Produktion zu trennen. Damit ist es sinnvoll, im Development-Environment einen eigenen Azure App Configuration Service zu nutzen. In der Production Subscription ist es wiederum sinnvoll, zumindest zwischen Staging und Production Environments zu trennen – um die letzten Tests vor dem Switch in Produktion im Staging Environment durchzuführen.

 

Dynamische Konfiguration

Um nach Änderungen von Konfigurationswerten einen App Service nicht restarten zu müssen, können Konfigurationen in der laufenden Applikation neu geladen werden. Dafür sind aber einige Änderungen erforderlich [6].

Bei der Konfiguration des Azure App Configuration Providers muss die ConfigureRefresh-Methode der AzureAppConfigurationOptions-Klasse aufgerufen werden. Der Delegate-Parameter bei dieser Methode erlaubt es, die AzureAppConfigurationRefreshOptions zu konfigurieren. Mit der Register-Methode (Listing 12) wird ein Key registriert und mit den Änderungen verglichen, bevor alle Konfigurationswerte neu geladen werden.

Hier ergibt es Sinn, einen Key (Sentinel) zu definieren, bei dem der Wert geändert wird, sobald irgendeine Konfiguration der Applikation geändert wird. Mit diesem Key wird verglichen und Werte im Memory werden neu geladen, sobald dieser Wert nach der Cache Expiration geändert wird. Die Zeitdauer für die Cache Expiration wird über die Methode SetCacheExpiration definiert.

Wird diese z. B. auf fünf Minuten gesetzt, wird der Sentinel-Wert alle 5 Minuten in Azure App Configuration geladen. Ist der Wert anders als im letzten Request, werden alle Konfigurationswerte der Applikation (definiert über die Select-Methode) neu geladen. Im Code-Sample wird die Expiration-Dauer auf 30 Tage gesetzt, womit der Cache explizit neu geladen werden muss. Ein explizites Laden kann über das Interface IConfigurationRefresher gesteuert werden.

 

Listing 12
.ConfigureRefresh(refreshConfig =>
  {
    refreshConfig.Register(
      "AppConfigurationSample:Sentinel", refreshAll: true)
        .SetCacheExpiration(TimeSpan.FromDays(30));
});

 

Weitere notwendige Änderungen für das Aktivieren der Refresh-Funktionalität sind die Konfiguration des DI-Containers über die Methode AddAzureAppConfiguration und die Konfiguration der Middleware mit der Methode UseAzureAppConfiguration (Startup-Klasse).

Statt des IOptions-Interface muss auch das IOptionsSnapshot-Interface injectet werden. Beim IOptions-Interface wird ein Cache der Konfiguration gehalten, und damit gibt es kein Update bei weiteren Aufrufen. Das IOptionsSnapshot-Interface hält einen Snapshot der Daten, die nach jedem Injecten aus dem Azure App Configuration Provider Cache geladen werden.

Um den Cache jetzt upzudaten, kann über Azure App Configuration ein Event Grid Event registriert werden, das aktiviert wird, sobald ein Key-Value-Wert modifiziert wird. Von dort ist es möglich, unterschiedliche Resources wie Azure Functions, Logic Apps, oder WebHooks anzustoßen.

In der Sample-Applikation wird in der RefreshSettings-Page (Listing 13) das IConfigurationRefreshProvider-Interface injectet, um die Azure App Configuration mit den Aufrufen SetDirty und TryRefreshAsync neu zu laden.

 

Listing 13: RefreshSettings.cshtml.cs
public class RefreshSettingsModel : PageModel
{
  private readonly IConfigurationRefresher _configurationRefresher;
  public RefreshSettingsModel(IConfigurationRefresherProvider refresherProvider)
  {
    _configurationRefresher = refresherProvider.Refreshers.First();
  }
  public async Task OnGet()
  {
    _configurationRefresher.SetDirty(TimeSpan.FromSeconds(1));
    await Task.Delay(1000);
    bool success = await _configurationRefresher.TryRefreshAsync();
  }
}

 

Konfiguration von Secrets

Für die Konfiguration der Secrets bietet Azure Key Vault Optionen, die von Azure App Configuration nicht zur Verfügung gestellt werden. Azure Key Vault bietet in der Standardvariante Software-Protection über unterschiedliche Rollen, in der Premiumvariante auch Hardware-Protection mit dedizierten Hardware-Security-Modulen.

Azure Key Vault [7] kann über einen eigenen Provider in der .NET-Konfiguration eingetragen werden. Alternativ ist es auch möglich, in Azure App Configuration eine Referenz auf den Azure Key Vault zu registrieren. Dabei ist es in der Applikation [8] nicht erforderlich, eine Connection zu Azure Key Vault zu konfigurieren – die Konfiguration zur Azure App Configuration reicht dafür.

Mit der ConfigureKeyVault-Methode wird konfiguriert, dass der Azure Key Vault auch über Azure App Configuration genutzt wird. Dabei ist es erforderlich, die Credentials mitzugeben, um vom Key Vault lesen zu können. Die Credentials, die über DefaultAzureCredential erzeugt werden, können auch hier mitgegeben werden – Voraussetzung ist, dass diese Credentials nicht nur Zugriff auf die Azure App Configuration sondern auch Lesezugriff auf die Secrets im Azure Key Vault haben.

 

Fazit

Der Azure App Configuration Service bietet eine zentrale Konfiguration. Um diesen Service mit .NET-Anwendungen zu verwenden, ist es nur erforderlich einen Configuration Provider hinzuzufügen. Azure App Configuration kann damit leicht in bestehende .NET-Anwendungen integriert werden. Für unterschiedliche Konfigurationen je nach Environment können mit Azure App Configuration Labels verwendet werden, die sich einfach in das .NET-Environment-Modell integrieren lassen.

Um diesen Service von Azure Services zu verwenden, bieten sich Managed Identities mit Role-based Security an. Um den Service auch vom Development-System verwenden zu können, eignet sich die DefaultAzureCredential-Klasse, die sowohl Managed Identities als auch Visual Studio Credentials unterstützt. Für ein dynamisches Update der Konfigurationswerte ist mit einem Refresh-Modell gesorgt – und Konfigurationen, die Secrets beinhalten, können über den Azure Key Vault von Azure App Configuration integriert werden.

 

Links & Literatur

[1] https://github.com/ProfessionalCSharp/MoreSamples/Azure/AppConfig/01-ConfigSample

[2] https://portal.azure.com

[3] https://github.com/ProfessionalCSharp/MoreSamples/Azure/AppConfig/02-ConfigSample

[4] https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview

[5] https://github.com/ProfessionalCSharp/MoreSamples/Azure/AppConfig/04-ConfigSample

[6] https://github.com/ProfessionalCSharp/MoreSamples/Azure/AppConfig/05-ConfigSample

[7] https://docs.microsoft.com/en-us/azure/key-vault/general/basic-concepts

[8] https://github.com/ProfessionalCSharp/MoreSamples/Azure/AppConfig/06-ConfigSample

Ihr aktueller Zugang zur .NET- und Microsoft-Welt.
Der BASTA! Newsletter:

Behind the Tracks

.NET Framework & C#
Visual Studio, TFS, C# & mehr

Agile & DevOps
Best Practices & mehr

Web Development
Alle Wege führen ins Web

Data Access & Storage
Alles rund um´s Thema Data

HTML5 & JavaScript
Leichtegewichtig entwickeln

User Interface
Alles rund um UI- und UX-Aspekte

Microservices & APIs
Services, die sich über APIs via REST und JavaScript nutzen lassen

Security
Tools und Methoden für sicherere Applikationen

Cloud & Azure
Cloud-basierte & Native Apps