Kralizek / AWSSecretsManagerConfigurationExtensions

This repository contains a provider for Microsoft.Extensions.Configuration that retrieves secrets stored in AWS Secrets Manager.
MIT License
219 stars 43 forks source link

Lambda Caching Layer #87

Open brignolij opened 1 year ago

brignolij commented 1 year ago

Hello,

I'm using AWSSecretsManagerConfigurationExtensions with api running on Lambda. But i'm now facing the issue where the GetSecretValue limit of 10k read/seconds is reached.

I saw AWS provide a cache layer for secret and parameter that can be dirrectly link to a lambda function : https://docs.aws.amazon.com/secretsmanager/latest/userguide/retrieving-secrets_lambda.html

For example you can add this layer to your function with cloudformation like this :

      Layers:
        - arn:aws:lambda:eu-central-1:187925254637:layer:AWS-Parameters-and-Secrets-Lambda-Extension-Arm64:4

Actually AWSSecretsManagerConfigurationExtensions is reading secret at each application start. in the case of .net api running on lambda, each invocations is reading secret.

Can we imagine an implementation of the cache layer of lambda ?

Regards

Kralizek commented 1 year ago

I will have to think at the best way to support this scenario.

brignolij commented 1 year ago

@Kralizek Hi again,

I spend time on this topic.

Using your Extension, we can "use" a custom implementation of GetSecret to call manually the secret layer.

I did it, but the layer seems not working very well. I'm getting error 4xx, not read to serve traffic.

But here how i did it :

public class SecretManagerAdvanced : AmazonSecretsManagerClient
{
    private readonly ISecretsProvider _secretsProvider;
    public SecretManagerAdvanced(AmazonSecretsManagerConfig config) : base(config)
    {
        _secretsProvider = new SecretsProvider();
    }

    public SecretManagerAdvanced(AWSCredentials credentials, AmazonSecretsManagerConfig config) : base(credentials, config)
    {
        _secretsProvider = new SecretsProvider();
    }

    public override async Task<GetSecretValueResponse> GetSecretValueAsync(GetSecretValueRequest request,
        CancellationToken cancellationToken = new CancellationToken())
    {

        var result = _secretsProvider.GetSecretAsync(request.SecretId);

        if (result.Result is not null)
        {
            var response = new GetSecretValueResponse();
            response.Name = request.SecretId;
            response.SecretString = result.Result;
            return response;
        }

        return await base.GetSecretValueAsync(request, cancellationToken);
    }
}
public static class SecretMapper{
///
 private static IAmazonSecretsManager CreateClient()
    {
        var options = new SecretsManagerConfigurationProviderOptions();
        var region = RegionEndpoint.EUCentral1;

       // AWSCredentials? Credentials;

        if (options.CreateClient != null)
        {
            return options.CreateClient();
        }

        var clientConfig = new AmazonSecretsManagerConfig
        {
            RegionEndpoint = region
        };

        options.ConfigureSecretsManagerConfig(clientConfig);

        return Credentials switch
        {
            null => new SecretManagerAdvanced(clientConfig),
            _ => new SecretManagerAdvanced(Credentials, clientConfig)
        };
    }
}

internal interface ISecretsProvider
{
    Task<string?> GetSecretAsync(string secretName);
}

class SecretsProvider : ISecretsProvider
{
    private readonly HttpClient _httpClient;
    private readonly string _token;

    private readonly string GetSecretsEndpoint = "/secretsmanager/get?secretId=";

    public SecretsProvider()
    {
        _httpClient = new HttpClient() { BaseAddress = new Uri("http://localhost:2773") };
        _httpClient.DefaultRequestHeaders.Add("X-AWS-Parameters-Secrets-Token", Environment.GetEnvironmentVariable("AWS_SESSION_TOKEN"));
    }

    public async Task<string?> GetSecretAsync(string secretName)
    {
        var httpRequest = new HttpRequestMessage(
            HttpMethod.Get, 
            new Uri($"{GetSecretsEndpoint}{secretName}", UriKind.Relative));

        var response = await _httpClient
            .SendAsync(httpRequest)
            .ConfigureAwait(false);

        var responseAsJsonString = await response.Content
            .ReadAsStringAsync()
            .ConfigureAwait(false);

        if (response.IsSuccessStatusCode)
        {
            return responseAsJsonString;
        }
        return null;
    }
}

Did you already use lambda secret layer and make it work ?

Regards

Kralizek commented 1 year ago

No I've never used the layer tbh

brignolij commented 1 year ago

@Kralizek according this : https://stackoverflow.com/questions/75624681/aws-secrets-and-parameters-lambda-extension-throw-not-ready-to-serve-traffic

the layer will be ready when the application has already started.

Is there a way to use your Extension after the application start ?

EDIT: I confirm, when calling HTTP GetSecret after application start, for example in a controller, the layer return the secret effectively.

For information, here how it works in lambda:

  1. LAMBDA INIT START is done
  2. dotnet app is started
  3. cache layer is ready
  4. LAMBDA START is done
  5. the http request is treated by dotnet app and return a response
  6. LAMBDA END is done