Azure Storage UseDevelopmentStorage=true when using KeyVault - azure

I have configured my API to get the Azure Storage connection string from Azure KeyVault using Managed Identity.
Now the problem is that when I run the code locally in Visual Studio, it no longer uses the connection string from the appsettings.development.json which was "StorageAccount": "UseDevelopmentStorage=true"
Therefore I can't use the emulator when running locally.
I have created the following condition in my controller to work around this issue :
public FileController(IConfiguration configuration, IWebHostEnvironment env)
{
this.configuration = configuration;
if (env.IsDevelopment())
{
conn = this.configuration.GetConnectionString("StorageAccount");
}
else
{
conn = this.configuration["StorageConnectionString"];
}
}
Is this the proper way to do it ?

In local, if your ASPNETCORE_ENVIRONMENT set to Development, then it will read you local storage account like UseDevelopmentStorage=true.
When you publish to azure, it will use your webapp's MSI to get the connectionstring from key vault.
For more details, you could refer to the following code:
private IConfiguration _configuration;
private IWebHostEnvironment _env;
public WeatherForecastController(IConfiguration configuration, IWebHostEnvironment env)
{
_configuration = configuration;
_env = env;
}
if (_env.IsDevelopment())
{
con = _configuration.GetSection("StorageAccount").Value;
}
else
{
AzureServiceTokenProvider azureServiceTokenProvider = new AzureServiceTokenProvider();
var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));
con = keyVaultClient.GetSecretAsync("https://xxxx.vault.azure.net/secrets/xxxx").GetAwaiter().GetResult().Value;
}

Related

How to retrieve Azure App Configuration Service settings while in Program.cs

I am using Asp.NET Core 5 (v5.0.6) web API deployed as an Azure App Service and I am migrating from using appsettings.json in the API to integrating with Azure App Configuration service. I have the Azure App Configuration Service set up but the issue I am having is how to access values in my Azure App Configuration service for retrieving a connection string for database access while I am still in Program.cs.
Note that in the CreateHostBuilder() method I am examining the context.HostingEnvironment.IsDevelopment() environment variable and if it is "IsDevelopment", implying a local DEV, I am reading an Azure App Configuration Service connection string via User Secrets but if it is not a local DEV, than I rely on the Managed Identity and just pass in the endpoint value from appsettings.json.
The only values I want to get that are not in Azure App Configuration Service is the local DEV Azure App Configuration Service Connection string (from User Secrets) and the Azure App Configuration Service endpoint from Appsettings.json. All other settings should come from Azure App Configuration Service.
The problem I am trying to solve is how to access the values in Azure App Configuration Service, while still in Program.cs, to retrieve the connection string for access to the Azure SQL database I am using for logging.
In the code below, when I link the Azure App Configuration in the CreateHostBuilderMethod and call build, I expected the values in Azure App Configuration Service to then be available via the static Configuration property. However when I try to retrieve the connection string value, it is always null.
How can I correctly retrieve the values for properties in Azure App Configuration Service to use them in Program.cs?
Here is my Program.cs file;
public class Program
{
public static IConfiguration Configuration { get; } = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true)
.AddUserSecrets<Startup>()
.Build();
public static void Main(string[] args)
{
var host = CreateHostBuilder(args)
.Build();
var connectionString = Configuration["CoreApi:Settings:LoggingDb"]; //<-- This returns null
const string tableName = "ApiLogs";
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.Filter.ByExcluding(Matching.FromSource("Microsoft.EntityFrameworkCore.Query"))
.WriteTo.MSSqlServer(
connectionString: connectionString,
sinkOptions: new MSSqlServerSinkOptions { TableName = tableName })
.CreateLogger();
// TODO Enable to debug any startup Serilog issues. Make sure to comment out for PROD
// Serilog.Debugging.SelfLog.Enable(msg =>
// {
// Debug.Print(msg);
// Debugger.Break();
// });
//var host = CreateHostBuilder(args)
// .Build();
try
{
Log.Information("Starting ConfirmDelivery API");
host.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "ConfirmDelivery API Host terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
var settings = config.Build();
if (context.HostingEnvironment.IsDevelopment())
{
var connectionString = settings.GetConnectionString("AzureAppConfiguration");
config.AddAzureAppConfiguration(connectionString);
}
else
{
var endpoint = settings["AppConfigEndpoint"];
var credentials = new ManagedIdentityCredential();
config.AddAzureAppConfiguration(options =>
{
options.Connect(new Uri(endpoint), credentials);
});
}
}).ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
Try the code below to get a config value:
using System;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Configuration.AzureAppConfiguration;
namespace myapp
{
class Program
{
static void Main(string[] args)
{
var configItemName = "";
var appConfigConnectionString = "";
var builder = new ConfigurationBuilder();
builder.AddAzureAppConfiguration(appConfigConnectionString);
var config = builder.Build();
Console.WriteLine(config[configItemName]);
}
}
}
Result :

