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.
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.
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.
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));
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With