Azure KeyVault - 来自Azure Functions的连接太多

问题描述 投票:4回答:3

我们使用WebJobs SDK中的[FunctionName]属性在类中定义了一些Azure函数。该类中有多个函数,它们都需要访问存储在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类静态,但反过来需要一个静态的KeyVaultClient对象来避免在每个函数调用上创建一个新连接 - 那么我们如何做到这一点还是有其他解决方案?我们无法相信需要KeyVault访问的功能无法扩展!?

azure azure-functions azure-keyvault
3个回答
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;
    }
}

1
投票

在帮助程序中尝试以下更改:

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

    }
}

0
投票

您实际上并不希望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 Functions由于缺少使用而关闭实例,则静态消失并在下次调用该函数时重新加载。或者您可以使用自己的功能重新加载静态。

© www.soinside.com 2019 - 2024. All rights reserved.