Net core Key vault configuration using Azure.Security.KeyVault.Secrets

I have found out it is easy to connect to Azure KeyVault using Managed Identity. The documentation shows how to do it :
var azureServiceTokenProvider = new AzureServiceTokenProvider();
var keyVaultClient = new KeyVaultClient(
new KeyVaultClient.AuthenticationCallback(
azureServiceTokenProvider.KeyVaultTokenCallback));
config.AddAzureKeyVault(
$"https://{builtConfig["KeyVaultName"]}.vault.azure.net/",
keyVaultClient,
new DefaultKeyVaultSecretManager());
Then I realized it requires the package Microsoft.Azure.KeyVault which is deprecated. So I'm struggling to figure out how to do the above with SDK 4. All the documentation I find is related to SDK 3.
[EDIT]
I have found out the following code works to get the azure KeyVault Secret using Managed Identiy with SDK 4. However I can't see how to add this to my configuration. It used to be done with config.AddAzureKeyVault() from the Microsoft.Extensions.Configuration.AzureKeyVault Package however it is not compatible with the SDK 4 SecretClient:
return Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
var azureCredentialOptions = new DefaultAzureCredentialOptions();
var credential = new DefaultAzureCredential(azureCredentialOptions);
var secretClient = new SecretClient(new System.Uri("https://mykeyvault.vault.azure.net/"), credential);
var secret = secretClient.GetSecret("StorageConnectionString");
config.AddAzureKeyVault()
})
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
As it turned out, I found the proper way to do it with SDK 4. I had to install the package azure.extensions.aspnetcore.configuration.secrets and then the code is simply :
var credential = new DefaultAzureCredential();
config.AddAzureKeyVault(new System.Uri("https://mykv.vault.azure.net/"), credential);
then to use it
configuration["StorageConnectionString"]
AS per June 2020
First thing is that Microsoft.Azure.KeyVault is not deprecated but replaced. Using the old nuget package is still a valid option.
I imagine in the future, the Microsoft.Extensions.Configuration.AzureKeyVault nuget package will use the new Azure.Security.KeyVault.Secrets package.
In my experience I would stick with the existing library and wait for future updates.
If you really want to use the Azure.Security.KeyVault.Secrets, you can implement your own custom configuration builder.
I had a look at the existing key vault configuration code on github and here is a simplified/modified version that you could use.
First install these nuget packages Azure.Identity and Azure.Security.KeyVault.Secrets.
The new key vault secrets package uses IAsyncEnumerable so you need to update your project to target C#8.0: update you csproj file with <LangVersion>8.0</LangVersion>.
Azure Key Vault Secret configuration code:
public interface IKeyVaultSecretManager
{
bool ShouldLoad(SecretProperties secret);
string GetKey(KeyVaultSecret secret);
}
public class DefaultKeyVaultSecretManager : IKeyVaultSecretManager
{
public bool ShouldLoad(SecretProperties secret) => true;
public string GetKey(KeyVaultSecret secret)
=> secret.Name.Replace("--", ConfigurationPath.KeyDelimiter);
}
public class AzureKeyVaultConfigurationProvider : ConfigurationProvider
{
private readonly SecretClient _client;
private readonly IKeyVaultSecretManager _manager;
public AzureKeyVaultConfigurationProvider(SecretClient client, IKeyVaultSecretManager manager)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
_manager = manager ?? throw new ArgumentNullException(nameof(manager));
}
public override void Load() => LoadAsync().ConfigureAwait(false).GetAwaiter().GetResult();
private async Task LoadAsync()
{
var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
await foreach (var secretProperties in _client.GetPropertiesOfSecretsAsync())
{
if (!_manager.ShouldLoad(secretProperties) || secretProperties?.Enabled != true)
continue;
var secret = await _client.GetSecretAsync(secretProperties.Name).ConfigureAwait(false);
var key = _manager.GetKey(secret.Value);
Data.Add(key, secret.Value.Value);
}
Data = data;
}
}
public class AzureKeyVaultConfigurationSource : IConfigurationSource
{
public SecretClient Client { get; set; }
public IKeyVaultSecretManager Manager { get; set; }
public IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new AzureKeyVaultConfigurationProvider(Client, Manager);
}
}
public static class AzureKeyVaultConfigurationExtensions
{
public static IConfigurationBuilder AddAzureKeyVault(
this IConfigurationBuilder configurationBuilder,
SecretClient client,
IKeyVaultSecretManager manager = null)
{
if (configurationBuilder == null)
throw new ArgumentNullException(nameof(configurationBuilder));
if (client == null)
throw new ArgumentNullException(nameof(client));
configurationBuilder.Add(new AzureKeyVaultConfigurationSource()
{
Client = client,
Manager = manager ?? new DefaultKeyVaultSecretManager()
});
return configurationBuilder;
}
}
You can now use this configuration builder in your project like that:
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((context, config) =>
{
var azureCredentialOptions = new DefaultAzureCredentialOptions();
var credential = new DefaultAzureCredential(azureCredentialOptions);
var secretClient = new SecretClient(new System.Uri("https://mykeyvault.vault.azure.net/"), credential);
config.AddAzureKeyVault(secretClient);
})
.UseStartup<Startup>();
}
For your info, if you're using Azure Key Vault secrets in your App Service or Azure Functions application settings, you don't have to add extra code to get the key vault value.
You just need to change your app settings values (in azure portal), with your key vault references.
For the steps, look at https://learn.microsoft.com/en-us/azure/app-service/app-service-key-vault-references
Setting example using key vault references:
{
"name": "DatabaseSettings:ConnectionString",
"value": "#Microsoft.KeyVault(SecretUri=https://myvault.vault.azure.net/secrets/DatabaseConnectionSettingSecret/ec96f02080254fxxxxxxxxxxxxxxx)",
"slotSetting": false
}
But this doesn't work for a local development, instead you should use plain secret value. But for me it's okay, because your local development uses different secret value and you don't add your local.settings.json to source control.
Latest working solution with version 4 library. My stack is .netcore 3.1 and I am using it inside an Azure Web App to access a secret from Azure KeyVault.
First thing first - go through this MS Doc link
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
//..........
var kvUri = "https://YOURVAULTNAME.vault.azure.net/";
var client = new SecretClient(new Uri(kvUri), new DefaultAzureCredential());
KeyVaultSecret secret = client.GetSecret("SECRETNAME");
// Can also use await.....GetSecretAsync()
this.ConnectionString = secret.Value.ToString();
\\thats my internal variable, secret.Value.ToString() is required value
I assume here that you have
Created keyVault in Azure
created a Managed identity in App Service(Using 'Identity' option in app service)
Added above identity in Azure KeyVault('Access policies' option)
I am using something like this,
var keyVaultEndpoint = GetKeyVaultEndpoint();
if (!string.IsNullOrEmpty(keyVaultEndpoint))
{
// Pass appropriate connection string
var azureServiceTokenProvider = new
AzureServiceTokenProvider(certThumbprintConnectionString);
var keyVaultClient = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(
azureServiceTokenProvider.KeyVaultTokenCallback));
config.AddAzureKeyVault(keyVaultEndpoint, keyVaultClient, new DefaultKeyVaultSecretManager());
}
private static string GetKeyVaultEndpoint() => "https://<<key-vault-name>>.vault.azure.net";

