Azure App Configuration with Key Vault and Managed Identity in HA

Goal: Production-ready Azure App Configuration setup with Key Vault secrets, Managed Identity authentication, and High Availability.

How the Client Works

Azure App Configuration client uses the sentinel pattern - a single key that determines if a full configuration refresh is needed.

On Refresh():

  1. If cache is valid → does nothing
  2. If cache expired → fetches sentinel key
  3. If sentinel unchanged → does nothing
  4. If sentinel changed → refreshes all configuration
  5. On failure → attempts failover App Configuration (typically different region)

Important: Update the sentinel whenever you change any parameter to trigger a client refresh.

Azure Setup

Create API app with Managed Identity:

Create Key Vault and grant API app identity read access to secrets.

Create App Configuration service and add API app identity with role App Configuration Data Reader in Access Control (IAM).

Note: App Configuration only stores Key Vault references, not values. Your app retrieves the actual secret from Key Vault.

Create Parameters

Use JSON-like structure: [ApplicationName]:[ParamName]

Bustroker.AzureAppConfiguration.WebApi:Sentinel => 0
Bustroker.AzureAppConfiguration.WebApi:CsvValues => Mr Orange,Mr Pink,Mr Brown
Bustroker.AzureAppConfiguration.WebApi:IntegerValue => 69
Bustroker.AzureAppConfiguration.WebApi:SecretInKeyvault => (Key Vault Reference)

For the Key Vault secret, select “Key Vault Reference” instead of “Key-value” when creating it.

Implementation

Install package:

dotnet add package Microsoft.Azure.AppConfiguration.AspNetCore

Configure in 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>("AzAppConfigurationFailOverRegionUrl");

                    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>();
        });

Failover: First provider is failover, second is primary (docs). For non-HA, configure only one with optional: false.

Local development: DefaultAzureCredential requires local credentials (docs).

Register configuration in Startup.ConfigureServices:

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

Use in controller:

[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);
    }
}

Critical: Use IOptionsSnapshot<T>, not IOptions<T> (no refresh without restart) or IOptionsMonitor<T> (values can change mid-request). Details.