Lambda Powertools for .NET: Fetching Data from AWS Systems Manager Parameter Store and AWS Secrets Manager

Lambda Powertools for .NET: Fetching Data from AWS Systems Manager Parameter Store and AWS Secrets Manager

The external configuration storage pattern is widely used today. To support this, AWS provides a range of services for storing and managing configuration data, including:

  • AWS Systems Manager Parameter Store offers secure, scalable, centralized, and hierarchical storage for configuration data and secrets management. It can be used as a simple, low-cost solution for storing and managing configuration data and secrets when automatic secret rotation is not required.

  • AWS Secrets Manager was designed specifically for confidential information. It provides a secure and scalable solution to store, retrieve, and rotate secrets. Keep in mind that Secrets Manager comes with a higher cost compared to Parameter Store.

  • AWS AppConfig is a service that enables us to manage and deploy configuration data. This service is ideal for situations where our configuration data updates and we need a rollback mechanism in case something goes wrong.

The AWS Lambda Powertools Parameters utility simplifies working with the first two services by offering high-level functionality for retrieving configuration data. In this post, we will explore the features offered by the library while building AWS Lambda functions to retrieve values from AWS Systems Manager Parameter Store and AWS Secrets Manager.

Pre-requisites

  • Have an IAM User with programmatic access.

  • Install the Amazon Lambda Templates (dotnet new -i Amazon.Lambda.Templates)

  • Install the Amazon Lambda Tools (dotnet tool install -g Amazon.Lambda.Tools)

  • Install AWS SAM CLI.

Lambda Function

Run the following commands to set up our project:

dotnet new lambda.EmptyFunction -n MyLambda -o .
dotnet add src/MyLambda package Amazon.Lambda.APIGatewayEvents
dotnet add src/MyLambda package AWS.Lambda.Powertools.Parameters
dotnet new sln -n PowerTools
dotnet sln add --in-root src/MyLambda

Open the solution and modify the Function.cs file as follows:

using Amazon.Lambda.APIGatewayEvents;
using Amazon.Lambda.Core;
using System.Text.Json;
// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace MyLambda;
public class Function
{
    public async Task<APIGatewayHttpApiV2ProxyResponse> FunctionHandler(APIGatewayHttpApiV2ProxyRequest input, ILambdaContext context)
    {
        var body = JsonSerializer.Serialize(new Response()
        {
        });

        return new APIGatewayHttpApiV2ProxyResponse
        {
            Body = body,
            StatusCode = 200,
            Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
        };
    }

    public class Response
    {
    }
}

At the solution level, create a template.yml file as follows:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  PowerTools Parameters

Resources:
  MyLambda:
    Type: AWS::Serverless::Function
    Properties:
      Timeout: 60
      MemorySize: 512
      Tracing: Active
      Runtime: dotnet6
      Architectures:
        - x86_64    
      Handler: MyLambda::MyLambda.Function::FunctionHandler
      CodeUri: ./src/MyLambda/
      Events:
        ListPosts:
          Type: Api
          Properties:
            Path: /api
            Method: get

Outputs:
  TaskApi:
    Description: "API Gateway endpoint URL"
    Value: 
      Fn::Sub:
        - https://${ApiId}.execute-api.${AWS::Region}.amazonaws.com/Prod/api
        - ApiId: 
            Ref: ServerlessRestApi

At the solution level, run the following commands to deploy the function:

sam build
sam deploy --guided

Parameter Store

Retrieving a Single Parameter

Modify the template.yml file as follows:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  PowerTools Parameters

Resources:
  BasicParameter:
    Type: AWS::SSM::Parameter
    Properties:
      Name: 'basicparameter'
      Type: String
      Value: 'Hello'
      Description: SSM Basic Parameter

  MyLambda:
    Type: AWS::Serverless::Function
    Properties:
      Timeout: 60
      MemorySize: 512
      Tracing: Active
      Runtime: dotnet6
      Architectures:
        - x86_64    
      Handler: MyLambda::MyLambda.Function::FunctionHandler
      CodeUri: ./src/MyLambda/
      Events:
        ListPosts:
          Type: Api
          Properties:
            Path: /api
            Method: get
      Environment:
        Variables:
          SecretArn: 
            Ref: SecretParameter
      Policies:
        - SSMParameterReadPolicy:
            ParameterName:
              Ref: BasicParameter

Outputs:
  TaskApi:
    Description: "API Gateway endpoint URL"
    Value: 
      Fn::Sub:
        - https://${ApiId}.execute-api.${AWS::Region}.amazonaws.com/Prod/api
        - ApiId: 
            Ref: ServerlessRestApi

