Eshopworld.DevOps package
Summary
DevOps extensions, used for application configuration setup.
Load configurations
Below is an example of loading various config sources using the UseDefaultConfigs
method. This will load the following configs (in the order shown):
- Environment Variables
- Command Line
-
appSettings.json
file - environment specific
appSettings.json
likeappSettings.CI.json
- region specific
appSettings.json
likeappSettings.CI.WE.json
public class Program
{
...
public void ConfigureAppConfiguration(IConfigurationBuilder builder)
{
// Load config sources by calling the UseDefaultConfigs.
builder.UseDefaultConfigs();
// Load other config here...
}
...
}
Load individual secrets from a single Key Vault
Individual secrets can be loaded into configuration, directly from Key Vault in the following way:
public class Program
{
...
public void ConfigureAppConfiguration(IConfigurationBuilder builder)
{
// Load various config sources.
builder.UseDefaultConfigs();
// Pass the name of the secrets you wish to load into the configuration builder.
builder.AddKeyVaultSecrets("TenantId",
"SubscriptionId",
"OtherSecretName");
}
...
}
To use this method, the code expects a setting called "KEYVAULT_URL". This will typically have been setup by DevOps as an environment variable on the machine running the code. If we take a key vault instance called example1, the environment setting would look like this:
https://example1.vault.azure.net/
Otherwise, if the KEYVAULT_URL
setting cannot be found, the method will fallback to a config setting called "KeyVaultInstanceName". This can be set in your AppSettings.json
file. It would be look like example1
and the url will be inferred automatically.
AppSettings.json
{
"KeyVaultInstanceName": "example1",
"SomeOtherSetting1": 100,
"SomeOtherSetting2": false
}
Load individual secrets from multiple key vaults
If you require settings to be loaded from multiple Key Vaults, it can be done in the following way:
public class Program
{
...
public void ConfigureAppConfiguration(IConfigurationBuilder builder)
{
// Load from all config sources.
builder.UseDefaultConfigs();
// Overload method 1: Add from key vault loaded in KEYVAULT_URL setting.
builder.AddKeyVaultSecrets("SomeKey1", "SomeKey2");
var kvUriInstance1 = new Uri("https://instance1.vault.azure.net");
var kvUriInstance2 = new Uri("https://instance2.vault.azure.net");
// Overload method 2: Pass the instance and list of the secrets you wish to load into configuration.
builder.AddKeyVaultSecrets(kvUriInstance1, new [] {
"TenantId",
"SubscriptionId",
"OtherSecretName",
"connectionstrings--servicebus" });
builder.AddKeyVaultSecrets(kvUriInstance2, new [] {
"OtherKey1",
"OtherKey2",
"OtherKey3" });
}
...
}
Load secrets from Key Vault and map secret names to other config value names
For convenience, you can choose to map your Key Vault secrets to other config names by passing a Dictionary (key is secret name, value is mapped name) instead of array of string (secret names), as follows:
public class Program
{
...
public void ConfigureAppConfiguration(IConfigurationBuilder builder)
{
// Load various config sources.
builder.UseDefaultConfigs();
// Pass the name of the secrets you wish to load into the configuration builder.
builder.AddKeyVaultSecrets(new Dictionary<string, string> {
{ "TenandId","MyClass:MyTenantId" },
{ "SubscriptionId","MyClass:SubClass1:MySubId" },
{ "OtherSecretName","OtherSecretName" }
});
// Optional overload - you can pass the specific KV url if needed.
}
...
}
Using the loaded configuration
Take this POCO class:
public class AppSettings {
public string TenantId { get; set; }
public string SubscriptionId { get; set; }
public string OtherSecretName { get; set; }
public ConnStrings ConnectionStrings { get; set; }
}
public class ConnStrings {
public string ServiceBus { get; set; }
public string Cosmos { get; set; }
}
We can bind the settings to this class using the BindBaseSection
call as follows:
// Taken from Startup.cs after the ConfigureAppConfiguration above has been run.
public void ConfigureServices(IServiceCollection services)
{
// Example of binding settings directly to a class (without the "GetSection" call).
var appSettings = _configuration.BindBaseSection<AppSettings>();
// Example of directly using the settings directly after they are loaded.
var tenantId = _configuration["TenantId"];
var otherSecret = null;
if (_configuration.TryGetValue<string>("OtherSecretName"), out otherSecret)
{
... do something conditional if the setting exists ...
}
}
It's worth noting, if you have a Key Vault setting called with dashes in the name, ex "My-Key-1", it can bind to a Poco class property called "MyKey1". The BindBaseSection
method will make a version of the config setting that has not got the dashes.
Bind to a class specifying Key Vault mappings
Take this POCO class:
public class CustomSettings {
[KeyVaultSecretName("my-kv-key")]
public string CosmosKey { get; set; }
[KeyVaultSecretName("my-kv-url")]
public string CosmosUrl { get; set; }
public string DbConnectionString { get; set; }
}
We can bind two of the properties of this class to Key Vault secrets using the BindSection
call as follows.
The third will be taken directly from the specified section.
// Taken from Startup.cs after the ConfigureAppConfiguration above has been run.
public void ConfigureServices(IServiceCollection services)
{
// Example of binding settings directly to a class (without the "GetSection" call).
var appSettings = _configuration.BindSection<CustomSettings>("MySection");
}
If the section is not specified then it will attempt to load from KeyVault only
public void ConfigureServices(IServiceCollection services)
{
// Example of binding settings directly to a class (without the "GetSection" call).
var appSettings = _configuration.BindSection<CustomSettings>();
}
If you do not have control of the POCO that is being bound to, then additional mappings can be specified
public void ConfigureServices(IServiceCollection services)
{
// Example of binding settings directly to a class (without the "GetSection" call).
var appSettings = _configuration.BindSection<CustomSettings>("MySection",
c => c.AddMapping(c => c.DbConnectionString, "db-connectionstring"));
}
This approach does not require initial Key Vault loading using AddKeyVaultSecrets
Full example of code above using WebHostBuilder
Example of using with a WebHostBuilder when bootstrapping a Web Application.
public class Program
{
public static void Main(string[] args)
{
try
{
// Build and run the web host.
var host = CreateWebHostBuilder(args).Build().Run();
}
catch (Exception e)
{
// Probably want to log this using BigBrother (there's a bit of a
// race condition here as BB might not be wired up yet!).
// Catch startup errors and bare minimum log to console or event log.
Console.WriteLine($"Problem occured during startup of {Assembly.GetExecutingAssembly().GetName().Name}");
Console.WriteLine(e);
// Stop the application by continuing to throw the exception.
throw;
}
}
// In program, we setup our configuration in the standard microsoft way...
private static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration(config => {
// Import default configurations (env vars, command line args, appSettings.json etc).
config.UseDefaultConfigs();
// Load config from key vault.
config.AddKeyVaultSecrets("TenantId",
"SubscriptionId",
"OtherSecretName",
"connectionstrings--cosmos",
"connectionstrings--servicebus");
})
.ConfigureLogging((context, logging) => {
// Add logging configuration and loggers.
logging.AddConfiguration(context.Configuration).AddConsole().AddDebug();
})
.UseStartup<Startup>();
...
}
public class Startup
{
private readonly ILogger<Startup> _logger;
private readonly IConfiguration _configuration;
// We can then grab IConfiguration from the constructor, to use in our startup file as follows:
public Startup(IConfiguration configuration, ILogger<Startup> logger)
{
_configuration = configuration;
_logger = logger;
}
public void ConfigureServices(IServiceCollection services)
{
// You could bind directly to a poco class of your choice.
_appSettings = _configuration.BindBaseSection<AppSettings>();
// Other setting bindings...
var bb = BigBrother.CreateDefault(_telemetrySettings.InstrumentationKey, _telemetrySettings.InternalKey);
_configuration.GetSection("Telemetry").Bind(_telemetrySettings);
_configuration.GetSection("HttpCors").Bind(_corsSettings);
_configuration.GetSection("RefreshingTokenProviderSettings").Bind(_refreshingTokenProviderOptions);
_configuration.GetSection("Endpoints").Bind(_endpoints);
}
...
}
How to access this package
All of the eshopworld.* packages are published to a public NuGet feed. To consume this on your local development machine, please add the following feed to your feed sources in Visual Studio: https://eshopworld.myget.org/F/github-dev/api/v3/index.json
For help setting up packages, follow this article: https://docs.microsoft.com/en-us/vsts/package/nuget/consume?view=vsts