How to read key vault secrets through IConfiguration object in local development in Azure web app

I've created a service principal for my local development through the AzureServicesAuthConnectionString environment variable and granted that principal access to the keyvault.
However, when I read configuration["secret"] for a secret in key vault, it is null, and if I inspect the providers in the configuration object, I don't see a keyvault provider, only json providers for my appsettings files. Is there a step that I'm missing?
I create a .net core webapp and test well in my site. Here is the code you could refer to.
In Program.cs:
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.ConfigureAppConfiguration(builder =>
{
var azureServiceTokenProvider = new AzureServiceTokenProvider();
var keyVaultClient =new KeyVaultClient(
new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback));
builder.AddAzureKeyVault("https://yourkeyvaultname.vault.azure.net/",
keyVaultClient, new DefaultKeyVaultSecretManager());
})
.UseStartup<Startup>();
In HomeController.cs:
private readonly IConfiguration _configuration;
public HomeController(IConfiguration configuration)
{
_configuration = configuration;
}
public IActionResult Index()
{
ViewBag.Secret = _configuration["yoursecretname"];
return View();
}
The snapshot:
For more details, you could refer to this article.
You can try the new feature we have to just pump the secret into the App Settings directly.
Here is the blog to show how to set that up. It is easy and works via Template deployments as well.
https://learn.microsoft.com/en-us/azure/app-service/app-service-key-vault-references