A new resource AWS::SSM::Parameter was added, and the corresponding permissions were granted to the function through the SSMParameterReadPolicy policy template. Open the Function.cs file and modify the Function class as follows:

public class Function
{
    public async Task<APIGatewayHttpApiV2ProxyResponse> FunctionHandler(APIGatewayHttpApiV2ProxyRequest input, ILambdaContext context)
    {
        var ssmProvider = ParametersManager.SsmProvider;

        var value = await ssmProvider
            .GetAsync("basicparameter");

        var body = JsonSerializer.Serialize(new Response()
        {
            BasicParameterValue = value,
        });

        return new APIGatewayHttpApiV2ProxyResponse
        {
            Body = body,
            StatusCode = 200,
            Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
        };
    }

    public class Response
    {
        public string? BasicParameterValue { get; set; }
    }
}

The ParametersManager static class contains a SsmProvider class that provides access to the GetAsync method for retrieving a single value.

Retrieving a Multiple Parameters

We can use parameter hierarchies to help organize and manage parameters. A hierarchy consists of a parameter name that includes a path, defined by forward slashes. Modify the template.yml file as follows:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  PowerTools Parameters

Resources:
  BasicParameter:
    Type: AWS::SSM::Parameter
    Properties:
      Name: 'basicparameter'
      Type: String
      Value: 'Hello'
      Description: SSM Basic Parameter

  MultipleParameter1:
    Type: AWS::SSM::Parameter
    Properties:
      Name: '/mylambda/multipleparameter1'
      Type: String
      Value: 'Parameter 1 Value'
      Description: SSM Multiple Parameter

  MultipleParameter2:
    Type: AWS::SSM::Parameter
    Properties:
      Name: '/mylambda/multipleparameter2'
      Type: String
      Value: 'Parameter 2 Value'
      Description: SSM Multiple Parameter

  MyLambda:
    Type: AWS::Serverless::Function
    Properties:
      Timeout: 60
      MemorySize: 512
      Tracing: Active
      Runtime: dotnet6
      Architectures:
        - x86_64    
      Handler: MyLambda::MyLambda.Function::FunctionHandler
      CodeUri: ./src/MyLambda/
      Events:
        ListPosts:
          Type: Api
          Properties:
            Path: /api
            Method: get
      Environment:
        Variables:
          SecretArn: 
            Ref: SecretParameter
      Policies:
        - SSMParameterReadPolicy:
            ParameterName:
              Ref: BasicParameter
        - SSMParameterWithSlashPrefixReadPolicy:
            ParameterName: '/mylambda'

Outputs:
  TaskApi:
    Description: "API Gateway endpoint URL"
    Value: 
      Fn::Sub:
        - https://${ApiId}.execute-api.${AWS::Region}.amazonaws.com/Prod/api
        - ApiId: 
            Ref: ServerlessRestApi

In this case, permissions were granted using the SSMParameterWithSlashPrefixReadPolicy policy template. Open the Function.cs file and modify the Function class as follows:

public class Function
{
    public async Task<APIGatewayHttpApiV2ProxyResponse> FunctionHandler(APIGatewayHttpApiV2ProxyRequest input, ILambdaContext context)
    {
        var ssmProvider = ParametersManager.SsmProvider;

        var value = await ssmProvider
            .GetAsync("basicparameter");

        var values = await ssmProvider
            .Recursive()
            .GetMultipleAsync("/mylambda");

        var body = JsonSerializer.Serialize(new Response()
        {
            BasicParameterValue = value,
            MultipleParameterValues = values,
        });

        return new APIGatewayHttpApiV2ProxyResponse
        {
            Body = body,
            StatusCode = 200,
            Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
        };
    }

    public class Response
    {
        public string? BasicParameterValue { get; set; }
        public IDictionary<string, string?>? MultipleParameterValues { get; set; }
    }
}

The SsmProvider class offers access to the Recursive method, which allows access to the GetMultipleAsync method for retrieving multiple values. Each retrieved item will consist of the parameter name and its corresponding value.

Retrieving a Secure Parameter

The resource AWS::SSM::Parameter does not support the creation of SecureString parameters. In this case, we will use the following command to create the resource:

aws ssm put-parameter --name "secureparameter" --type SecureString --value "ch4ng1ng-s3cr3t"

Modify the template.yml file as follows:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  PowerTools Parameters

