Azure KeyVault - слишком много подключений из функций Azure

У нас есть некоторые функции Azure, определенные в классе с помощью [FunctionName] атрибуты из SDK WebJobs. В классе есть несколько функций, и всем им нужен доступ к секретам, хранящимся в Azure KeyVault. Проблема в том, что у нас много сотен вызовов функций в минуту, и поскольку каждый из них выполняет вызов KeyVault, KeyVault завершается ошибкой с сообщением, напоминающим: "Слишком много подключений. Обычно разрешено только 10 подключений".

@crandycodes (Крис Андерсон) в Twitter предложил сделать KeyVaultClient статичный. Тем не менее, конструктор, который мы используем для KeyVaultClient требуется функция делегата для конструктора, и вы не можете использовать статический метод в качестве делегата. Так как мы можем сделать KeyVaultClient статические? Это должно позволить функциям совместно использовать клиент, уменьшая количество сокетов.

Вот наш KeyVaultHelper учебный класс:

public class KeyVaultHelper
{
    public string ClientId { get; protected set; }

    public string ClientSecret { get; protected set; }

    public string VaultUrl { get; protected set; }

    public KeyVaultHelper(string clientId, string secret, string vaultName = null)
    {
        ClientId = clientId;
        ClientSecret = secret;
        VaultUrl = vaultName == null ? null : $"https://{vaultName}.vault.azure.net/";
    }

    public async Task<string> GetSecretAsync(string key)
    {
        try
        {

            using (var client = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(GetAccessTokenAsync),
                new HttpClient()))
            {
                var secret = await client.GetSecretAsync(VaultUrl, key);
                return secret.Value;
            }
        }
        catch (Exception ex)
        {
            throw new ApplicationException($"Could not get value for secret {key}", ex);
        }
    }

    public async Task<string> GetAccessTokenAsync(string authority, string resource, string scope)
    {
        var authContext = new AuthenticationContext(authority, TokenCache.DefaultShared);
        var clientCred = new ClientCredential(ClientId, ClientSecret);
        var result = await authContext.AcquireTokenAsync(resource, clientCred);

        if (result == null)
        {
            throw new InvalidOperationException("Could not get token for vault");
        }

        return result.AccessToken;
    }
}

Вот как мы ссылаемся на класс из наших функций:

public class ProcessorEntryPoint
{
    [FunctionName("MyFuncA")]
    public static async Task ProcessA(
        [QueueTrigger("queue-a", Connection = "queues")]ProcessMessage msg,
        TraceWriter log
        )
    {
        var keyVaultHelper = new KeyVaultHelper(CloudConfigurationManager.GetSetting("ClientId"), CloudConfigurationManager.GetSetting("ClientSecret"),
            CloudConfigurationManager.GetSetting("VaultName"));
        var secret = keyVaultHelper.GetSecretAsync("mysecretkey");
        // do a stuff
    }

    [FunctionName("MyFuncB")]
    public static async Task ProcessB(
        [QueueTrigger("queue-b", Connection = "queues")]ProcessMessage msg,
        TraceWriter log
        )
    {
        var keyVaultHelper = new KeyVaultHelper(CloudConfigurationManager.GetSetting("ClientId"), CloudConfigurationManager.GetSetting("ClientSecret"),
            CloudConfigurationManager.GetSetting("VaultName"));
        var secret = keyVaultHelper.GetSecretAsync("mysecretkey");
        // do b stuff
    }
}

Мы могли бы сделать KeyVaultHelper класс static, но для этого, в свою очередь, потребуется статический KeyVaultClient объект, чтобы избежать создания нового соединения при каждом вызове функции - так как мы это делаем или есть другое решение? Мы не можем поверить, что функции, требующие доступа к KeyVault, не масштабируемы!?

3 ответа

Решение

Вы можете использовать кэш-память и установить длительность кэширования на определенное время, которое приемлемо в вашем сценарии. В следующем случае у вас есть скользящий срок действия, вы также можете использовать абсолютный срок действия, в зависимости от того, когда секреты меняются.

public async Task<string> GetSecretAsync(string key)
{
    MemoryCache memoryCache = MemoryCache.Default;
    string mkey = VaultUrl + "_" +key;
    if (!memoryCache.Contains(mkey))
    {
      try
      {

          using (var client = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(GetAccessTokenAsync),
            new HttpClient()))
          {
               memoryCache.Add(mkey, await client.GetSecretAsync(VaultUrl, key), new CacheItemPolicy() { SlidingExpiration = TimeSpan.FromHours(1) });
          }
      }
      catch (Exception ex)
      {
          throw new ApplicationException($"Could not get value for secret {key}", ex);
      }
      return memoryCache[mkey] as string;
    }
}

