Azure KeyVault - 来自 Azure Functions 的连接太多
Azure KeyVault - too many connections from Azure Functions
我们使用 WebJobs SDK 中的 [FunctionName]
属性在 class 中定义了一些 Azure Functions。 class 中有几个函数,它们都需要访问存储在 Azure KeyVault 中的秘密。问题是我们每分钟调用数百次函数,并且由于每个调用都在调用 KeyVault,KeyVault 失败并显示类似 "Too many connections. Usually only 10 connections are allowed."
的消息
@crandycodes
(Chris Anderson) 在 Twitter 上建议将 KeyVaultClient
设为静态。但是,我们为 KeyVaultClient
使用的构造函数需要构造函数的委托函数,并且不能将静态方法用作委托。那么我们怎样才能使 KeyVaultClient
静态呢?这应该允许函数共享客户端,减少套接字的数量。
这是我们的 KeyVaultHelper
class:
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;
}
}
以下是我们如何从我们的函数中引用 class:
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
class 静态化,但这又需要一个静态 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;
}
}
尝试在助手中进行以下更改:
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 像那样扩展。它可以保护您免受不必要的成本和缓慢的行为。您需要做的就是保存秘密以备后用。我为静态实例化创建了一个静态 class。
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 由于未使用而关闭实例,则静态会消失并在下次调用该函数时重新加载。或者您可以使用自己的功能来重新加载静态。
我们使用 WebJobs SDK 中的 [FunctionName]
属性在 class 中定义了一些 Azure Functions。 class 中有几个函数,它们都需要访问存储在 Azure KeyVault 中的秘密。问题是我们每分钟调用数百次函数,并且由于每个调用都在调用 KeyVault,KeyVault 失败并显示类似 "Too many connections. Usually only 10 connections are allowed."
@crandycodes
(Chris Anderson) 在 Twitter 上建议将 KeyVaultClient
设为静态。但是,我们为 KeyVaultClient
使用的构造函数需要构造函数的委托函数,并且不能将静态方法用作委托。那么我们怎样才能使 KeyVaultClient
静态呢?这应该允许函数共享客户端,减少套接字的数量。
这是我们的 KeyVaultHelper
class:
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;
}
}
以下是我们如何从我们的函数中引用 class:
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
class 静态化,但这又需要一个静态 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;
}
}
尝试在助手中进行以下更改:
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 像那样扩展。它可以保护您免受不必要的成本和缓慢的行为。您需要做的就是保存秘密以备后用。我为静态实例化创建了一个静态 class。
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 由于未使用而关闭实例,则静态会消失并在下次调用该函数时重新加载。或者您可以使用自己的功能来重新加载静态。