Resources:
  BasicParameter:
    Type: AWS::SSM::Parameter
    Properties:
      Name: 'basicparameter'
      Type: String
      Value: 'Hello'
      Description: SSM Basic Parameter

  MultipleParameter1:
    Type: AWS::SSM::Parameter
    Properties:
      Name: '/mylambda/multipleparameter1'
      Type: String
      Value: 'Parameter 1 Value'
      Description: SSM Multiple Parameter

  MultipleParameter2:
    Type: AWS::SSM::Parameter
    Properties:
      Name: '/mylambda/multipleparameter2'
      Type: String
      Value: 'Parameter 2 Value'
      Description: SSM Multiple Parameter

  MyLambda:
    Type: AWS::Serverless::Function
    Properties:
      Timeout: 60
      MemorySize: 512
      Tracing: Active
      Runtime: dotnet6
      Architectures:
        - x86_64    
      Handler: MyLambda::MyLambda.Function::FunctionHandler
      CodeUri: ./src/MyLambda/
      Events:
        ListPosts:
          Type: Api
          Properties:
            Path: /api
            Method: get
      Environment:
        Variables:
          SecretArn: 
            Ref: SecretParameter
      Policies:
        - SSMParameterReadPolicy:
            ParameterName:
              Ref: BasicParameter
        - SSMParameterWithSlashPrefixReadPolicy:
            ParameterName: '/mylambda'
        - SSMParameterReadPolicy:
            ParameterName: 'secureparameter'

Outputs:
  TaskApi:
    Description: "API Gateway endpoint URL"
    Value: 
      Fn::Sub:
        - https://${ApiId}.execute-api.${AWS::Region}.amazonaws.com/Prod/api
        - ApiId: 
            Ref: ServerlessRestApi

The permissions were granted using the SSMParameterReadPolicy policy template. Open the Function.cs file and modify the Function class as follows:

public class Function
{
    public async Task<APIGatewayHttpApiV2ProxyResponse> FunctionHandler(APIGatewayHttpApiV2ProxyRequest input, ILambdaContext context)
    {
        var ssmProvider = ParametersManager.SsmProvider;

        var value = await ssmProvider
            .GetAsync("basicparameter");

        var values = await ssmProvider
            .Recursive()
            .GetMultipleAsync("/mylambda");

        var secureValue = await ssmProvider
            .WithDecryption()
            .GetAsync("secureparameter");

        stopwatch.Stop();

        var body = JsonSerializer.Serialize(new Response()
        {
            BasicParameterValue = value,
            MultipleParameterValues = values,
            SecureParameterValue = secureValue,
        });

        return new APIGatewayHttpApiV2ProxyResponse
        {
            Body = body,
            StatusCode = 200,
            Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
        };
    }

    public class Response
    {
        public string? BasicParameterValue { get; set; }
        public IDictionary<string, string?>? MultipleParameterValues { get; set; }
        public string? SecureParameterValue { get; set; }
    }
}

The SsmProvider class offers access to the WithDecryption method, which allows access to the GetAsync method for retrieving a single decrypted value.

Secret Manager

Modify the template.yml file as follows:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  PowerTools Parameters

Resources:
  BasicParameter:
    Type: AWS::SSM::Parameter
    Properties:
      Name: 'basicparameter'
      Type: String
      Value: 'Hello'
      Description: SSM Basic Parameter

  MultipleParameter1:
    Type: AWS::SSM::Parameter
    Properties:
      Name: '/mylambda/multipleparameter1'
      Type: String
      Value: 'Parameter 1 Value'
      Description: SSM Multiple Parameter

  MultipleParameter2:
    Type: AWS::SSM::Parameter
    Properties:
      Name: '/mylambda/multipleparameter2'
      Type: String
      Value: 'Parameter 2 Value'
      Description: SSM Multiple Parameter

  SecretParameter:
    Type: AWS::SecretsManager::Secret
    Properties:
      Description: Secrets Manager Secret
      Name: 'secret'
      GenerateSecretString:
        PasswordLength: 16

  MyLambda:
    Type: AWS::Serverless::Function
    Properties:
      Timeout: 60
      MemorySize: 512
      Tracing: Active
      Runtime: dotnet6
      Architectures:
        - x86_64    
      Handler: MyLambda::MyLambda.Function::FunctionHandler
      CodeUri: ./src/MyLambda/
      Events:
        ListPosts:
          Type: Api
          Properties:
            Path: /api
            Method: get
      Environment:
        Variables:
          SecretArn: 
            Ref: SecretParameter
      Policies:
        - SSMParameterReadPolicy:
            ParameterName:
              Ref: BasicParameter
        - SSMParameterWithSlashPrefixReadPolicy:
            ParameterName: '/mylambda'
        - SSMParameterReadPolicy:
            ParameterName: 'secureparameter'
        - AWSSecretsManagerGetSecretValuePolicy:
            SecretArn: 
              Ref: SecretParameter

