Azure App Configuration with Key Vault and Managed Identity in HA

Here’s what Azure App Configuration Service is.

And I want a production ready setup:

How Azure App Configuration client works

Here’s how the client works, using the sentinel pattern, i.e., using one key to be the one that the client actually checks to know whether or not a full configuration refresh is needed. When Refresh methods are called, the client:

So, every time a parameter is changed in the configuration, sentinel must be changed as well so the client knows that it needs to do a full refresh.

Configure API, Key Vault and App Configuration access in Azure

It’s relevant that the App Configuration services does not keep key vault values or accesses key vault in any way. It just keeps the reference to the parameter, so what we’ll configure next is the application configuration provider to be able to go to the key vault and actually retrieve the parameter value, after the name is received from the App Configuration.

Create the params in App Configuration

I like to simulate a json structure, as one normally comes from appsettings.json. So I prefix with application name, and observe the format [ApplicationName]:[ParamName] It’s also usefull when more than one application consume the App Configuration service.

The params in our case would be (let’s create various types/options):

Now the code

First, add the nuget package

dotnet add package Microsoft.Azure.AppConfiguration.AspNetCore

Then it goes like this.

// Program.cs
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder
                .ConfigureAppConfiguration((hostingContext, config) =>
                    {
                        var settings = config.Build();
                        var azAppConfigurationMainRegionUrl = settings.GetValue<string>("AzAppConfigurationMainRegionUrl");
                        var azAppConfigurationFailOverRegionUrl = settings.GetValue<string>("AzAppConfigurationMainRegionUrl");

                        config
                            .AddAzureAppConfiguration(options => 
                            {
                                options.Connect(new Uri(azAppConfigurationFailOverRegionUrl), new DefaultAzureCredential())
                                    .ConfigureKeyVault(kv => 
                                    {
                                        kv.SetCredential(new DefaultAzureCredential());
                                    })
                                    .ConfigureRefresh(refreshOptions =>
                                    {
                                        refreshOptions.Register("Bustroker.AzureAppConfiguration.WebApi:sentinel", refreshAll: true)
                                                .SetCacheExpiration(TimeSpan.FromSeconds(30));
                                    });
                            }, optional: true)
                            .AddAzureAppConfiguration(options => 
                            {
                                options.Connect(new Uri(azAppConfigurationMainRegionUrl), new DefaultAzureCredential())
                                    .ConfigureKeyVault(kv => 
                                    {
                                        kv.SetCredential(new DefaultAzureCredential());
                                    })
                                    .ConfigureRefresh(refreshOptions =>
                                    {
                                        refreshOptions.Register("Bustroker.AzureAppConfiguration.WebApi:sentinel", refreshAll: true)
                                                .SetCacheExpiration(TimeSpan.FromSeconds(1));
                                    });
                            }, optional: true);
                    })
                .UseStartup<Startup>();
        });

Note the ConfigureKeyVault(kv => ..) method.

DefaultAzureCredential class is the one responsible for getting the credentials available through MI. Note: for DefaultAzureCredential class to work locally, the credentials need to be provided in the local environment. See here

Register a configuration section so it’s bound against TOptions, and IAzureAppConfigurationRefresher service

// Startup.ConfigureServices(...)
services.Configure<AppSettings>(Configuration.GetSection("Bustroker.AzureAppConfiguration.WebApi"));
services.AddScoped<IAzureAppConfigurationRefresher, OnDemandAzureAppConfigurationRefresher>();

Using Configuration values

To use the configuration values, inject IOptionsSnapshot into the Controller.

// AzAppConfigurationController.cs
[ApiController]
[Route("[controller]")]
public class AzAppConfigurationController : ControllerBase
{
    private readonly AppSettings _appSettings;

    public AzAppConfigurationController(IOptionsSnapshot<AppSettings> appSettings)
    {
        _appSettings = appSettings.Value;
    }

    [HttpGet]
    public async Task<IActionResult> Get()
    {
        await Task.CompletedTask;
        return Ok(_appSettings);
    }
}