ASP.Net Core 2.1 Serilog SQL Server Sink using Azure SQL is working locally but not from Azure app service

I have an ASP.Net Core 2.1 Website that uses Azure SQL Database for the Microsoft Identity component.
I added a Logs table to that database and added Serilog to my website with the SQL Server Sink.
When I run the website locally, while still connected to the Azure SQL database, I can see my log entries in the Logs table just fine. However, when I deploy the website to my Azure App Service, I no longer get any log entries in the Logs table of the database.
Mind you, in the deployed version, I am connecting to and using the Azure SQL database for my MS Identity stuff just fine and I can create new users and edit existing users just fine. So I know the Connection String in my App Service Application Settings is correct.
I have reviewed the Serilog MSSQL Github to compare their configuration recommendations to my own and could not find anything that stood out.
I have this setup working correctly on an ASP.Net Core API that I deploy to another Azure App Service. That service uses a different database but it is on the same SQL Server resource.
I have reviewed the list of SO posts recommended when I started this question with no luck.
I ran the following SQL on the database when I first set up the user account;
EXEC sp_addrolemember N'db_datareader', N'myuser'
EXEC sp_addrolemember N'db_datawriter', N'myuser'
EXEC sp_addrolemember N'db_ddladmin', N'myuser'
And, as I stated, the user account can update and add user data in the AspNetUsers table just fine. So, it doesn't seem like a user account issue.
I have verified that the connection string in my Azure app service DEV deployment slot (the one I am testing), Application Settings, Connection Strings is the exact same as what I have in my local DEV UserSecrets. Plus, again, I can read/write to the AspNet* tables in that same database when deployed to Azure.
Here is my Program.cs class where I set up Serilog;
public class Program
{
public static IConfiguration Configuration { get; } = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true)
.AddUserSecrets<Startup>()
.AddEnvironmentVariables()
.Build();
public static void Main(string[] args)
{
var connectionString = Configuration.GetConnectionString("MyConnectionString");
const string tableName = "Logs";
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithThreadId()
.WriteTo.MSSqlServer(connectionString, tableName)
.CreateLogger();
// TODO Enable to debug any startup Serilog issues. Make sure to comment out for PROD
//Serilog.Debugging.SelfLog.Enable(msg =>
//{
// Debug.Print(msg);
// Debugger.Break();
//});
try
{
Log.Information("Starting Application");
CreateWebHostBuilder(args).Build().Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
}
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseSerilog();
}
}
The only difference between the API that I have deployed in Azure that is writing logs to Azure SQL and this website is that in the API, which is older, I have
public static IWebHost BuildWebHost(string[] args)
in program.cs whereas the newer website has
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
So... any ideas would be greatly appreciated.
[UPDATE 1/23/19]
I added the connection string directly to the
var connectionString
in Program.cs rather than getting it from
Configuration.GetConnectionString("MyConnectionString")
and it started logging to the database.
So it seems the issue is with Program.cs being able to read the connection string from Azure App Service deployment slot, Application Settings, Connection Strings section.
This connection string is being correctly read from Startup.cs and has worked since I first created the website.
So, is there some known issue with Azure not being able to read values from deployment slot Application Settings / Connection Strings from Program.cs?
Since there seems to be an issue with Azure, that it doesn't provide the application settings to the web app until CreateWebHostBuilder is invoked, a straightforward workaround (assuming that hardcoding the connection string in the source code isn't a viable option) would be to configure Serilog to use SqlServer in Startup.cs instead of Program.cs.
If it's important to log the initial events that occur during the app's starting up, Serilog can temporarily be configured in Program.cs to write to a file.
Program.cs:
public class Program
{
public static void Main(string[] args)
{
var AzureLogFilePath = #"D:\home\LogFiles\Application\log.txt";
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.Enrich.FromLogContext()
.WriteTo.File(path: AzureLogFilePath, fileSizeLimitBytes: 1_000_000, rollOnFileSizeLimit: true, shared: true);
.CreateLogger();
try
{
Log.Information("Starting Application");
CreateWebHostBuilder(args).Build().Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
}
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>()
.UseSerilog();
}
Startup.cs:
public class Startup
{
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{env.EnvironmentName ?? "Production"}.json", optional: true)
.AddEnvironmentVariables();
if (env.IsDevelopment()) builder.AddUserSecrets<Startup>(); // according to the docs, AddUserSecrets should be used only in development
Configuration = builder.Build();
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
var connectionString = Configuration.GetConnectionString("MyConnectionString");
const string tableName = "Logs";
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.Enrich.FromLogContext()
.Enrich.WithMachineName()
.Enrich.WithThreadId()
.WriteTo.MSSqlServer(connectionString, tableName)
.CreateLogger();
//...
}
}

