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 отключают экземпляр из-за неиспользования, статическое состояние исчезает и перезагружается при следующем вызове функции. Или вы можете использовать свой собственный функционал для перезагрузки статики.