Azure KeyVault - 来自Azure函数的太多连接

问题描述:

我们已经使用WebJobs SDK中的[FunctionName]属性在类中定义了一些Azure函数。该类有几个功能,他们都需要访问存储在Azure KeyVault中的秘密。问题是我们每分钟都有数百次的函数调用,并且由于每个函数都在调用KeyVault,因此KeyVault失败时会显示一条消息,如“连接太多,通常只允许10个连接”。Azure KeyVault - 来自Azure函数的太多连接

@crandycodes(Chris Anderson)在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访问的功能不可扩展!?

您可以使用内存缓存并将缓存的长度设置为您的方案中可接受的某个时间。在以下情况下,您有滑动过期,您也可以使用绝对过期,具体取决于秘密更改的时间。

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; 
    } 
} 
+0

感谢@Peter - 我们结束做这样的事情。可惜的是,你无法扩展需要KeyVault的函数 - 看起来有点棘手的问题。 MemCache感觉很糟糕。 –

尝试在助手以下变化:

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 

    } 
}