azure.net-coreazure-keyvault

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?


Solution

  • 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));
            }
        }
    }