Outputs:
  TaskApi:
    Description: "API Gateway endpoint URL"
    Value: 
      Fn::Sub:
        - https://${ApiId}.execute-api.${AWS::Region}.amazonaws.com/Prod/api
        - ApiId: 
            Ref: ServerlessRestApi

A new resource AWS::SecretsManager::Secret was added, and the corresponding permissions were granted to the function through the AWSSecretsManagerGetSecretValuePolicy policy template. Open the Function.cs file and modify the Function class as follows:

public class Function
{
    public async Task<APIGatewayHttpApiV2ProxyResponse> FunctionHandler(APIGatewayHttpApiV2ProxyRequest input, ILambdaContext context)
    {
        var ssmProvider = ParametersManager.SsmProvider;

        var value = await ssmProvider
            .GetAsync("basicparameter");

        var values = await ssmProvider
            .Recursive()
            .GetMultipleAsync("/mylambda");

        var secureValue = await ssmProvider
            .WithDecryption()
            .GetAsync("secureparameter");

        var secretsProvider = ParametersManager.SecretsProvider;

        var secret = await secretsProvider
            .GetAsync("secret");

        var body = JsonSerializer.Serialize(new Response()
        {
            BasicParameterValue = value,
            MultipleParameterValues = values,
            SecureParameterValue = secureValue,
            Secret = secret
        });

        return new APIGatewayHttpApiV2ProxyResponse
        {
            Body = body,
            StatusCode = 200,
            Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
        };
    }

    public class Response
    {
        public string? BasicParameterValue { get; set; }
        public IDictionary<string, string?>? MultipleParameterValues { get; set; }
        public string? SecureParameterValue { get; set; }
        public string? Secret { get; set; }
    }
}

The ParametersManager static class contains a SecretsProvider class that provides access to the GetAsync method for retrieving a single value.

Transformations

Parameter values can be transformed using the WithTransformation method. The supported formats include Base64 and JSON. Modify the template.yml file as follows:

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  PowerTools Parameters

Resources:
  BasicParameter:
    Type: AWS::SSM::Parameter
    Properties:
      Name: 'basicparameter'
      Type: String
      Value: 'Hello'
      Description: SSM Basic Parameter

  MultipleParameter1:
    Type: AWS::SSM::Parameter
    Properties:
      Name: '/mylambda/multipleparameter1'
      Type: String
      Value: 'Parameter 1 Value'
      Description: SSM Multiple Parameter

  MultipleParameter2:
    Type: AWS::SSM::Parameter
    Properties:
      Name: '/mylambda/multipleparameter2'
      Type: String
      Value: 'Parameter 2 Value'
      Description: SSM Multiple Parameter

  SecretParameter:
    Type: AWS::SecretsManager::Secret
    Properties:
      Description: Secrets Manager Secret
      Name: 'secret'
      GenerateSecretString:
        PasswordLength: 16

  JsonParameter:
    Type: AWS::SSM::Parameter
    Properties:
      Name: 'jsonparameter'
      Type: String
      Value: "{\"Parameter1\":\"value1\", \"Parameter2\":\"value2\"}"
      Description: SSM Json Parameter

  MyLambda:
    Type: AWS::Serverless::Function
    Properties:
      Timeout: 60
      MemorySize: 512
      Tracing: Active
      Runtime: dotnet6
      Architectures:
        - x86_64    
      Handler: MyLambda::MyLambda.Function::FunctionHandler
      CodeUri: ./src/MyLambda/
      Events:
        ListPosts:
          Type: Api
          Properties:
            Path: /api
            Method: get
      Environment:
        Variables:
          SecretArn: 
            Ref: SecretParameter
      Policies:
        - SSMParameterReadPolicy:
            ParameterName:
              Ref: BasicParameter
        - SSMParameterWithSlashPrefixReadPolicy:
            ParameterName: '/mylambda'
        - SSMParameterReadPolicy:
            ParameterName: 'secureparameter'
        - AWSSecretsManagerGetSecretValuePolicy:
            SecretArn: 
              Ref: SecretParameter
        - SSMParameterReadPolicy:
            ParameterName:
              Ref: JsonParameter

Outputs:
  TaskApi:
    Description: "API Gateway endpoint URL"
    Value: 
      Fn::Sub:
        - https://${ApiId}.execute-api.${AWS::Region}.amazonaws.com/Prod/api
        - ApiId: 
            Ref: ServerlessRestApi

