Eshopworld.DevOps

eShopWorld DevOps SDK artifacts


Keywords
configuration, devops, tooling
License
MIT
Install
Install-Package Eshopworld.DevOps -Version 5.1.11

Documentation

Eshopworld.DevOps package

Build status Coverage

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 like appSettings.CI.json
  • region specific appSettings.json like appSettings.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