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:
- use it to serve secret params as well, keeping them in Key Vault
- access it with Managed Identity(MI).
- have it in High Availability
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:
- checks whether cache is expired. If it’s not, it does nothing and keeps using the values in cache
- if cache is expired, client will go ahead and get ‘sentinel’ app configuration parameter.
- if sentinel hasn’t changed from last time, it does nothing and keeps using the values in cache
- if sentinel has changed, it will go and refresh all configuration parameters.
- when making a request to the App Configuration service if it fails, then an attempt with the Fail over App Configuration configured is attempted. The fail over service is typically located in a different azure region.
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
- Create an api app and add a Systems Managed Identity
- Create a Key Vault and add a policy so the Api App identity can read secrets
- Create an App Configuration. In Access Control(IAM) add the Api App identity with the role App Configuration Data Reader.
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):
- Bustroker.AzureAppConfiguration.WebApi:Sentinel => 0
- Bustroker.AzureAppConfiguration.WebApi:CsvValues => Mr Orange,Mr Pink,Mr Brown
- Bustroker.AzureAppConfiguration.WebApi:IntegerValue => 69
- Bustroker.AzureAppConfiguration.WebApi:SecretInKeyvault => The value is kept in key vault. When creating it, just select “Key Vault Reference”, rather than “Key-value”.
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.
- AddAzureAppConfiguration(..): adds a configuration provider for Az App Configuration service to the API, i.e., a source to populate IConfiguration from Az App Configuration. Two are configured, one for failover (yes, the first one is the failover one ¿?… see here), both optional. In case of no HA desired, only configure one, preferably with optional: false.
- Connect(..): configure actual connection to az AppConfiguration, in this case with Managed Id
- ConfigureKeyVault(..): configure keyvault so values in Az App Configuration that are in Key Vault can be accessed by the application
- ConfigureRefresh(..): configure refresh of the cache of the App Configuration Client, as explained before.
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);
}
}
- NOTE: it’s important to use IOptionsSnapshot, and not IOptions (wouldn’t refresh anyway unless restarted the application), nor IOptionsMonitor (could change the values in the middle of a request potentially leading to weird results). Check here.