使用 AddAzureKeyVault 使我的应用程序慢 10 秒

Using AddAzureKeyVault makes my application 10 seconds slower

我有这个非常简单的 .NET Core 应用程序:

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

csproj 文件如下所示:

<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>

我的问题是应用程序在连接调试器的情况下执行大约需要 10 秒(没有调试器大约需要 5 秒)。如果我删除带有 AddAzureKeyVault 的行,应用程序将在不到一秒的时间内执行。我知道 AddAzureKeyVault 将使应用程序连接到 Azure 并从密钥保管库读取值,但我预计这会快得多。

这是预期的行为吗?我可以做些什么来加快速度吗?

您可以尝试使用 clientId 和 clientSecret 获取 azure keyvault,它可能 运行 更快。

builder.AddAzureKeyVault("https://yourkeyvaultname.vault.azure.net", clientId,clinetSecret);

我用它测试了 3 秒。

更多细节,你可以参考这个article

对于 Microsoft.Azure.Services.AppAuthentication library, see the original answer. For the newer Azure.Identity 库,请参阅更新 2021-03-22。


原答案:

是的,明确配置 AzureServiceTokenProvider 以使用 az cli 进行身份验证。您可以通过设置名为 AzureServicesAuthConnectionString.

的环境变量来执行此操作

Bash:

export AzureServicesAuthConnectionString="RunAs=Developer; DeveloperTool=AzureCli"

PowerShell:

$Env:AzureServicesAuthConnectionString = "RunAs=Developer; DeveloperTool=AzureCli"

请注意,无论您在哪个会话中都需要设置环境变量 运行 您的应用程序。

说明

MS docs on authentication 中暗示了问题的根源,即“默认情况下,AzureServiceTokenProvider 使用多种方法来检索令牌。”

在使用的多种方法中,az cli 身份验证是列表中的一种方法。因此 AzureServiceTokenProvider 在最终使用 az cli 作为令牌源之前需要一些时间来尝试优先级更高的其他身份验证方法。在环境变量中设置连接字符串可以消除您等待其他身份验证方法失败所花费的时间。

此解决方案优于对 clientIdclientSecret 进行硬编码,不仅为了方便,而且因为 az cli auth 不需要对 clientSecret 进行硬编码或存储明文。


更新 (2021-03-22)

与较新的 Azure 客户端 SDK(如 Azure.Security.KeyVault.Secrets)兼容的 Azure.Identity 身份验证提供程序具有基于代码的选项(而不是连接字符串)来跳过某些身份验证方法。您可以:

  1. DefaultAzureCredential constructor

    中设置排除项
  2. 使用更多 specific class type constructors (see also the auth provider migration chart here).

    生成 TokenCredential

之前建议的带有 clientId 和 AzureServiceTokenProvider 的解决方案确实对已弃用的数据包有影响Microsoft.Azure.KeyVault. But with the new packet Azure.Security.KeyVault.Secrets这些解决方案在我的测量中不再需要。

我的解决方案是从 Azure KeyVault 缓存配置并将该配置存储在本地。通过此解决方案,您将能够在开发过程中使用 Azure KeyVault,并且仍然拥有出色的性能。以下代码显示了如何执行此操作:

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