A new resource AWS::SSM::Parameter was added, but this time the value is a JSON object. In addition, the corresponding permissions were granted to the function. Open the Function.cs file and modify the Function class as follows:

public class Function
{
    public async Task<APIGatewayHttpApiV2ProxyResponse> FunctionHandler(APIGatewayHttpApiV2ProxyRequest input, ILambdaContext context)
    {
        var ssmProvider = ParametersManager.SsmProvider;

        var value = await ssmProvider
            .GetAsync("basicparameter");

        var values = await ssmProvider
            .Recursive()
            .GetMultipleAsync("/mylambda");

        var secureValue = await ssmProvider
            .WithDecryption()
            .GetAsync("secureparameter");

        var secretsProvider = ParametersManager.SecretsProvider;

        var secret = await secretsProvider
            .GetAsync("secret");

        var configuration = await ssmProvider
            .WithTransformation(Transformation.Json)
            .GetAsync<Configuration>("jsonparameter");

        var body = JsonSerializer.Serialize(new Response()
        {
            BasicParameterValue = value,
            MultipleParameterValues = values,
            SecureParameterValue = secureValue,
            Secret = secret,
            Configuration = configuration
        });

        return new APIGatewayHttpApiV2ProxyResponse
        {
            Body = body,
            StatusCode = 200,
            Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
        };
    }

    public class Response
    {
        public string? BasicParameterValue { get; set; }
        public IDictionary<string, string?>? MultipleParameterValues { get; set; }
        public string? SecureParameterValue { get; set; }
        public Configuration? Configuration { get; set; }
        public string? Secret { get; set; }
    }

    public record Configuration(string Parameter1, string Parameter2);
}

Deploy the application by running the commands sam build and sam deploy.

Caching

By default, all parameters and their corresponding values are cached for five seconds. However, we can customize the duration using the DefaultMaxAge method.

public class Function
{
    public async Task<APIGatewayHttpApiV2ProxyResponse> FunctionHandler(APIGatewayHttpApiV2ProxyRequest input, ILambdaContext context)
    {
        var stopwatch = new Stopwatch();
        stopwatch.Start();

        var ssmProvider = ParametersManager.SsmProvider;

        var value = await ssmProvider
            .DefaultMaxAge(TimeSpan.FromMinutes(1))
            .GetAsync("basicparameter");

        var values = await ssmProvider
            .Recursive()
            .GetMultipleAsync("/mylambda");

        var secureValue = await ssmProvider
            .WithDecryption()
            .GetAsync("secureparameter");

        var secretsProvider = ParametersManager.SecretsProvider;

        var secret = await secretsProvider
            .DefaultMaxAge(TimeSpan.FromMinutes(1))
            .GetAsync("secret");

        var configuration = await ssmProvider
            .WithTransformation(Transformation.Json)
            .GetAsync<Configuration>("jsonparameter");

        stopwatch.Stop();

        var body = JsonSerializer.Serialize(new Response()
        {
            BasicParameterValue = value,
            MultipleParameterValues = values,
            SecureParameterValue = secureValue,
            Secret = secret,
            Configuration = configuration,
            ElapsedMilliseconds = stopwatch.ElapsedMilliseconds
        });

        return new APIGatewayHttpApiV2ProxyResponse
        {
            Body = body,
            StatusCode = 200,
            Headers = new Dictionary<string, string> { { "Content-Type", "application/json" } }
        };
    }

    public class Response
    {
        public string? BasicParameterValue { get; set; }
        public IDictionary<string, string?>? MultipleParameterValues { get; set; }
        public string? SecureParameterValue { get; set; }
        public Configuration? Configuration { get; set; }
        public long ElapsedMilliseconds { get; set; }
        public string? Secret { get; set; }
    }

    public record Configuration(string Parameter1, string Parameter2);
}

We set the cache for both providers to 1 minute. Deploy the application by running the commands sam build and sam deploy.

Disclaimer

During the Lambda execution lifecycle, there are two phases where a parameter can be read:

  • Init phase: This approach reduces API calls, as they are not made during every invocation. However, this can result in outdated parameters and potentially different values across concurrent execution environments.

  • During the invocation: This approach keeps the value up to date but may result in higher retrieval costs and longer function durations due to the additional API call during every invocation.

Choose wisely according to your needs.

It's worth mentioning that, in addition to the Parameter Store and Secret Manager providers, the DynamoDB provider is also available. Furthermore, if that's not enough, it's quite easy to create our own provider.

All the code can be found here. Thanks, and happy coding.