Using AddAzureKeyVault makes my application 10 seconds slower

I’m having this very simple .NET Core application:
static void Main(string[] args)
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true);
builder.AddAzureKeyVault("https://MyKeyVault.vault.azure.net");
var stopwatch = new Stopwatch();
stopwatch.Start();
var configuration = builder.Build();
var elapsed = stopwatch.Elapsed;
Console.WriteLine($"Elapsed time: {elapsed.TotalSeconds}");
}
The csproj-file looks like this:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.AzureKeyVault" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="2.1.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.1.1" />
</ItemGroup>
</Project>
My problem is that the application takes about 10 seconds to execute with a debugger attached (about 5 seconds without a debugger). If I remove the line with AddAzureKeyVault the application is executed in less than a second. I know that AddAzureKeyVault will make the application connect to Azure and read values from a key vault but I expected this to be a lot faster.
Is this an expected behaviour? Is there anything I could do to make this faster?
For the Microsoft.Azure.Services.AppAuthentication library, see the original answer. For the newer Azure.Identity library, see Update 2021-03-22.
Original Answer:
Yes, configure the AzureServiceTokenProvider explicitly to use the az cli for authentication. You can do this by setting an environment variable named AzureServicesAuthConnectionString.
Bash:
export AzureServicesAuthConnectionString="RunAs=Developer; DeveloperTool=AzureCli"
PowerShell:
$Env:AzureServicesAuthConnectionString = "RunAs=Developer; DeveloperTool=AzureCli"
Note that the environment variable needs to be set in whatever session you're running your app.
Explanation
The root of the problem is hinted at in MS docs on authentication, which state, "By default, AzureServiceTokenProvider uses multiple methods to retrieve a token."
Of the multiple methods used, az cli authentication is a ways down the list. So the AzureServiceTokenProvider takes some time to try other auth methods higher in the pecking order before finally using the az cli as the token source. Setting the connection string in the environment variable removes the time you spend waiting for other auth methods to fail.
This solution is preferable to hardcoding a clientId and clientSecret not only for convenience, but also because az cli auth doesn't require hardcoding your clientSecret or storing it in plaintext.
UPDATE (2021-03-22)
The Azure.Identity auth provider, compatible with newer Azure client SDKs (like Azure.Security.KeyVault.Secrets) has code-based options (instead of a connection string) to skip certain authentication methods. You can:
set exclusions in the DefaultAzureCredential constructor, or
generate a TokenCredential using more specific class type constructors (see also the auth provider migration chart here).
You could try to get azure keyvault with clientId and clientSecret and it may run faster.
builder.AddAzureKeyVault("https://yourkeyvaultname.vault.azure.net", clientId,clinetSecret);
And I test with it and it costs 3 seconds.
For more details, you could refer to this article.
The previously suggested solutions with clientId and AzureServiceTokenProvider do have an affect in the deprecated packet Microsoft.Azure.KeyVault. But with the new packet Azure.Security.KeyVault.Secrets these solutions are no longer necessary in my measurements.
My solution is to cache the configuration from Azure KeyVault and store that configuration locally. With this solution you will be able to use Azure KeyVault during development and still have a great performance. This following code shows how to do this:
using Azure.Extensions.AspNetCore.Configuration.Secrets;
using Azure.Identity;
using Azure.Security.KeyVault.Secrets;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.Json;
namespace ConfigurationCache
{
public class Program
{
private static readonly Stopwatch Stopwatch = new Stopwatch();
public static void Main(string[] args)
{
Stopwatch.Start();
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureAppConfiguration((ctx, builder) =>
{
builder.AddAzureConfigurationServices();
})
.ConfigureServices((hostContext, services) =>
{
Stopwatch.Stop();
Console.WriteLine($"Start time: {Stopwatch.Elapsed}");
Console.WriteLine($"Config: {hostContext.Configuration.GetSection("ConnectionStrings:MyContext").Value}");
services.AddHostedService<Worker>();
});
}
public static class AzureExtensions
{
public static IConfigurationBuilder AddAzureConfigurationServices(this IConfigurationBuilder builder)
{
// Build current configuration. This is later used to get environment variables.
IConfiguration config = builder.Build();
#if DEBUG
if (Debugger.IsAttached)
{
// If the debugger is attached, we use cached configuration instead of
// configurations from Azure.
AddCachedConfiguration(builder, config);
return builder;
}
#endif
// Add the standard configuration services
return AddAzureConfigurationServicesInternal(builder, config);
}
private static IConfigurationBuilder AddAzureConfigurationServicesInternal(IConfigurationBuilder builder, IConfiguration currentConfig)
{
// Get keyvault endpoint. This is normally an environment variable.
string keyVaultEndpoint = currentConfig["KEYVAULT_ENDPOINT"];
// Setup keyvault services
SecretClient secretClient = new SecretClient(new Uri(keyVaultEndpoint), new DefaultAzureCredential());
builder.AddAzureKeyVault(secretClient, new AzureKeyVaultConfigurationOptions());
return builder;
}
private static void AddCachedConfiguration(IConfigurationBuilder builder, IConfiguration currentConfig)
{
//Setup full path to cached configuration file.
string path = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"MyApplication");
string filename = System.IO.Path.Combine(path, $"configcache.dat");
// If the file does not exists, or is more than 12 hours, update the cached configuration.
if (!System.IO.File.Exists(filename) || System.IO.File.GetLastAccessTimeUtc(filename).AddHours(12) < DateTime.UtcNow)
{
System.IO.Directory.CreateDirectory(path);
UpdateCacheConfiguration(filename, currentConfig);
}
// Read the file
string encryptedFile = System.IO.File.ReadAllText(filename);
// Decrypt the content
string jsonString = Decrypt(encryptedFile);
// Create key-value pairs
var keyVaultPairs = JsonSerializer.Deserialize<Dictionary<string, string>>(jsonString);
// Use the key-value pairs as configuration
builder.AddInMemoryCollection(keyVaultPairs);
}
private static void UpdateCacheConfiguration(string filename, IConfiguration currentConfig)
{
// Create a configuration builder. We will just use this to get the
// configuration from Azure.
ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
// Add the services we want to use.
AddAzureConfigurationServicesInternal(configurationBuilder, currentConfig);
// Build the configuration
IConfigurationRoot azureConfig = configurationBuilder.Build();
// Serialize the configuration to a JSON-string.
string jsonString = JsonSerializer.Serialize(
azureConfig.AsEnumerable().ToDictionary(a => a.Key, a => a.Value),
options: new JsonSerializerOptions()
{
WriteIndented = true
}
);
//Encrypt the string
string encryptedString = Encrypt(jsonString);
// Save the encrypted string.
System.IO.File.WriteAllText(filename, encryptedString);
}
// Replace the following with your favorite encryption code.
private static string Encrypt(string str)
{
return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(str));
}
private static string Decrypt(string str)
{
return System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(str));
}
}
}

Resources