Попробуйте следующие изменения в помощнике:

public class KeyVaultHelper
{
    public string ClientId { get; protected set; }

    public string ClientSecret { get; protected set; }

    public string VaultUrl { get; protected set; }

    KeyVaultClient client = null;

    public KeyVaultHelper(string clientId, string secret, string vaultName = null)
    {
        ClientId = clientId;
        ClientSecret = secret;
        VaultUrl = vaultName == null ? null : $"https://{vaultName}.vault.azure.net/";
        client = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(GetAccessTokenAsync), new HttpClient());
    }

    public async Task<string> GetSecretAsync(string key)
    {
        try
        {
            if (client == null)
                client = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(GetAccessTokenAsync), new HttpClient());

            var secret = await client.GetSecretAsync(VaultUrl, key);
            return secret.Value;
        }
        catch (Exception ex)
        {
            if (client != null)
            {
                client.Dispose();
                client = null;
            }
            throw new ApplicationException($"Could not get value for secret {key}", ex);
        }
    }

    public async Task<string> GetAccessTokenAsync(string authority, string resource, string scope)
    {
        var authContext = new AuthenticationContext(authority, TokenCache.DefaultShared);
        var clientCred = new ClientCredential(ClientId, ClientSecret);
        var result = await authContext.AcquireTokenAsync(resource, clientCred);

        if (result == null)
        {
            throw new InvalidOperationException("Could not get token for vault");
        }

        return result.AccessToken;
    }
}

теперь функция может использовать статический конструктор по умолчанию для хранения прокси клиента:

public static class ProcessorEntryPoint
{
    static KeyVaultHelper keyVaultHelper;

    static ProcessorEntryPoint()
    {
        keyVaultHelper = new KeyVaultHelper(CloudConfigurationManager.GetSetting("ClientId"), CloudConfigurationManager.GetSetting("ClientSecret"), CloudConfigurationManager.GetSetting("VaultName"));
    }

    [FunctionName("MyFuncA")]
    public static async Task ProcessA([QueueTrigger("queue-a", Connection = "queues")]ProcessMessage msg, TraceWriter log )
    {           
        var secret = keyVaultHelper.GetSecretAsync("mysecretkey");
        // do a stuff

    }

    [FunctionName("MyFuncB")]
    public static async Task ProcessB([QueueTrigger("queue-b", Connection = "queues")]ProcessMessage msg, TraceWriter log )
    {
        var secret = keyVaultHelper.GetSecretAsync("mysecretkey");
        // do b stuff

    }
}

Вы на самом деле не хотите, чтобы KeyVault так масштабировался. Он защищает вас от ненужных затрат и медленного поведения. Все, что вам нужно сделать, это сохранить секрет для дальнейшего использования. Я создал статический класс для статической реализации.

public static class KeyVaultHelper
{
    private static Dictionary<string, string> Cache = new Dictionary<string, string>();

    public static async Task<string> GetSecretAsync(string secretIdentifier)
    {
        if (Cache.ContainsKey(secretIdentifier))
            return Cache[secretIdentifier];

        var kv = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(GetToken));
        var secretValue = (await kv.GetSecretAsync(secretIdentifier)).Value;
        Cache[secretIdentifier] = secretValue;
        return secretValue;
    }

    private static async Task<string> GetToken(string authority, string resource, string scope)
    {
        var clientId = ConfigurationManager.AppSettings["ClientID"];
        var clientSecret = ConfigurationManager.AppSettings["ClientSecret"];
        var clientCred = new ClientCredential(clientId, clientSecret);

        var authContext = new AuthenticationContext(authority);
        AuthenticationResult result = await authContext.AcquireTokenAsync(resource, clientCred);

        if (result == null)
            throw new InvalidOperationException("Failed to obtain the JWT token");

        return result.AccessToken;
    }
}

Теперь в вашем коде вы можете сделать что-то вроде этого:

private static readonly string ConnectionString = KeyVaultHelper.GetSecretAsync(ConfigurationManager.AppSettings["SqlConnectionSecretUri"]).GetAwaiter().GetResult();

Теперь, когда вам нужен ваш секрет, он сразу же там.

ПРИМЕЧАНИЕ. Если функции Azure отключают экземпляр из-за неиспользования, статическое состояние исчезает и перезагружается при следующем вызове функции. Или вы можете использовать свой собственный функционал для перезагрузки статики.

Другие вопросы по тегам